@marcomigozzi/trailhead 0.11.0 → 0.12.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.it.md +6 -3
- package/README.md +6 -3
- package/bin/lib/codex-projection.js +2 -1
- package/bin/lib/codex-projection.test.js +4 -1
- package/bin/trailhead.js +27 -15
- package/bin/trailhead.test.js +417 -2
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/commands/adopt.md +5 -1
- package/plugins/trailhead/commands/audit.md +10 -0
- package/plugins/trailhead/commands/auto.md +5 -1
- package/plugins/trailhead/commands/bug.md +5 -1
- package/plugins/trailhead/commands/config.md +5 -1
- package/plugins/trailhead/commands/dashboard.md +5 -1
- package/plugins/trailhead/commands/grill.md +5 -1
- package/plugins/trailhead/commands/idea.md +5 -1
- package/plugins/trailhead/commands/inbox.md +5 -1
- package/plugins/trailhead/commands/map.md +5 -1
- package/plugins/trailhead/commands/new.md +5 -1
- package/plugins/trailhead/commands/note.md +5 -1
- package/plugins/trailhead/commands/pause.md +5 -1
- package/plugins/trailhead/commands/prune.md +5 -1
- package/plugins/trailhead/commands/quick.md +6 -2
- package/plugins/trailhead/commands/resume.md +5 -1
- package/plugins/trailhead/commands/seed.md +5 -1
- package/plugins/trailhead/commands/split.md +5 -1
- package/plugins/trailhead/commands/ticket.md +5 -1
- package/plugins/trailhead/commands/todo.md +5 -1
- package/plugins/trailhead/commands/update.md +5 -1
- package/plugins/trailhead/commands/whiteboard.md +5 -1
- package/plugins/trailhead/commands/work.md +5 -1
- package/plugins/trailhead/hooks/hooks.json +9 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +802 -0
- package/plugins/trailhead/hooks/lib/mockup-link.js +50 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +399 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +489 -0
- package/plugins/trailhead/hooks/lib/model-defaults.json +16 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +69 -14
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +95 -0
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +145 -0
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.js +38 -0
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +64 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +151 -6
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +56 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +7 -4
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +5 -5
- package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +5 -1
- package/plugins/trailhead/skills/trailhead/SKILL.md +3 -3
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +5 -4
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +132 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +1 -1
- package/plugins/trailhead/templates/trailhead-audit-walk.js +685 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// mockup-link.js: shared detection for the mockup-link guards.
|
|
2
|
+
// Consumers: trailhead-mockup-link-guard.js (Claude Code, PreToolUse on
|
|
3
|
+
// AskUserQuestion) and trailhead-mockup-link-stop.js (Codex, Stop on the
|
|
4
|
+
// final reply, where an ask is a plain-text numbered list, not a tool call).
|
|
5
|
+
// Pure functions, no dependencies.
|
|
6
|
+
|
|
7
|
+
const MOCKUP_WORD = /mock-?ups?|prototip|prototype|wireframe/i;
|
|
8
|
+
// The ask itself, second person (IT + EN): "approvi", "do you approve",
|
|
9
|
+
// "accetti", "quale variante". Kept narrow on purpose: "il mockup approvato"
|
|
10
|
+
// (a past approval) and the "want mockups first?" offer are not asks.
|
|
11
|
+
const APPROVAL_WORD = /\bapprovi\b|\bapprove\b|\baccetti\b|\bquale variante\b|\bwhich variant\b/i;
|
|
12
|
+
|
|
13
|
+
const URL_RE = /https?:\/\/\S+/i;
|
|
14
|
+
const FILE_RE = /[\w~.\-/]+\.(?:html?|png|jpe?g|svg|webp|gif|pdf)\b/i;
|
|
15
|
+
|
|
16
|
+
function hasLink(prose) {
|
|
17
|
+
return typeof prose === 'string' && (URL_RE.test(prose) || FILE_RE.test(prose));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// One question's question, header and option labels. Option descriptions are
|
|
21
|
+
// left out: an offer like "Prima un mockup — attendo approvazione" describes
|
|
22
|
+
// a future approval, it does not ask for one.
|
|
23
|
+
function questionText(q) {
|
|
24
|
+
const labels = Array.isArray(q.options) ? q.options.map((o) => o && o.label) : [];
|
|
25
|
+
return [q.question, q.header, ...labels].filter((s) => typeof s === 'string').join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// AskUserQuestion input: does any single question ask to approve a mockup?
|
|
29
|
+
function isMockupApprovalAsk(input) {
|
|
30
|
+
const qs = (input && Array.isArray(input.questions)) ? input.questions : [];
|
|
31
|
+
return qs.some((q) => {
|
|
32
|
+
const t = q ? questionText(q) : '';
|
|
33
|
+
return MOCKUP_WORD.test(t) && APPROVAL_WORD.test(t);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A plain-text reply (Codex): it talks about a mockup and one of its
|
|
38
|
+
// question lines (a line holding "?") asks for the approval.
|
|
39
|
+
function isMockupApprovalMessage(text) {
|
|
40
|
+
if (typeof text !== 'string' || !MOCKUP_WORD.test(text)) return false;
|
|
41
|
+
return text.split('\n').some((l) => l.includes('?') && APPROVAL_WORD.test(l));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const BLOCK_REASON =
|
|
45
|
+
'trailhead mockup-link: this asks to approve/react to a mockup, but the message carries ' +
|
|
46
|
+
'no link to it. Write each mockup\'s clickable link (URL or file path, one line per ' +
|
|
47
|
+
'screen/variant) in the plain prose immediately before the question, not the project ' +
|
|
48
|
+
'name, then ask again. See "Surface the mockup" in _shared/techniques/prototype.md.';
|
|
49
|
+
|
|
50
|
+
module.exports = { hasLink, isMockupApprovalAsk, isMockupApprovalMessage, BLOCK_REASON };
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// model-defaults-review.js: quando un aggiornamento sposta i default dei
|
|
3
|
+
// tier (plugins/trailhead/hooks/lib/model-defaults.json), offre la revisione
|
|
4
|
+
// dei models.* / models.codex.* pinnati che sono rimasti ai vecchi default.
|
|
5
|
+
// check() e' di sola lettura: non scrive mai l'ack, quindi ripropone la
|
|
6
|
+
// notice finche' qualcuno non chiama ack(cwd) (dopo che l'utente ha risposto
|
|
7
|
+
// a una delle tre scelte). Non lancia mai eccezioni dagli entry point
|
|
8
|
+
// pubblici: ogni funzione degrada a null/[]/false in caso di dato mancante
|
|
9
|
+
// o malformato. Entry point condiviso anche dagli host senza hook bus (CLI
|
|
10
|
+
// "check"/"ack" in fondo al file).
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const DATA_FILE = path.join(__dirname, 'model-defaults.json');
|
|
17
|
+
const HOSTS = ['claude', 'codex'];
|
|
18
|
+
const TIERS = ['strong', 'standard', 'fast'];
|
|
19
|
+
|
|
20
|
+
function isPlainObject(v) {
|
|
21
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadData() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return { schema: 1, history: [] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function currentEntry(data) {
|
|
33
|
+
const history = (data && data.history) || [];
|
|
34
|
+
return history[history.length - 1] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Confronto semver minimale: true se a < b. Ignora pre-release, tollera "v".
|
|
38
|
+
function semverLt(a, b) {
|
|
39
|
+
const clean = (v) => String(v || '').replace(/^v/, '').split('-')[0];
|
|
40
|
+
const pa = clean(a).split('.').map(Number);
|
|
41
|
+
const pb = clean(b).split('.').map(Number);
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
const x = pa[i] || 0;
|
|
44
|
+
const y = pb[i] || 0;
|
|
45
|
+
if (x !== y) return x < y;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Trova il tier T a cui un id e' storicamente appartenuto per un host dato:
|
|
51
|
+
// T's default in una entry PIU' VECCHIA della corrente, oppure elencato nei
|
|
52
|
+
// supersedes[host][T] di una qualsiasi entry (corrente inclusa). Se piu'
|
|
53
|
+
// entry corrispondono, vince quella con indice piu' alto (piu' recente).
|
|
54
|
+
function findTier(data, host, id, current) {
|
|
55
|
+
const history = data.history || [];
|
|
56
|
+
let best = null; // { tier, index }
|
|
57
|
+
history.forEach((entry, index) => {
|
|
58
|
+
if (entry !== current && entry[host]) {
|
|
59
|
+
for (const tier of TIERS) {
|
|
60
|
+
if (entry[host][tier] === id) {
|
|
61
|
+
if (!best || index > best.index) best = { tier, index };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (entry.supersedes && entry.supersedes[host]) {
|
|
66
|
+
for (const tier of TIERS) {
|
|
67
|
+
const list = entry.supersedes[host][tier] || [];
|
|
68
|
+
if (list.includes(id)) {
|
|
69
|
+
if (!best || index > best.index) best = { tier, index };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return best ? best.tier : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isSkippable(id) {
|
|
78
|
+
return !id || typeof id !== 'string' || id.trim() === '' || id === 'inherit session';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Valuta un singolo pin (id) per un host e ritorna { from, to } se stale,
|
|
82
|
+
// null altrimenti (id sconosciuto o gia' allineato al default corrente).
|
|
83
|
+
function evaluatePin(data, host, id, current) {
|
|
84
|
+
if (isSkippable(id)) return null;
|
|
85
|
+
const tier = findTier(data, host, id, current);
|
|
86
|
+
if (!tier) return null;
|
|
87
|
+
const to = current[host][tier];
|
|
88
|
+
if (to === id) return null;
|
|
89
|
+
return { from: id, to };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stalePins(config, data) {
|
|
93
|
+
const out = [];
|
|
94
|
+
const current = currentEntry(data);
|
|
95
|
+
if (!current) return out;
|
|
96
|
+
const models = (config && config.models) || {};
|
|
97
|
+
|
|
98
|
+
for (const key of Object.keys(models)) {
|
|
99
|
+
if (key === 'codex') continue;
|
|
100
|
+
const result = evaluatePin(data, 'claude', models[key], current);
|
|
101
|
+
if (result) out.push({ key: `models.${key}`, from: result.from, to: result.to });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Un models.codex malformato (non un oggetto piano) non produce pin: come
|
|
105
|
+
// se fosse assente, invece di iterare i suoi indici/caratteri.
|
|
106
|
+
const codex = isPlainObject(models.codex) ? models.codex : {};
|
|
107
|
+
for (const key of Object.keys(codex)) {
|
|
108
|
+
const raw = codex[key];
|
|
109
|
+
const id = raw && typeof raw === 'object' ? raw.model : raw;
|
|
110
|
+
const result = evaluatePin(data, 'codex', id, current);
|
|
111
|
+
if (result) out.push({ key: `models.codex.${key}`, from: result.from, to: result.to });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Ritorna una NUOVA config con i pin stale aggiornati; non muta l'input.
|
|
118
|
+
function applyUpdate(config, stale) {
|
|
119
|
+
const next = { ...config, models: { ...((config && config.models) || {}) } };
|
|
120
|
+
if (next.models.codex) next.models.codex = { ...next.models.codex };
|
|
121
|
+
|
|
122
|
+
for (const s of stale) {
|
|
123
|
+
if (s.key.startsWith('models.codex.')) {
|
|
124
|
+
const k = s.key.slice('models.codex.'.length);
|
|
125
|
+
const cur = next.models.codex[k];
|
|
126
|
+
next.models.codex[k] = cur && typeof cur === 'object' ? { ...cur, model: s.to } : s.to;
|
|
127
|
+
} else {
|
|
128
|
+
const k = s.key.slice('models.'.length);
|
|
129
|
+
next.models[k] = s.to;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return next;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Unisce piu' layer di config (dal piu' generico al piu' specifico, es.
|
|
136
|
+
// [global, project]) in una config effettiva pura: nessun input viene
|
|
137
|
+
// mutato. La precedenza e' per PRESENZA della chiave, mai per verita' del
|
|
138
|
+
// valore: un layer piu' specifico che imposta una chiave (anche a null, ''
|
|
139
|
+
// o "inherit session") vince sempre, e il valore del layer sotto non
|
|
140
|
+
// riaffiora mai. models.codex e' unito chiave per chiave solo quando sia
|
|
141
|
+
// l'accumulo corrente sia il valore in arrivo sono oggetti piani; altrimenti
|
|
142
|
+
// il valore del layer piu' specifico rimpiazza l'intero models.codex (un
|
|
143
|
+
// models.codex malformato nel progetto produce quindi zero pin codex, non
|
|
144
|
+
// un fallback al globale). Ogni layer il cui `models` non e' un oggetto
|
|
145
|
+
// piano (null, array, stringa) non contribuisce, come se fosse assente.
|
|
146
|
+
// Ritorna { config, sources }, dove sources mappa ogni chiave vincente
|
|
147
|
+
// (es. "models.plan", "models.codex.plan") a { layer, file } di chi l'ha
|
|
148
|
+
// decisa, cosi' la notice puo' stampare il path reale.
|
|
149
|
+
function effectiveModels(layers) {
|
|
150
|
+
let models = {};
|
|
151
|
+
let codex; // undefined finche' nessun layer tocca models.codex
|
|
152
|
+
const sources = {};
|
|
153
|
+
|
|
154
|
+
for (const layer of layers || []) {
|
|
155
|
+
const cfgModels = layer && layer.config && isPlainObject(layer.config.models) ? layer.config.models : null;
|
|
156
|
+
if (!cfgModels) continue;
|
|
157
|
+
|
|
158
|
+
for (const key of Object.keys(cfgModels)) {
|
|
159
|
+
if (key === 'codex') continue;
|
|
160
|
+
models = { ...models, [key]: cfgModels[key] };
|
|
161
|
+
sources[`models.${key}`] = { layer: layer.layer, file: layer.file };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(cfgModels, 'codex')) {
|
|
165
|
+
const incoming = cfgModels.codex;
|
|
166
|
+
if (isPlainObject(incoming) && isPlainObject(codex)) {
|
|
167
|
+
for (const k of Object.keys(incoming)) {
|
|
168
|
+
codex = { ...codex, [k]: incoming[k] };
|
|
169
|
+
sources[`models.codex.${k}`] = { layer: layer.layer, file: layer.file };
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
for (const k of Object.keys(sources)) {
|
|
173
|
+
if (k.startsWith('models.codex.')) delete sources[k];
|
|
174
|
+
}
|
|
175
|
+
codex = isPlainObject(incoming) ? { ...incoming } : incoming;
|
|
176
|
+
if (isPlainObject(codex)) {
|
|
177
|
+
for (const k of Object.keys(codex)) {
|
|
178
|
+
sources[`models.codex.${k}`] = { layer: layer.layer, file: layer.file };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const outModels = { ...models };
|
|
186
|
+
if (codex !== undefined) outModels.codex = codex;
|
|
187
|
+
return { config: { models: outModels }, sources };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function reviewOffer({ config, data, ack, sources }) {
|
|
191
|
+
const current = currentEntry(data);
|
|
192
|
+
if (!current) return null;
|
|
193
|
+
const stale = stalePins(config, data).map((s) => {
|
|
194
|
+
const source = sources && sources[s.key];
|
|
195
|
+
return source ? { ...s, source } : s;
|
|
196
|
+
});
|
|
197
|
+
if (!stale.length) return null;
|
|
198
|
+
if (ack && !semverLt(ack, current.since)) return null;
|
|
199
|
+
return { since: current.since, stale, notice: reviewNotice(stale, current.since) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sourceSuffix(source) {
|
|
203
|
+
if (!source) return ' (new default)';
|
|
204
|
+
const layerName = source.layer === 'global' ? 'the global config' : 'the project config';
|
|
205
|
+
return ` (new default; from ${layerName}, ${source.file})`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function reviewNotice(stale, since) {
|
|
209
|
+
const lines = stale.map((s) => `- ${s.key}: ${s.from} -> ${s.to}${sourceSuffix(s.source)}`);
|
|
210
|
+
return [
|
|
211
|
+
`trailhead's default models changed in ${since}. The following pinned model(s) are older than the new default:`,
|
|
212
|
+
...lines,
|
|
213
|
+
'',
|
|
214
|
+
'Offer the user, once, without blocking their current request, three choices:',
|
|
215
|
+
'1) update those keys to the new defaults (edit each key in the config file it comes from, with exactly the values above; keep any codex effort unchanged),',
|
|
216
|
+
'2) keep them as they are (nothing to do; trailhead will not ask again for these defaults once acknowledged),',
|
|
217
|
+
'3) open /trailhead:config to pick per key.',
|
|
218
|
+
'This offer reappears every time it is checked until acknowledged: run the ack instruction below only after the user has picked one of the three choices above.',
|
|
219
|
+
].join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Paragrafo con l'istruzione pronta all'uso per acquisire l'ack, appesa alla
|
|
223
|
+
// notice da check(). Nomina il path assoluto di questo lib e la root (la
|
|
224
|
+
// root del progetto trovata, oppure il cwd nel caso solo-globale), cosi'
|
|
225
|
+
// l'agente puo' eseguirla cosi' com'e'.
|
|
226
|
+
function ackInstruction(root) {
|
|
227
|
+
return `Once the user has answered, run: node "${__filename}" ack "${root}"`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- I/O ---------------------------------------------------------------
|
|
231
|
+
const ACK_FILE = path.join('.trailhead', 'model-defaults-ack');
|
|
232
|
+
const CONFIG_FILE = path.join('.trailhead', 'config.json');
|
|
233
|
+
|
|
234
|
+
// Dir base della config globale: stessa risoluzione di trailhead-check-update.js.
|
|
235
|
+
function globalConfigDir() {
|
|
236
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function globalConfigFile() {
|
|
240
|
+
return path.join(globalConfigDir(), 'trailhead', 'config.json');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// L'ack globale vive accanto al config.json globale, senza nidificare
|
|
244
|
+
// ".trailhead" (che e' una convenzione solo di progetto).
|
|
245
|
+
function globalAckFile() {
|
|
246
|
+
return path.join(globalConfigDir(), 'trailhead', 'model-defaults-ack');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Mancante o malformato -> {}: non deve mai bloccare la review di progetto.
|
|
250
|
+
function loadGlobalConfig() {
|
|
251
|
+
try {
|
|
252
|
+
const raw = JSON.parse(fs.readFileSync(globalConfigFile(), 'utf8'));
|
|
253
|
+
return isPlainObject(raw) ? raw : {};
|
|
254
|
+
} catch {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function findProjectRoot(cwd) {
|
|
260
|
+
let dir = path.resolve(cwd || '.');
|
|
261
|
+
for (;;) {
|
|
262
|
+
if (fs.existsSync(path.join(dir, CONFIG_FILE))) return dir;
|
|
263
|
+
const parent = path.dirname(dir);
|
|
264
|
+
if (parent === dir) return null;
|
|
265
|
+
dir = parent;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function readAckFile(file) {
|
|
270
|
+
try {
|
|
271
|
+
return fs.readFileSync(file, 'utf8').trim() || null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function writeAckFile(file, since) {
|
|
278
|
+
try {
|
|
279
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
280
|
+
fs.writeFileSync(`${file}.tmp`, since);
|
|
281
|
+
fs.renameSync(`${file}.tmp`, file);
|
|
282
|
+
} catch {
|
|
283
|
+
/* mai far fallire il chiamante per un ack non scrivibile */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readAck(root) {
|
|
288
|
+
return readAckFile(path.join(root, ACK_FILE));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function writeAck(root, since) {
|
|
292
|
+
writeAckFile(path.join(root, ACK_FILE), since);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Di sola lettura: non scrive mai l'ack. Ripropone la stessa notice finche'
|
|
296
|
+
// non arriva un ack esplicito via ack(cwd). Valuta i pin EFFETTIVI: la
|
|
297
|
+
// config globale unita sotto quella di progetto (progetto vince chiave per
|
|
298
|
+
// chiave); senza un progetto sopra cwd, valuta la sola config globale.
|
|
299
|
+
function check(cwd) {
|
|
300
|
+
try {
|
|
301
|
+
const resolvedCwd = path.resolve(cwd || '.');
|
|
302
|
+
const root = findProjectRoot(cwd);
|
|
303
|
+
const data = loadData();
|
|
304
|
+
const globalFile = globalConfigFile();
|
|
305
|
+
const globalLayer = { layer: 'global', file: globalFile, config: loadGlobalConfig() };
|
|
306
|
+
|
|
307
|
+
if (root) {
|
|
308
|
+
let projectConfig;
|
|
309
|
+
try {
|
|
310
|
+
projectConfig = JSON.parse(fs.readFileSync(path.join(root, CONFIG_FILE), 'utf8'));
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const projectLayer = {
|
|
315
|
+
layer: 'project',
|
|
316
|
+
file: path.join(root, CONFIG_FILE),
|
|
317
|
+
config: isPlainObject(projectConfig) ? projectConfig : {},
|
|
318
|
+
};
|
|
319
|
+
const { config, sources } = effectiveModels([globalLayer, projectLayer]);
|
|
320
|
+
const ack = readAck(root);
|
|
321
|
+
const offer = reviewOffer({ config, data, ack, sources });
|
|
322
|
+
if (!offer) return null;
|
|
323
|
+
return `${offer.notice}\n${ackInstruction(root)}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Nessun progetto sopra cwd: valuta la sola config globale, se esiste.
|
|
327
|
+
if (!fs.existsSync(globalFile)) return null;
|
|
328
|
+
const { config, sources } = effectiveModels([globalLayer]);
|
|
329
|
+
const ack = readAckFile(globalAckFile());
|
|
330
|
+
const offer = reviewOffer({ config, data, ack, sources });
|
|
331
|
+
if (!offer) return null;
|
|
332
|
+
return `${offer.notice}\n${ackInstruction(resolvedCwd)}`;
|
|
333
|
+
} catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Scrive l'ack, con il "since" corrente dei default. Va chiamata SOLO dopo
|
|
339
|
+
// che l'utente ha risposto a una delle tre scelte della notice. Progetto
|
|
340
|
+
// trovato sopra cwd -> ack di progetto; altrimenti, se esiste una config
|
|
341
|
+
// globale -> ack globale. True se un target e' stato trovato e la scrittura
|
|
342
|
+
// tentata (un fallimento silenzioso non e' riportato, comportamento
|
|
343
|
+
// invariato); false se nessun target esiste. Non lancia mai eccezioni.
|
|
344
|
+
function ack(cwd) {
|
|
345
|
+
try {
|
|
346
|
+
const root = findProjectRoot(cwd);
|
|
347
|
+
const data = loadData();
|
|
348
|
+
const current = currentEntry(data);
|
|
349
|
+
if (!current) return false;
|
|
350
|
+
if (root) {
|
|
351
|
+
writeAck(root, current.since);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (fs.existsSync(globalConfigFile())) {
|
|
355
|
+
writeAckFile(globalAckFile(), current.since);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
} catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
loadData,
|
|
366
|
+
currentEntry,
|
|
367
|
+
stalePins,
|
|
368
|
+
applyUpdate,
|
|
369
|
+
semverLt,
|
|
370
|
+
effectiveModels,
|
|
371
|
+
reviewOffer,
|
|
372
|
+
reviewNotice,
|
|
373
|
+
findProjectRoot,
|
|
374
|
+
globalConfigDir,
|
|
375
|
+
globalConfigFile,
|
|
376
|
+
globalAckFile,
|
|
377
|
+
readAckFile,
|
|
378
|
+
writeAckFile,
|
|
379
|
+
readAck,
|
|
380
|
+
writeAck,
|
|
381
|
+
check,
|
|
382
|
+
ack,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// CLI: entry point condiviso per gli host senza hook bus.
|
|
386
|
+
if (require.main === module) {
|
|
387
|
+
const [cmd, cwdArg] = process.argv.slice(2);
|
|
388
|
+
try {
|
|
389
|
+
if (cmd === 'check') {
|
|
390
|
+
const notice = check(cwdArg || process.cwd());
|
|
391
|
+
if (notice) process.stdout.write(notice + '\n');
|
|
392
|
+
} else if (cmd === 'ack') {
|
|
393
|
+
ack(cwdArg || process.cwd());
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
/* mai fallire da CLI */
|
|
397
|
+
}
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|