@lamemind/loom-deck 0.63.0 → 0.64.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 +35 -2
- package/dist/cli.js +19 -0
- package/dist/frame.js +42 -3
- package/dist/gitlink.js +210 -0
- package/dist/input.js +13 -0
- package/dist/overlays/gitlink.js +154 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni
|
|
|
74
74
|
| `⏎` | azione primaria del pane | Tasks → apre il **detail** della task selezionata · Sessions → riprende (`claude --resume`) la sessione selezionata |
|
|
75
75
|
| **MAIUSCOLA** | **apre un modale** | cattura tutti i tasti; `esc` annulla, non esce |
|
|
76
76
|
| minuscola | azione immediata, one-shot | |
|
|
77
|
-
| `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt · `^G`/`^O` = genera/apri il project status |
|
|
77
|
+
| `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt · `^G`/`^O` = genera/apri il project status · `^U` = update del gitlink |
|
|
78
78
|
| `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
|
|
79
79
|
| `esc` | chiude l'overlay aperto | in modalità normale è inerte: **nessuna lettera chiude il deck** |
|
|
80
80
|
| `^C` `^C` | chiude il deck | la prima pressione avverte in riga di stato, la seconda entro **5 secondi** esce; passata la finestra si riparte dall'avviso. Da dentro un modale il primo `^C` riporta alla lista — l'avviso vive nella riga di stato, che le schermate a pieno frame non disegnano |
|
|
@@ -96,6 +96,7 @@ Assegnazioni correnti:
|
|
|
96
96
|
| modale | `CANC` | **elimina** — la task selezionata; su una riga meta della vista `archiviabili`, l'intero insieme |
|
|
97
97
|
| modale | `^O` | apre il **project status** in cache (viewer fullscreen) |
|
|
98
98
|
| immediata | `^G` | **genera** il project status (skill headless, dura minuti) |
|
|
99
|
+
| immediata | `^U` | **bumpa il gitlink** dei submodule idonei (solo su un progetto con `.gitmodules`) |
|
|
99
100
|
| immediata | `^K` | spawna la task selezionata col prompt di **recap** |
|
|
100
101
|
| immediata | `^P` | spawna la task selezionata sul **preflight** |
|
|
101
102
|
| immediata | `^R` | spawna la task selezionata in **esecuzione** |
|
|
@@ -108,7 +109,7 @@ Assegnazioni correnti:
|
|
|
108
109
|
Il footer è **due righe con due nature diverse**:
|
|
109
110
|
|
|
110
111
|
```
|
|
111
|
-
⏎ detail · ^K/^P/^R spawn · ^G genera status · ^O apri status · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
|
|
112
|
+
⏎ detail · ^K/^P/^R spawn · ^G genera status · ^O apri status · ^U bumpa gitlink · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
|
|
112
113
|
t 💻 · c 🤖 · 1 📝 codium · 2 ☕ idea
|
|
113
114
|
```
|
|
114
115
|
|
|
@@ -274,6 +275,38 @@ Il recap vive in un file markdown sotto `/tmp/loom-deck-status-<uid>/`, col nome
|
|
|
274
275
|
|
|
275
276
|
A generazione finita arriva una notifica desktop (`notify-send`): la generazione dura minuti e nel frattempo si guarda altrove, e la notifica di GNOME persiste nel cassetto — cosa che un suono non farebbe. Senza `notify-send` installato non succede **niente**: nessuna riga, nessun messaggio. L'assenza di un binario è una proprietà stabile della macchina, non un evento, e segnalarla a ogni generazione sarebbe rumore ripetuto su una funzione accessoria mentre il deliverable vero è arrivato comunque.
|
|
276
277
|
|
|
278
|
+
### `^U` — il gitlink dei submodule
|
|
279
|
+
|
|
280
|
+
Su un progetto **cappello** (un repo che aggrega altri repo come submodule) ogni membro è pinnato a un commit fisso. Quando il membro avanza — un commit nel deck, nel plugin — quel puntatore resta indietro finché qualcuno non lo bumpa, e il gesto vive nella memoria di chi committa. Il deck lo mette sotto gli occhi e lo rende un tasto.
|
|
281
|
+
|
|
282
|
+
L'indicatore sta in coda alla riga della legenda, accanto agli altri due:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
gitlink 2 wrap 77 [ Inbox 📄 3/1/2 ]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| Corpo | Significato |
|
|
289
|
+
|---|---|
|
|
290
|
+
| _(assente)_ | il progetto non ha `.gitmodules`: nessun timer, nessuno spawn, nessuna colonna spesa |
|
|
291
|
+
| `missing` | prima misura non ancora tornata |
|
|
292
|
+
| `?` | plugin loom-works non installato su questa macchina, o script in errore |
|
|
293
|
+
| `ok` | ogni membro coincide col proprio gitlink |
|
|
294
|
+
| `2` | due membri col checkout diverso dal gitlink |
|
|
295
|
+
| `⚠` in coda | l'ultimo bump è fallito, **oppure** un membro richiede un gesto che il bump non fa |
|
|
296
|
+
|
|
297
|
+
Nel numero entrano solo i membri che un bump può sistemare. Un submodule non inizializzato o in conflitto di merge resta fuori e si dice col glifo: sommarlo darebbe un contatore che il tasto non porta mai a zero, e un contatore così smette di essere letto.
|
|
298
|
+
|
|
299
|
+
**`^U` bumpa**, senza conferma: il commit è locale e si annulla con un `git reset` che non tocca nessun remote. Il deck non pusha il cappello — quel gesto resta al flusso normale, perché un gitlink pushato per sbaglio è già nella copia di chiunque abbia pullato.
|
|
300
|
+
|
|
301
|
+
Il deck **non classifica da sé**: spawna `bump-gitlink.sh` del plugin (in `--dry-run` per la misura, nudo per il bump) e mostra il TSV che ne esce. Nessuna sessione Claude in mezzo — è un `execFile` con esito sincrono, come il contatore inbox e lo scan hard-wrap. Lo stesso script si lancia a mano da shell, ed è lì che vivono le due guardie che rendono il bump sicuro:
|
|
302
|
+
|
|
303
|
+
- **il verso** — `git submodule status` marca con `+` un checkout diverso dal gitlink **in entrambi i versi**, senza distinguerli. Un cappello pullato senza `git submodule update` ha il checkout più vecchio del gitlink e porta lo stesso `+`: bumparlo pinnerebbe il cappello a un commit precedente. Quel membro si salta, e la riga di stato nomina `git submodule update`.
|
|
304
|
+
- **il remote** — il cappello può pinnare solo commit già raggiungibili dal remote del membro. Un gitlink verso un commit locale produce un cappello che si clona ma non si ricostruisce, e il difetto è invisibile a chi lo introduce: nella sua copia quel commit esiste. Quel membro si salta, e la riga di stato nomina `git -C <membro> push`.
|
|
305
|
+
|
|
306
|
+
La riga di stato dopo il tasto riporta **un esito per membro**, non un totale: nel caso misto — uno pushato, uno no — un riassunto direbbe «1 bumpato» e lascerebbe fuori proprio il comando che sblocca l'altro.
|
|
307
|
+
|
|
308
|
+
La misura gira ogni 30 secondi e subito dopo un bump. Il `--dry-run` non contatta la rete: il `fetch` che precede la guardia sul remote gira solo nel bump, quindi il contatore può dire «bumpabile» di un commit che il bump poi salta perché sparito dal remote. La guardia sta sull'azione, e un contatore non è un'autorizzazione.
|
|
309
|
+
|
|
277
310
|
## Vista: filtri e ordinamenti
|
|
278
311
|
|
|
279
312
|
La lista è una **vista** sulla `tasks.md`: si filtra e si ordina senza toccare il file.
|
package/dist/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ import { useAssignOverlay } from './overlays/assign.js';
|
|
|
32
32
|
import { useProjectStatus } from './overlays/status.js';
|
|
33
33
|
import { useInboxOverlay } from './overlays/inbox.js';
|
|
34
34
|
import { useWrapOverlay } from './overlays/wrap.js';
|
|
35
|
+
import { useGitlink } from './overlays/gitlink.js';
|
|
35
36
|
import { usePurgeOverlay } from './overlays/purge.js';
|
|
36
37
|
import { useTextModals, useViewModals } from './overlays/modals.js';
|
|
37
38
|
import { useTerminalSize } from './hooks.js';
|
|
@@ -110,6 +111,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
110
111
|
setNote,
|
|
111
112
|
onApply: actions.unwrapPath,
|
|
112
113
|
});
|
|
114
|
+
// T155 — il gitlink dei submodule. Terzo sensore con lo stato in vista
|
|
115
|
+
// normale, dopo il project status e l'hard-wrap: qui però non c'è nessuna
|
|
116
|
+
// schermata da aprire — l'unica superficie è l'indicatore, e l'unica azione
|
|
117
|
+
// committa.
|
|
118
|
+
const gitlink = useGitlink({ cwd, setNote });
|
|
113
119
|
const purge = usePurgeOverlay({
|
|
114
120
|
setMode,
|
|
115
121
|
setNote,
|
|
@@ -139,6 +145,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
139
145
|
status,
|
|
140
146
|
inbox,
|
|
141
147
|
wrap,
|
|
148
|
+
gitlink,
|
|
142
149
|
purge,
|
|
143
150
|
view: viewModals,
|
|
144
151
|
text: textModals,
|
|
@@ -172,6 +179,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
172
179
|
// PRIMA della legenda perché è la loro larghezza a decidere quanto ne resta:
|
|
173
180
|
// hanno la precedenza sul budget (D5 preflight).
|
|
174
181
|
const indicators = indicatorRow({
|
|
182
|
+
// T155 — `null` chiude il gate: su un progetto senza `.gitmodules` il
|
|
183
|
+
// segmento non esiste, e la riga legenda si riprende quelle colonne.
|
|
184
|
+
gitlink: gitlink.enabled
|
|
185
|
+
? {
|
|
186
|
+
count: gitlink.count,
|
|
187
|
+
attention: gitlink.attention,
|
|
188
|
+
scanned: gitlink.scanned,
|
|
189
|
+
ok: gitlink.ok,
|
|
190
|
+
failed: gitlink.failed,
|
|
191
|
+
}
|
|
192
|
+
: null,
|
|
175
193
|
inbox: {
|
|
176
194
|
counts: model.inboxCounts,
|
|
177
195
|
stale: model.inboxStale,
|
|
@@ -251,6 +269,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
251
269
|
purgeBulk: model.purgeBulk,
|
|
252
270
|
inboxPane: model.rightPane === 'inbox',
|
|
253
271
|
hasInbox: model.selInbox !== null,
|
|
272
|
+
gitlink: gitlink.enabled,
|
|
254
273
|
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
255
274
|
}
|
|
256
275
|
const cwd = process.cwd();
|
package/dist/frame.js
CHANGED
|
@@ -106,6 +106,9 @@ export function deckLegend(state) {
|
|
|
106
106
|
// cosa e sta a due voci di distanza.
|
|
107
107
|
'^G genera status',
|
|
108
108
|
'^O apri status',
|
|
109
|
+
// T155 — la voce nomina il GESTO, come le due sopra: «gitlink» da solo
|
|
110
|
+
// direbbe l'oggetto e non che il tasto lo allinea con un commit.
|
|
111
|
+
...(state.gitlink ? ['^U bumpa gitlink'] : []),
|
|
109
112
|
// T134 — la voce nomina il pane che il tasto MONTA, non quello montato:
|
|
110
113
|
// `^B` scambia i due, e annunciare quello che si sta già guardando
|
|
111
114
|
// direbbe il contrario di ciò che il tasto fa. Stessa regola di `CANC
|
|
@@ -232,11 +235,47 @@ export function wrapIndicator(state) {
|
|
|
232
235
|
: String(state.count);
|
|
233
236
|
return sanitize(`wrap ${body}${state.ok ? '' : ` ${WARN}`}`);
|
|
234
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* T155 — l'indicatore del gitlink: `gitlink 2`.
|
|
240
|
+
*
|
|
241
|
+
* Conta i membri il cui checkout NON coincide col gitlink registrato, in
|
|
242
|
+
* entrambi i versi (`disalignedCount` in `gitlink.ts`). Fuori dal numero restano
|
|
243
|
+
* i membri che nessun bump sistema — non inizializzati, in conflitto — perché un
|
|
244
|
+
* contatore che il tasto non può portare a zero smette di essere letto: è la
|
|
245
|
+
* stessa regola per cui `misto` non entra nel contatore dell'hard-wrap.
|
|
246
|
+
*
|
|
247
|
+
* Quattro corpi distinti, come per gli altri due indicatori: `missing` finché la
|
|
248
|
+
* prima misura non è tornata, `?` quando lo script non è raggiungibile (plugin
|
|
249
|
+
* non installato su questa macchina, o uscito male), il numero quando c'è
|
|
250
|
+
* qualcosa da bumpare, `ok` quando tutti i membri coincidono.
|
|
251
|
+
*
|
|
252
|
+
* Il glifo di allerta si AGGIUNGE al corpo invece di sostituirlo, e copre i due
|
|
253
|
+
* fatti che il numero non dice: l'ultimo bump è fallito, oppure un membro
|
|
254
|
+
* richiede un gesto che il bump non fa. Entrambi restano veri accanto al
|
|
255
|
+
* conteggio, e chi lo vede preme il tasto per leggere la riga di stato, che
|
|
256
|
+
* nomina i membri.
|
|
257
|
+
*/
|
|
258
|
+
export function gitlinkIndicator(state) {
|
|
259
|
+
const body = !state.scanned
|
|
260
|
+
? STATUS_MISSING
|
|
261
|
+
: !state.ok
|
|
262
|
+
? '?'
|
|
263
|
+
: state.count > 0
|
|
264
|
+
? String(state.count)
|
|
265
|
+
: 'ok';
|
|
266
|
+
const warn = state.failed || state.attention > 0 ? ` ${WARN}` : '';
|
|
267
|
+
return sanitize(`gitlink ${body}${warn}`);
|
|
268
|
+
}
|
|
235
269
|
export function indicatorRow(state, columns) {
|
|
236
|
-
// Ordine di lettura:
|
|
237
|
-
// destro perché è l'unico dei
|
|
238
|
-
// normale — la lista del wrap è una schermata che si apre e si chiude.
|
|
270
|
+
// Ordine di lettura: gli indicatori, poi il bottone. Il bottone sta
|
|
271
|
+
// all'estremo destro perché è l'unico dei tre che MONTA qualcosa restando in
|
|
272
|
+
// vista normale — la lista del wrap è una schermata che si apre e si chiude.
|
|
273
|
+
//
|
|
274
|
+
// Il gitlink sta in TESTA al blocco, che è ancorato al bordo destro: è l'unico
|
|
275
|
+
// segmento che può mancare del tutto (il gate su `.gitmodules`), e da lì la
|
|
276
|
+
// sua assenza non sposta gli altri due.
|
|
239
277
|
const segments = [
|
|
278
|
+
...(state.gitlink ? [{ key: '^u', text: gitlinkIndicator(state.gitlink) }] : []),
|
|
240
279
|
{ key: '^w', text: wrapIndicator(state.wrap) },
|
|
241
280
|
{ key: '^b', text: inboxButton(state.inbox) },
|
|
242
281
|
];
|
package/dist/gitlink.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// T155 — il gitlink dei submodule come dato del deck.
|
|
2
|
+
//
|
|
3
|
+
// Il cappello pinna ogni membro a un commit fisso (gitlink `160000`). Quando un
|
|
4
|
+
// submodule avanza — un commit nel deck, nel plugin, in compass — il gitlink
|
|
5
|
+
// resta indietro finché qualcuno non lo bumpa a mano.
|
|
6
|
+
//
|
|
7
|
+
// LA CLASSIFICAZIONE NON STA QUI (P1 preflight). Il deck spawna
|
|
8
|
+
// `bump-gitlink.sh --dry-run` del plugin e mostra il TSV che ne esce: la stessa
|
|
9
|
+
// sequenza che poi committa è quella che ha misurato, quindi il sensore non può
|
|
10
|
+
// dire «bumpabile» in base a un criterio diverso da quello del bump. Replicarla
|
|
11
|
+
// in TypeScript darebbe due implementazioni delle due guardie — quella sul verso
|
|
12
|
+
// e quella sul remote — e una divergenza fra le due si vedrebbe solo come un
|
|
13
|
+
// cappello pinnato su un commit sbagliato, giorni dopo, nel clone di qualcun
|
|
14
|
+
// altro. Stesso confine di `inbox.ts` e `wrap-scan.ts`, e non quello di
|
|
15
|
+
// `purge.ts`, che passa da una sessione Claude: qui non c'è nessun prompt, solo
|
|
16
|
+
// un `execFile` con esito sincrono.
|
|
17
|
+
//
|
|
18
|
+
// Lettore puro + spawn: nessun React, nessuna resa. È l'unico modo di provarlo
|
|
19
|
+
// senza pseudo-terminale.
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { promisify } from 'node:util';
|
|
24
|
+
import { pluginScript } from './plugin-cache.js';
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
export const GITLINK_SCRIPT = 'scripts/utils/bump-gitlink.sh';
|
|
27
|
+
/**
|
|
28
|
+
* Ogni 30 secondi (P2 preflight).
|
|
29
|
+
*
|
|
30
|
+
* Il gate che regge gli altri poll stretti del deck qui NON funziona: il
|
|
31
|
+
* disallineamento nasce da un commit dentro il submodule, che lascia fermi sia
|
|
32
|
+
* `HEAD` del cappello (`commit-times.ts`) sia la mtime dei task file
|
|
33
|
+
* (`epic-hierarchy.ts`) — non c'è niente di economico da misurare prima. Resta
|
|
34
|
+
* il costo nudo, misurato sul cappello a tre submodule: 0,03s per giro, dentro
|
|
35
|
+
* il budget che T153 ha fissato per il loop.
|
|
36
|
+
*
|
|
37
|
+
* Il dry-run NON contatta la rete (P11): la guardia col `fetch` gira solo nel
|
|
38
|
+
* bump. Ne discende che il sensore può contare «bumpabile» un commit che il
|
|
39
|
+
* bump poi salta perché sparito dal remote — la guardia sta sull'AZIONE, e un
|
|
40
|
+
* contatore non è un'autorizzazione.
|
|
41
|
+
*/
|
|
42
|
+
export const GITLINK_SCAN_INTERVAL_MS = 30 * 1000;
|
|
43
|
+
/**
|
|
44
|
+
* Gli stati che lo script emette, più `sconosciuto` che è del solo lettore.
|
|
45
|
+
*
|
|
46
|
+
* I primi tre vengono dai prefissi di `git submodule status`; gli altri
|
|
47
|
+
* spacchettano il prefisso `+`, che da solo NON dice il verso — `man
|
|
48
|
+
* git-submodule`: «`+` if the currently checked out submodule commit does not
|
|
49
|
+
* match the SHA-1 found in the index». Un checkout più vecchio del gitlink
|
|
50
|
+
* (cappello pullato senza `git submodule update`) porta lo stesso `+` di un
|
|
51
|
+
* commit nuovo nel membro, e le due situazioni hanno rimedi opposti.
|
|
52
|
+
*
|
|
53
|
+
* `bumpato` esiste solo nell'output del bump: nel dry-run un membro idoneo resta
|
|
54
|
+
* `avanti`. Sono due fatti diversi — «lo bumperei» e «l'ho bumpato» — e un solo
|
|
55
|
+
* nome per entrambi renderebbe la riga di stato indistinguibile da un'anteprima.
|
|
56
|
+
*/
|
|
57
|
+
export const GITLINK_STATES = [
|
|
58
|
+
'allineato',
|
|
59
|
+
'non-inizializzato',
|
|
60
|
+
'conflitto',
|
|
61
|
+
'avanti',
|
|
62
|
+
'indietro',
|
|
63
|
+
'non-pushato',
|
|
64
|
+
'remote-irraggiungibile',
|
|
65
|
+
'bumpato',
|
|
66
|
+
];
|
|
67
|
+
const KNOWN = new Set(GITLINK_STATES);
|
|
68
|
+
/**
|
|
69
|
+
* Il TSV di `bump-gitlink.sh`: una riga per submodule, `path ⇥ stato ⇥ dettaglio`.
|
|
70
|
+
* Nessuna riga di intestazione — il primo campo è già un path.
|
|
71
|
+
*
|
|
72
|
+
* Uno stato che il deck non conosce diventa `sconosciuto` invece di sparire: la
|
|
73
|
+
* riga la si è comunque ricevuta, e scartarla farebbe calare il contatore ogni
|
|
74
|
+
* volta che lo script guadagna uno stato nuovo — cioè proprio quando il deck ha
|
|
75
|
+
* più bisogno di dirlo. Il dettaglio è l'ULTIMO campo e si ricompone: può
|
|
76
|
+
* contenere un comando, e un comando può contenere un tab.
|
|
77
|
+
*/
|
|
78
|
+
export function parseGitlinkTsv(stdout) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const line of stdout.split('\n')) {
|
|
81
|
+
if (!line.trim())
|
|
82
|
+
continue;
|
|
83
|
+
const c = line.split('\t');
|
|
84
|
+
if (c.length < 2 || !c[0])
|
|
85
|
+
continue;
|
|
86
|
+
out.push({
|
|
87
|
+
path: c[0],
|
|
88
|
+
state: KNOWN.has(c[1]) ? c[1] : 'sconosciuto',
|
|
89
|
+
detail: c.slice(2).join('\t'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Gli stati che il contatore somma: i membri il cui checkout non coincide col
|
|
96
|
+
* gitlink, in entrambi i versi.
|
|
97
|
+
*
|
|
98
|
+
* `non-inizializzato` e `conflitto` restano FUORI per decisione: nessun bump li
|
|
99
|
+
* sistema — il primo vuole un `git submodule update --init`, il secondo la
|
|
100
|
+
* risoluzione di un merge — e sommarli darebbe un numero che il tasto non può
|
|
101
|
+
* portare a zero. Un contatore che non arriva mai a zero smette di essere letto
|
|
102
|
+
* (stessa regola di `misto` nell'indicatore hard-wrap). Si dicono comunque, col
|
|
103
|
+
* glifo di allerta accanto al numero.
|
|
104
|
+
*/
|
|
105
|
+
const DISALIGNED = new Set([
|
|
106
|
+
'avanti',
|
|
107
|
+
'indietro',
|
|
108
|
+
'non-pushato',
|
|
109
|
+
'remote-irraggiungibile',
|
|
110
|
+
]);
|
|
111
|
+
/** Quanti membri hanno il checkout diverso dal gitlink: è il numero a schermo. */
|
|
112
|
+
export function disalignedCount(members) {
|
|
113
|
+
return members.filter((m) => DISALIGNED.has(m.state)).length;
|
|
114
|
+
}
|
|
115
|
+
/** Quanti sono bumpabili adesso: il solo `avanti` passa entrambe le guardie. */
|
|
116
|
+
export function bumpableCount(members) {
|
|
117
|
+
return members.filter((m) => m.state === 'avanti').length;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Qualcosa richiede attenzione ma nessun bump lo sistema: un membro non
|
|
121
|
+
* inizializzato, un conflitto di merge, uno stato che questo deck non conosce.
|
|
122
|
+
* Vale il glifo di allerta, mai una cifra nel contatore.
|
|
123
|
+
*/
|
|
124
|
+
export function attentionCount(members) {
|
|
125
|
+
return members.filter((m) => m.state === 'non-inizializzato' || m.state === 'conflitto' || m.state === 'sconosciuto').length;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Gli stati che il bump ha SALTATO: il membro è disallineato ma una delle due
|
|
129
|
+
* guardie l'ha fermato, e il rimedio non è un bump ma il gesto che il dettaglio
|
|
130
|
+
* nomina.
|
|
131
|
+
*/
|
|
132
|
+
const SKIPPED = new Set(['indietro', 'non-pushato', 'remote-irraggiungibile']);
|
|
133
|
+
/**
|
|
134
|
+
* La riga di stato dopo un bump: un esito per membro, non un totale.
|
|
135
|
+
*
|
|
136
|
+
* Il caso che rende necessario nominarli è il MISTO — un membro pushato e uno
|
|
137
|
+
* no. Lì un messaggio riassuntivo direbbe «1 bumpato» e lascerebbe fuori proprio
|
|
138
|
+
* l'informazione che serve ad agire: quale membro è rimasto indietro e con quale
|
|
139
|
+
* comando lo si sblocca. Il gesto arriva già scritto dallo script, che è dove la
|
|
140
|
+
* guardia lo ha deciso: comporlo qui darebbe due frasi da tenere allineate.
|
|
141
|
+
*
|
|
142
|
+
* I bumpati vengono per primi — è l'esito che il tasto prometteva — e i saltati
|
|
143
|
+
* dopo, ognuno col proprio glifo di allerta. I membri allineati non compaiono:
|
|
144
|
+
* la riga di stato dice cosa è successo, non cosa esiste.
|
|
145
|
+
*/
|
|
146
|
+
export function gitlinkNote(members) {
|
|
147
|
+
const done = members
|
|
148
|
+
.filter((m) => m.state === 'bumpato')
|
|
149
|
+
.map((m) => `✔ ${m.path} ${m.detail}`.trim());
|
|
150
|
+
const skipped = members
|
|
151
|
+
.filter((m) => SKIPPED.has(m.state))
|
|
152
|
+
.map((m) => `⚠ ${m.path}: ${m.detail}`);
|
|
153
|
+
return [...done, ...skipped].join(' · ');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Il progetto ha submodule da misurare.
|
|
157
|
+
*
|
|
158
|
+
* È il gate di ATTIVAZIONE dell'intero sensore (DLV2): senza `.gitmodules` non
|
|
159
|
+
* parte nessun timer, non si spawna niente e l'indicatore non compare affatto —
|
|
160
|
+
* non «0», che occuperebbe colonne per dire che la funzione non si applica.
|
|
161
|
+
*
|
|
162
|
+
* Il file, non `git submodule status`: rispondere alla domanda «ci sono
|
|
163
|
+
* submodule?» con lo stesso spawn che si vuole evitare sarebbe la misura che il
|
|
164
|
+
* gate esiste per non pagare.
|
|
165
|
+
*/
|
|
166
|
+
export function hasGitmodules(projectRoot) {
|
|
167
|
+
return existsSync(join(projectRoot, '.gitmodules'));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Invoca lo script e ne parsa l'output.
|
|
171
|
+
*
|
|
172
|
+
* I modi di non avere il dato — plugin non installato su questa macchina,
|
|
173
|
+
* script in errore, git muto — collassano in un solo `ok: false`, come per la
|
|
174
|
+
* coda inbox: a schermo non cambiano la prima mossa di chi li vede. Nessuno dei
|
|
175
|
+
* due è un throw, un indicatore informativo non può rompere il deck.
|
|
176
|
+
*
|
|
177
|
+
* Lo stdout si legge ANCHE quando l'exit è non-zero: lo script classifica tutti
|
|
178
|
+
* i membri prima di agire, quindi su un errore parziale (un `git` che fallisce
|
|
179
|
+
* su un membro) le righe già emesse restano vere e dicono più di una schermata
|
|
180
|
+
* vuota.
|
|
181
|
+
*/
|
|
182
|
+
async function run(projectRoot, args) {
|
|
183
|
+
const script = pluginScript(GITLINK_SCRIPT);
|
|
184
|
+
if (!script)
|
|
185
|
+
return { members: [], ok: false };
|
|
186
|
+
try {
|
|
187
|
+
const { stdout } = await execFileAsync(script, args, {
|
|
188
|
+
cwd: projectRoot,
|
|
189
|
+
maxBuffer: 1024 * 1024,
|
|
190
|
+
});
|
|
191
|
+
return { members: parseGitlinkTsv(stdout), ok: true };
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
const stdout = e.stdout ?? '';
|
|
195
|
+
return { members: parseGitlinkTsv(stdout), ok: false };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** La misura, senza toccare niente: nessun fetch, nessun commit. */
|
|
199
|
+
export function scanGitlink(projectRoot) {
|
|
200
|
+
return run(projectRoot, ['--dry-run']);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Il bump vero: fetch, due guardie, commit unico coi soli membri idonei. Il
|
|
204
|
+
* cappello NON viene pushato — quel gesto resta al flusso normale, perché un
|
|
205
|
+
* gitlink committato per sbaglio si annulla con un `git reset` che non tocca
|
|
206
|
+
* nessun remote, mentre uno pushato è già nella copia di chiunque abbia pullato.
|
|
207
|
+
*/
|
|
208
|
+
export function bumpGitlink(projectRoot) {
|
|
209
|
+
return run(projectRoot, []);
|
|
210
|
+
}
|
package/dist/input.js
CHANGED
|
@@ -322,6 +322,19 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
322
322
|
else if (input === 'e') {
|
|
323
323
|
overlays.wrap.scan();
|
|
324
324
|
}
|
|
325
|
+
else if (input === 'u') {
|
|
326
|
+
// T155/P5 preflight — `^U` (update del gitlink) bumpa i submodule
|
|
327
|
+
// idonei. Non è la coppia genera/apri di `^G`/`^O` e `^W`/`^E`: qui non
|
|
328
|
+
// c'è nessuna schermata da aprire, la misura gira da sé ogni 30 secondi
|
|
329
|
+
// e l'unico gesto è l'azione. Nessuna conferma, perché il commit è
|
|
330
|
+
// locale e reversibile (P6).
|
|
331
|
+
//
|
|
332
|
+
// `^B` era la mnemonica migliore per «bump» ed è di T134 (scambia i due
|
|
333
|
+
// pane dello slot destro). `^U` svuota un campo dentro i modi capturing,
|
|
334
|
+
// ma quelli consumano l'input per intero e non vedono mai questo ramo —
|
|
335
|
+
// stesso doppio significato per contesto già in esercizio su `^F`.
|
|
336
|
+
overlays.gitlink.bump();
|
|
337
|
+
}
|
|
325
338
|
else if (input === 'b') {
|
|
326
339
|
// T134/D8 preflight — `^B` (box/bacheca) scambia i due pane dello slot
|
|
327
340
|
// destro. Non è un modale né una schermata: il pane resta uno dei due
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// T155 — il sensore del gitlink: stato, gate di attivazione, cadenza.
|
|
2
|
+
//
|
|
3
|
+
// Non è un overlay come `sheet` o `search` — non apre nessuna schermata e non
|
|
4
|
+
// entra in `MODE_KEYS`. Sta qui per la stessa ragione di `useProjectStatus` e
|
|
5
|
+
// `useWrapOverlay`: tiene uno stato che si legge in vista normale e possiede
|
|
6
|
+
// l'azione che quello stato abilita, e `hooks.ts` è già a 487 righe (P3
|
|
7
|
+
// preflight, D1/P12 — nessuno split dentro questa task).
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
|
+
import { attentionCount, bumpableCount, bumpGitlink, disalignedCount, gitlinkNote, GITLINK_SCAN_INTERVAL_MS, hasGitmodules, scanGitlink, } from '../gitlink.js';
|
|
10
|
+
export function useGitlink(deps) {
|
|
11
|
+
const { cwd, setNote } = deps;
|
|
12
|
+
/**
|
|
13
|
+
* IL GATE. Senza `.gitmodules` il progetto non ha submodule, quindi non c'è
|
|
14
|
+
* niente da misurare: nessun timer parte, nessuno spawn viene fatto, e
|
|
15
|
+
* l'indicatore non compare affatto — non «0», che occuperebbe colonne della
|
|
16
|
+
* riga legenda per dire che la funzione non si applica.
|
|
17
|
+
*
|
|
18
|
+
* Misurato UNA VOLTA per project root e non a ogni giro: un repo non guadagna
|
|
19
|
+
* submodule mentre il deck è aperto, e se lo facesse basta riavviarlo. È lo
|
|
20
|
+
* stesso limite accettato dalla cache del plugin (`plugin-cache.ts`), scritto
|
|
21
|
+
* qui invece che scoperto dopo.
|
|
22
|
+
*/
|
|
23
|
+
const enabled = useMemo(() => hasGitmodules(cwd), [cwd]);
|
|
24
|
+
const [members, setMembers] = useState([]);
|
|
25
|
+
const [ok, setOk] = useState(true);
|
|
26
|
+
/** Almeno un tentativo è stato fatto: distingue «non ho ancora misurato» da
|
|
27
|
+
* «ho misurato e sono tutti allineati», che a schermo sono due cose diverse. */
|
|
28
|
+
const [scanned, setScanned] = useState(false);
|
|
29
|
+
const [epoch, setEpoch] = useState(0);
|
|
30
|
+
const [bumping, setBumping] = useState(false);
|
|
31
|
+
/** L'ultimo bump si è fermato prima di committare. STICKY: un giro di misura
|
|
32
|
+
* riuscito non lo azzera — dice com'è andata l'ultima AZIONE, che è un fatto
|
|
33
|
+
* diverso da com'è andata l'ultima misura, e i due restano entrambi veri.
|
|
34
|
+
* Stessa regola del `failed` del project status. */
|
|
35
|
+
const [failed, setFailed] = useState(false);
|
|
36
|
+
// Il flag di corsa sta in un ref e non in stato, come in `useInboxScan`: due
|
|
37
|
+
// richieste ravvicinate girano nello stesso tick di React, e un valore di
|
|
38
|
+
// stato arriverebbe al render dopo — cioè troppo tardi per impedire il
|
|
39
|
+
// secondo spawn.
|
|
40
|
+
const busy = useRef(false);
|
|
41
|
+
// `alive` vive fuori dall'effect perché la richiesta forzata sopravvive al
|
|
42
|
+
// giro che l'ha ospitata: la scatena un gesto, non l'effect.
|
|
43
|
+
const alive = useRef(true);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
alive.current = true;
|
|
46
|
+
return () => {
|
|
47
|
+
alive.current = false;
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
const run = useCallback(() => {
|
|
51
|
+
if (busy.current)
|
|
52
|
+
return;
|
|
53
|
+
busy.current = true;
|
|
54
|
+
scanGitlink(cwd).then((res) => {
|
|
55
|
+
busy.current = false;
|
|
56
|
+
if (!alive.current)
|
|
57
|
+
return;
|
|
58
|
+
// Un tentativo fallito NON butta via l'esito dell'ultimo riuscito: le
|
|
59
|
+
// righe precedenti restano vere, e il guasto si dice col glifo di allerta
|
|
60
|
+
// accanto al numero. Stessa regola del project status e della coda inbox.
|
|
61
|
+
setOk(res.ok);
|
|
62
|
+
setScanned(true);
|
|
63
|
+
if (res.ok || res.members.length > 0)
|
|
64
|
+
setMembers(res.members);
|
|
65
|
+
});
|
|
66
|
+
}, [cwd]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!enabled)
|
|
69
|
+
return;
|
|
70
|
+
run();
|
|
71
|
+
const id = setInterval(run, GITLINK_SCAN_INTERVAL_MS);
|
|
72
|
+
return () => clearInterval(id);
|
|
73
|
+
}, [enabled, run, epoch]);
|
|
74
|
+
/**
|
|
75
|
+
* Rimisura adesso e riparte col timer da zero.
|
|
76
|
+
*
|
|
77
|
+
* Il reset non è cosmetico: senza, l'intervallo continuerebbe a scorrere
|
|
78
|
+
* dall'ultimo tick automatico e potrebbe rimisurare pochi istanti dopo un
|
|
79
|
+
* dato appena misurato — costo pieno, risultato identico. Stessa forma di
|
|
80
|
+
* `useWrapScan` e `useInboxScan`.
|
|
81
|
+
*/
|
|
82
|
+
const refresh = useCallback(() => {
|
|
83
|
+
setEpoch((e) => e + 1);
|
|
84
|
+
}, []);
|
|
85
|
+
const count = disalignedCount(members);
|
|
86
|
+
const bumpable = bumpableCount(members);
|
|
87
|
+
/**
|
|
88
|
+
* Il bump vero — l'unica azione del sensore.
|
|
89
|
+
*
|
|
90
|
+
* NESSUNA CONFERMA (P6 preflight): il commit è locale e si annulla con un
|
|
91
|
+
* `git reset` che non tocca nessun remote, e lo script non pusha il cappello.
|
|
92
|
+
* È la stessa forma di `^G`, che genera senza chiedere.
|
|
93
|
+
*
|
|
94
|
+
* I quattro rifiuti dicono SEMPRE perché (AC: il tasto su zero disallineati
|
|
95
|
+
* non spawna niente e lo dice). Un tasto che non facesse niente in silenzio
|
|
96
|
+
* sarebbe indistinguibile da un binding mancante.
|
|
97
|
+
*/
|
|
98
|
+
function bump() {
|
|
99
|
+
if (!enabled) {
|
|
100
|
+
setNote('^U → questo progetto non ha submodule: nessun gitlink da bumpare');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (busy.current || bumping) {
|
|
104
|
+
setNote('^U → bump del gitlink già in corso');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!scanned) {
|
|
108
|
+
setNote('^U → gitlink non ancora misurato');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (bumpable === 0) {
|
|
112
|
+
const why = gitlinkNote(members);
|
|
113
|
+
setNote(count === 0
|
|
114
|
+
? '^U → gitlink già allineato: niente da bumpare'
|
|
115
|
+
: `^U → nessun membro bumpabile · ${why}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
busy.current = true;
|
|
119
|
+
setBumping(true);
|
|
120
|
+
setNote(`⏳ bump del gitlink su ${bumpable} membro${bumpable > 1 ? 'i' : ''}…`);
|
|
121
|
+
bumpGitlink(cwd).then((res) => {
|
|
122
|
+
busy.current = false;
|
|
123
|
+
if (!alive.current)
|
|
124
|
+
return;
|
|
125
|
+
setBumping(false);
|
|
126
|
+
setOk(res.ok);
|
|
127
|
+
setScanned(true);
|
|
128
|
+
setFailed(!res.ok);
|
|
129
|
+
if (res.members.length > 0)
|
|
130
|
+
setMembers(res.members);
|
|
131
|
+
const note = gitlinkNote(res.members);
|
|
132
|
+
setNote(note || (res.ok ? '✔ gitlink: nessun membro bumpato' : '⚠ bump del gitlink fallito'));
|
|
133
|
+
// Rimisura: l'output del bump porta già gli stati nuovi, ma il giro
|
|
134
|
+
// successivo li conferma da `git submodule status` invece che dalla
|
|
135
|
+
// memoria di ciò che si è appena fatto — ed è ciò che fa scendere il
|
|
136
|
+
// contatore senza riavviare il deck anche quando il commit ha toccato più
|
|
137
|
+
// di quanto lo script credeva.
|
|
138
|
+
refresh();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
enabled,
|
|
143
|
+
members,
|
|
144
|
+
ok,
|
|
145
|
+
scanned,
|
|
146
|
+
bumping,
|
|
147
|
+
failed,
|
|
148
|
+
count,
|
|
149
|
+
bumpable,
|
|
150
|
+
attention: attentionCount(members),
|
|
151
|
+
bump,
|
|
152
|
+
refresh,
|
|
153
|
+
};
|
|
154
|
+
}
|
package/package.json
CHANGED