@marcomigozzi/trailhead 0.14.0 → 0.15.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.
Files changed (92) hide show
  1. package/README.it.md +118 -30
  2. package/README.md +107 -29
  3. package/bin/base-preflight-docs.test.js +65 -0
  4. package/bin/execute-split-docs.test.js +146 -0
  5. package/bin/isolation-answer-docs.test.js +41 -0
  6. package/bin/isolation-copy-docs.test.js +28 -0
  7. package/bin/lib/codex-projection.js +1 -1
  8. package/bin/lib/codex-projection.test.js +41 -0
  9. package/bin/report-status-docs.test.js +195 -0
  10. package/bin/resolve-helpers-docs.test.js +36 -0
  11. package/bin/scope-confirm-skip-docs.test.js +70 -0
  12. package/bin/trailhead.js +3 -3
  13. package/bin/trailhead.test.js +341 -1
  14. package/bin/uat-setup-docs.test.js +65 -0
  15. package/package.json +1 -1
  16. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  17. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  18. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  19. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  20. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  21. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  22. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  23. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  24. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  25. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  26. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  27. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  28. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  29. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  30. package/plugins/trailhead/hooks/lib/dashboard.test.js +409 -0
  31. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  32. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  33. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  34. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  35. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  36. package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
  37. package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
  38. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  39. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  40. package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
  41. package/plugins/trailhead/hooks/mod/README.md +38 -6
  42. package/plugins/trailhead/hooks/mod/map-pane-view.ts +168 -15
  43. package/plugins/trailhead/hooks/mod/map-pane.tsx +275 -106
  44. package/plugins/trailhead/hooks/mod/register.tsx +15 -2
  45. package/plugins/trailhead/hooks/mod/work-state-view.ts +17 -0
  46. package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
  47. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
  48. package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
  49. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
  50. package/plugins/trailhead/skills/_shared/choices.md +4 -2
  51. package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
  52. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  53. package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
  54. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  55. package/plugins/trailhead/skills/_shared/substrate-commands.md +22 -11
  56. package/plugins/trailhead/skills/_shared/substrate.md +6 -4
  57. package/plugins/trailhead/skills/_shared/teamwork.md +16 -7
  58. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
  59. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  60. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  61. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  62. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  63. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  64. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  65. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  66. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  67. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +2 -2
  68. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  69. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
  70. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  71. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  72. package/plugins/trailhead/skills/trailhead-view/SKILL.md +8 -6
  73. package/plugins/trailhead/skills/trailhead-work/SKILL.md +14 -12
  74. package/plugins/trailhead/skills/trailhead-work/references/auto.md +4 -4
  75. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  76. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +26 -10
  77. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  78. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  79. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  80. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  81. package/plugins/trailhead/templates/trailhead-dashboard.js +340 -0
  82. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  83. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  84. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  85. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  86. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  87. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  88. package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
  89. package/plugins/trailhead/tests/map-pane-ui.test.ts +566 -19
  90. package/plugins/trailhead/tests/map-pane-view.test.ts +304 -16
  91. package/plugins/trailhead/tests/work-state-view.test.ts +25 -1
  92. package/plugins/trailhead/types/index.d.ts +37 -3
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // trailhead isolation copy (self-contained, ships in templates/ so it reaches every
4
+ // install channel: bin/trailhead.js copies templates/ wholesale for Claude/npm
5
+ // and Codex). Copies the gitignored files an app needs to run (env files, local
6
+ // config) from the main checkout into a fresh worktree or clone. #242.
7
+ //
8
+ // CLI:
9
+ // node trailhead-isolation-copy.js --from <main-checkout> --to <workspace>
10
+ // The list lives in `isolation.copy` of .trailhead/config.json: an array of
11
+ // repo-relative paths (file or directory, no globs), nested ({"isolation":
12
+ // {"copy":[...]}}) or flat ({"isolation.copy":[...]}); nested wins when a file
13
+ // carries both (a warning says so). The project file wins over the global one
14
+ // (${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/config.json) as a whole value: the
15
+ // global list applies only when the project file has no isolation.copy at all.
16
+ // Rules: a destination that already exists is never overwritten (reported
17
+ // `present`); absolute paths, `..`, any `.git` segment, and paths whose parent
18
+ // directory resolves outside its root (checked on both sides) are `rejected`;
19
+ // a final symlink is copied as a symlink, never followed; nested `.git` entries
20
+ // are skipped inside copied directories. An invalid config (bad JSON, non-array,
21
+ // non-string or empty entry) copies nothing and offers no hint.
22
+ // When the key is unset, tracked `*.example` / `*.sample` files whose stripped
23
+ // sibling exists in the main checkout and is gitignored are listed in
24
+ // `suggestions` (never copied).
25
+ // Prints one JSON line:
26
+ // {"status","copied":[],"present":[],"missing":[],"rejected":[],"suggestions":[],"warnings":[],"message"?}
27
+ // status: copied | nothing | unset | invalid | skipped
28
+ // Exit 0 on every normal outcome and on any unexpected I/O error (it must never
29
+ // block the caller: status skipped with the message); exit 2 only on a usage
30
+ // error (missing --from/--to, unknown flag).
31
+
32
+ const fs = require('fs');
33
+ const os = require('os');
34
+ const path = require('path');
35
+ const { execFileSync } = require('child_process');
36
+
37
+ const CONFIG_REL = path.join('.trailhead', 'config.json');
38
+ const SUFFIXES = ['.example', '.sample'];
39
+
40
+ function parseArgs(argv) {
41
+ const out = {};
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const a = argv[i];
44
+ if (a === '--from' || a === '--to') {
45
+ const v = argv[++i];
46
+ if (v === undefined || v.startsWith('--')) return { error: `usage: ${a} needs a value` };
47
+ out[a.slice(2)] = v;
48
+ } else {
49
+ return { error: `usage: unknown argument ${a}` };
50
+ }
51
+ }
52
+ if (!out.from) return { error: 'usage: missing --from <main-checkout>' };
53
+ if (!out.to) return { error: 'usage: missing --to <workspace>' };
54
+ return out;
55
+ }
56
+
57
+ // Returns null when the repo-relative entry is acceptable, else the reason.
58
+ function checkRelPath(rel) {
59
+ if (typeof rel !== 'string' || rel === '') return 'empty path';
60
+ if (path.isAbsolute(rel) || path.win32.isAbsolute(rel)) return 'absolute path';
61
+ const segments = rel.split(/[\\/]+/).filter(Boolean);
62
+ if (segments.length === 0 || segments.every((s) => s === '.')) return 'empty path';
63
+ if (segments.includes('..')) return 'parent segment';
64
+ if (segments.includes('.git')) return '.git segment';
65
+ return null;
66
+ }
67
+
68
+ // Reads the copy list out of one config file.
69
+ // { state: 'unset' } | { state: 'set', list, warnings } | { state: 'invalid', message }
70
+ function readCopyList(file, fs_) {
71
+ let text;
72
+ try {
73
+ text = fs_.readFileSync(file, 'utf8');
74
+ } catch (err) {
75
+ if (err && err.code === 'ENOENT') return { state: 'unset' };
76
+ return { state: 'invalid', message: `cannot read ${file}: ${err && err.message ? err.message : err}` };
77
+ }
78
+ let cfg;
79
+ try {
80
+ cfg = JSON.parse(text);
81
+ } catch (err) {
82
+ return { state: 'invalid', message: `${file} is not valid JSON` };
83
+ }
84
+ if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg)) {
85
+ return { state: 'invalid', message: `${file} is not a JSON object` };
86
+ }
87
+ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
88
+ const nested = cfg.isolation !== null && typeof cfg.isolation === 'object' && !Array.isArray(cfg.isolation) && has(cfg.isolation, 'copy');
89
+ const flat = has(cfg, 'isolation.copy');
90
+ if (!nested && !flat) return { state: 'unset' };
91
+ const warnings = [];
92
+ if (nested && flat) warnings.push(`${file} sets isolation.copy in both nested and flat form; the nested form wins`);
93
+ const value = nested ? cfg.isolation.copy : cfg['isolation.copy'];
94
+ if (!Array.isArray(value)) return { state: 'invalid', message: `isolation.copy in ${file} must be an array of paths` };
95
+ if (value.some((e) => typeof e !== 'string' || e.trim() === '')) {
96
+ return { state: 'invalid', message: `isolation.copy in ${file} must hold only non-empty strings` };
97
+ }
98
+ return { state: 'set', list: value, warnings };
99
+ }
100
+
101
+ function globalConfigPath(env) {
102
+ const base = env.CLAUDE_CONFIG_DIR || path.join(env.HOME || os.homedir(), '.claude');
103
+ return path.join(base, 'trailhead', 'config.json');
104
+ }
105
+
106
+ // realpath of the deepest existing ancestor of p (p itself included).
107
+ function deepestReal(p, fs_) {
108
+ let cur = p;
109
+ for (;;) {
110
+ try {
111
+ return fs_.realpathSync(cur);
112
+ } catch (err) {
113
+ const up = path.dirname(cur);
114
+ if (up === cur) return cur;
115
+ cur = up;
116
+ }
117
+ }
118
+ }
119
+
120
+ // True when the parent directory of root/rel resolves inside realRoot.
121
+ function parentInside(root, realRoot, rel, fs_) {
122
+ const real = deepestReal(path.dirname(path.join(root, rel)), fs_);
123
+ return real === realRoot || real.startsWith(realRoot + path.sep);
124
+ }
125
+
126
+ const exists = (p, fs_) => {
127
+ try { fs_.lstatSync(p); return true; } catch (err) { return false; }
128
+ };
129
+
130
+ // Tracked *.example / *.sample files whose stripped sibling is present and ignored.
131
+ function suggest(from, fs_) {
132
+ let tracked;
133
+ try {
134
+ tracked = execFileSync('git', ['-C', from, 'ls-files', '-z'], { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 })
135
+ .toString().split('\0').filter(Boolean);
136
+ } catch (err) {
137
+ return [];
138
+ }
139
+ const out = [];
140
+ for (const file of tracked) {
141
+ const suffix = SUFFIXES.find((s) => file.endsWith(s) && file.length > s.length);
142
+ if (!suffix) continue;
143
+ const sibling = file.slice(0, -suffix.length);
144
+ if (out.includes(sibling) || checkRelPath(sibling) !== null || !exists(path.join(from, sibling), fs_)) continue;
145
+ try {
146
+ execFileSync('git', ['-C', from, 'check-ignore', '-q', '--', sibling], { stdio: 'ignore' });
147
+ out.push(sibling);
148
+ } catch (err) {
149
+ // not ignored (exit 1) or git error: not a suggestion
150
+ }
151
+ }
152
+ return out;
153
+ }
154
+
155
+ function copyEntries(list, from, to, fs_) {
156
+ const result = { copied: [], present: [], missing: [], rejected: [], warnings: [] };
157
+ const realFrom = fs_.realpathSync(from);
158
+ const realTo = fs_.realpathSync(to);
159
+ for (const rel of list) {
160
+ if (checkRelPath(rel) !== null || !parentInside(from, realFrom, rel, fs_)) {
161
+ result.rejected.push(rel);
162
+ continue;
163
+ }
164
+ const src = path.join(from, rel);
165
+ const dst = path.join(to, rel);
166
+ if (!exists(src, fs_)) { result.missing.push(rel); continue; }
167
+ if (!parentInside(to, realTo, rel, fs_)) { result.rejected.push(rel); continue; }
168
+ if (exists(dst, fs_)) { result.present.push(rel); continue; }
169
+ try {
170
+ fs_.mkdirSync(path.dirname(dst), { recursive: true });
171
+ // Re-check after creating parents: nothing may have been redirected meanwhile.
172
+ if (!parentInside(to, realTo, rel, fs_)) { result.rejected.push(rel); continue; }
173
+ fs_.cpSync(src, dst, {
174
+ recursive: true,
175
+ force: false,
176
+ errorOnExist: true,
177
+ verbatimSymlinks: true,
178
+ filter: (s) => path.basename(s) !== '.git',
179
+ });
180
+ result.copied.push(rel);
181
+ } catch (err) {
182
+ result.warnings.push(`could not copy ${rel}: ${err && err.message ? err.message : err}`);
183
+ // This run created the destination (it did not exist before): drop any
184
+ // partial copy so the next run retries instead of reporting it present.
185
+ try {
186
+ fs_.rmSync(dst, { recursive: true, force: true });
187
+ } catch (cleanupErr) {
188
+ result.warnings.push(`could not clean up partial ${rel}: ${cleanupErr && cleanupErr.message ? cleanupErr.message : cleanupErr}`);
189
+ }
190
+ }
191
+ }
192
+ return result;
193
+ }
194
+
195
+ function writeResult(out, fields) {
196
+ const result = { status: 'skipped', copied: [], present: [], missing: [], rejected: [], suggestions: [], warnings: [], ...fields };
197
+ out.write(JSON.stringify(result) + '\n');
198
+ }
199
+
200
+ function main(argv, { cwd, stdout, env, fs: fsMod } = {}) {
201
+ const cwd_ = cwd || process.cwd();
202
+ const out = stdout || process.stdout;
203
+ const env_ = env || process.env;
204
+ const fs_ = fsMod || fs;
205
+
206
+ const parsed = parseArgs(argv || []);
207
+ if (parsed.error) {
208
+ process.stderr.write(parsed.error + '\n');
209
+ return 2;
210
+ }
211
+ const from = path.resolve(cwd_, parsed.from);
212
+ const to = path.resolve(cwd_, parsed.to);
213
+ try {
214
+ let cfg = readCopyList(path.join(from, CONFIG_REL), fs_);
215
+ if (cfg.state === 'unset') cfg = readCopyList(globalConfigPath(env_), fs_);
216
+ if (cfg.state === 'invalid') {
217
+ writeResult(out, { status: 'invalid', message: cfg.message });
218
+ return 0;
219
+ }
220
+ if (cfg.state === 'unset') {
221
+ writeResult(out, { status: 'unset', suggestions: suggest(from, fs_) });
222
+ return 0;
223
+ }
224
+ const res = copyEntries(cfg.list, from, to, fs_);
225
+ res.warnings = [...cfg.warnings, ...res.warnings];
226
+ writeResult(out, { ...res, status: res.copied.length > 0 ? 'copied' : 'nothing' });
227
+ return 0;
228
+ } catch (err) {
229
+ writeResult(out, { status: 'skipped', message: 'error: ' + (err && err.message ? err.message : String(err)) });
230
+ return 0;
231
+ }
232
+ }
233
+
234
+ module.exports = { checkRelPath, readCopyList, parseArgs, main };
235
+
236
+ if (require.main === module) {
237
+ const code = main(process.argv.slice(2), { cwd: process.cwd(), stdout: process.stdout, env: process.env });
238
+ process.exit(code);
239
+ }