@abarcenas/ai-workflow-template 1.29.0 → 1.32.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/.agents/instructions/learned-knowledge.instructions.md +23 -0
- package/.github/workflows/npm-publish.yml +3 -0
- package/.github/workflows/pr-test.yml +23 -0
- package/.husky/post-merge +1 -0
- package/.husky/pre-commit +2 -0
- package/README.md +48 -1
- package/bin/setup.js +23 -0
- package/package.json +7 -2
- package/scripts/setup/constants.js +226 -0
- package/scripts/setup/discover.js +236 -0
- package/scripts/setup/discover.test.js +388 -0
- package/scripts/setup/hooks.js +261 -0
- package/scripts/setup/hooks.test.js +527 -0
- package/scripts/setup/husky-init.js +102 -0
- package/scripts/setup/index.js +354 -0
- package/scripts/setup/index.test.js +639 -0
- package/scripts/setup/prepare.js +127 -0
- package/scripts/setup/prepare.test.js +718 -0
- package/scripts/setup/sync-phase.js +172 -0
- package/scripts/setup/ui.js +503 -0
- package/scripts/setup/utils.js +371 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// index.js — Setup command orchestrator
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Implements the full setup pipeline:
|
|
6
|
+
// 1. Parse CLI arguments
|
|
7
|
+
// 2. Handle --help / --version (early exit with code 0)
|
|
8
|
+
// 3. Display header banner
|
|
9
|
+
// 4. Phase 1 — Discovery (detect consumer project state)
|
|
10
|
+
// 5. Phase 2 — Hooks (install / merge git hooks, unless --skip-hooks)
|
|
11
|
+
// 6. Phase 3 — Prepare (configure consumer package.json prepare script)
|
|
12
|
+
// 7. Phase 4 — Husky Init (generate .husky/_/ shims, unless --skip-hooks)
|
|
13
|
+
// 8. Phase 5 — Sync (spawn sync.js + normalize-memory.js, unless --skip-sync)
|
|
14
|
+
// 9. Render summary table via ui.summary()
|
|
15
|
+
// 10. Determine exit code and return it
|
|
16
|
+
//
|
|
17
|
+
// Each phase is wrapped in try/catch. Only discovery failure is fatal.
|
|
18
|
+
// All other phase errors are logged and the pipeline continues (REQ-04).
|
|
19
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from 'fs'
|
|
22
|
+
import { join } from 'path'
|
|
23
|
+
|
|
24
|
+
import { EXIT_CODES } from './constants.js'
|
|
25
|
+
import { parseCliArgs, resolvePackageRoot } from './utils.js'
|
|
26
|
+
import {
|
|
27
|
+
ciModeBanner,
|
|
28
|
+
header,
|
|
29
|
+
help,
|
|
30
|
+
section,
|
|
31
|
+
stepError,
|
|
32
|
+
stepSuccess,
|
|
33
|
+
stepWarn,
|
|
34
|
+
summary,
|
|
35
|
+
} from './ui.js'
|
|
36
|
+
import { discover } from './discover.js'
|
|
37
|
+
import { installHooks } from './hooks.js'
|
|
38
|
+
import { handlePrepare } from './prepare.js'
|
|
39
|
+
import { initHusky } from './husky-init.js'
|
|
40
|
+
import { runSyncPhase } from './sync-phase.js'
|
|
41
|
+
|
|
42
|
+
// ── Package version (lazy, cached) ───────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** @type {string|null} */
|
|
45
|
+
let _cachedVersion = null
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads the package version from `package.json`.
|
|
49
|
+
*
|
|
50
|
+
* The result is cached after the first successful read so repeated calls
|
|
51
|
+
* (only the `--version` flag currently) don't re-read the filesystem.
|
|
52
|
+
*
|
|
53
|
+
* @returns {string} Semver string (e.g. `'1.29.0'`). Falls back to `'0.0.0'`.
|
|
54
|
+
*/
|
|
55
|
+
function getPackageVersion() {
|
|
56
|
+
if (_cachedVersion === null) {
|
|
57
|
+
try {
|
|
58
|
+
const pkgRoot = resolvePackageRoot()
|
|
59
|
+
const pkgPath = join(pkgRoot, 'package.json')
|
|
60
|
+
_cachedVersion =
|
|
61
|
+
JSON.parse(readFileSync(pkgPath, 'utf-8')).version || '0.0.0'
|
|
62
|
+
} catch {
|
|
63
|
+
_cachedVersion = '0.0.0'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return _cachedVersion
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Result mapping helper ────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Maps a phase module action string to a summary status.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} action - Action value returned by a phase module
|
|
75
|
+
* (e.g. `'added'`, `'skipped'`, `'failed'`, `'dry-run'`).
|
|
76
|
+
* @returns {'success' | 'warn' | 'error'}
|
|
77
|
+
*/
|
|
78
|
+
function actionStatus(action) {
|
|
79
|
+
switch (action) {
|
|
80
|
+
// These actions represent a successful, completed operation
|
|
81
|
+
case 'added':
|
|
82
|
+
case 'created':
|
|
83
|
+
case 'initialized':
|
|
84
|
+
case 'completed':
|
|
85
|
+
case 'dry-run':
|
|
86
|
+
case 'overwritten':
|
|
87
|
+
case 'merged':
|
|
88
|
+
return 'success'
|
|
89
|
+
// These actions indicate the phase was intentionally skipped or
|
|
90
|
+
// only partially completed — not an error but worth noting
|
|
91
|
+
case 'skipped':
|
|
92
|
+
case 'partial':
|
|
93
|
+
return 'warn'
|
|
94
|
+
// Explicit failure
|
|
95
|
+
case 'failed':
|
|
96
|
+
return 'error'
|
|
97
|
+
// Unknown action — treat as a warning
|
|
98
|
+
default:
|
|
99
|
+
return 'warn'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Main export ──────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Runs the full setup pipeline and returns an exit code.
|
|
107
|
+
*
|
|
108
|
+
* The pipeline orchestrates five phases in sequence. Each phase is wrapped
|
|
109
|
+
* in a try/catch so that a non-fatal failure in one phase does not prevent
|
|
110
|
+
* subsequent phases from running (graceful degradation per REQ-04).
|
|
111
|
+
*
|
|
112
|
+
* The returned exit code follows the `EXIT_CODES` constants:
|
|
113
|
+
* - `SUCCESS` (0) — All phases completed without errors or warnings
|
|
114
|
+
* - `WARNINGS` (1) — One or more phases had warnings or were skipped
|
|
115
|
+
* - `FATAL` (2) — Discovery phase failed (fatal, no recovery possible)
|
|
116
|
+
*
|
|
117
|
+
* @param {string[]} argv - CLI arguments (typically `process.argv.slice(2)`).
|
|
118
|
+
* @returns {Promise<number>} Numeric exit code for `process.exit(code)`.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```js
|
|
122
|
+
* const code = await main(process.argv.slice(2))
|
|
123
|
+
* process.exit(code)
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export async function main(argv) {
|
|
127
|
+
// ── 1. Parse CLI arguments ──────────────────────────────────────────────
|
|
128
|
+
const flags = parseCliArgs(argv)
|
|
129
|
+
|
|
130
|
+
// ── 2. Handle early-exit flags ──────────────────────────────────────────
|
|
131
|
+
if (flags.help) {
|
|
132
|
+
help()
|
|
133
|
+
return EXIT_CODES.SUCCESS
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (flags.version) {
|
|
137
|
+
console.log(getPackageVersion())
|
|
138
|
+
return EXIT_CODES.SUCCESS
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── 3. Display header banner ────────────────────────────────────────────
|
|
142
|
+
if (!flags.quiet) {
|
|
143
|
+
header()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Accumulated phase results for the final summary table.
|
|
147
|
+
// Each entry: `{ phase: string, status: 'success'|'warn'|'error', message: string }`
|
|
148
|
+
/** @type {Array<{ phase: string, status: string, message: string }>} */
|
|
149
|
+
const phases = []
|
|
150
|
+
|
|
151
|
+
// Mutable context object — enriched by discovery and each subsequent phase.
|
|
152
|
+
/** @type {import('./discover.js').Context | object} */
|
|
153
|
+
let context
|
|
154
|
+
|
|
155
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// Phase 1 — Discovery
|
|
157
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
158
|
+
section('Phase 1: Discovery')
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
context = await discover(flags)
|
|
162
|
+
const msg = context.hasGit
|
|
163
|
+
? `Git repository found at ${context.consumerRoot}`
|
|
164
|
+
: `Not a git repository (${context.consumerRoot}); hooks will be skipped`
|
|
165
|
+
stepSuccess(msg)
|
|
166
|
+
phases.push({ phase: 'discover', status: 'success', message: msg })
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const msg = `Discovery failed: ${err.message}`
|
|
169
|
+
stepError(msg)
|
|
170
|
+
phases.push({ phase: 'discover', status: 'error', message: msg })
|
|
171
|
+
// Discovery failure is fatal — no further detection is possible
|
|
172
|
+
if (!flags.quiet) summary(phases)
|
|
173
|
+
return EXIT_CODES.FATAL
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── CI mode banner ──────────────────────────────────────────────────────
|
|
177
|
+
if (context.isCI && !flags.quiet) {
|
|
178
|
+
ciModeBanner()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
182
|
+
// Phase 2 — Hooks (unless --skip-hooks)
|
|
183
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
184
|
+
if (!flags.skipHooks) {
|
|
185
|
+
section('Phase 2: Hooks')
|
|
186
|
+
|
|
187
|
+
if (!context.hasGit) {
|
|
188
|
+
stepWarn(
|
|
189
|
+
'Not a git repository \u2014 skipping hooks. ' +
|
|
190
|
+
'Run "git init" first, then re-run setup.',
|
|
191
|
+
)
|
|
192
|
+
phases.push({
|
|
193
|
+
phase: 'hooks',
|
|
194
|
+
status: 'warn',
|
|
195
|
+
message: 'Skipped \u2014 not a git repository',
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
try {
|
|
199
|
+
const hookResults = await installHooks(context)
|
|
200
|
+
context.hookResults = hookResults
|
|
201
|
+
|
|
202
|
+
// Aggregate per-hook results into a single summary entry
|
|
203
|
+
const actions = hookResults.map((r) => r.action)
|
|
204
|
+
const hasErrors = actions.some((a) => a === 'skipped')
|
|
205
|
+
const onlyDryRun = actions.every((a) => a === 'dry-run')
|
|
206
|
+
const allCreated = actions.every((a) => a === 'created')
|
|
207
|
+
const allOverwritten = actions.every((a) => a === 'overwritten')
|
|
208
|
+
|
|
209
|
+
if (onlyDryRun) {
|
|
210
|
+
phases.push({
|
|
211
|
+
phase: 'hooks',
|
|
212
|
+
status: 'success',
|
|
213
|
+
message: 'Dry-run complete (no files modified)',
|
|
214
|
+
})
|
|
215
|
+
} else if (hasErrors) {
|
|
216
|
+
phases.push({
|
|
217
|
+
phase: 'hooks',
|
|
218
|
+
status: 'error',
|
|
219
|
+
message: 'One or more hooks failed',
|
|
220
|
+
})
|
|
221
|
+
} else if (allCreated || allOverwritten) {
|
|
222
|
+
phases.push({
|
|
223
|
+
phase: 'hooks',
|
|
224
|
+
status: 'success',
|
|
225
|
+
message: 'All hooks installed',
|
|
226
|
+
})
|
|
227
|
+
} else {
|
|
228
|
+
// Mixed results (created, merged, overwritten, etc.)
|
|
229
|
+
const summaryParts = [...new Set(actions)].map((a) => {
|
|
230
|
+
const count = actions.filter((x) => x === a).length
|
|
231
|
+
return `${count} ${a}`
|
|
232
|
+
})
|
|
233
|
+
phases.push({
|
|
234
|
+
phase: 'hooks',
|
|
235
|
+
status: 'success',
|
|
236
|
+
message: summaryParts.join(', '),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
stepError(`Hook installation failed: ${err.message}`)
|
|
241
|
+
phases.push({
|
|
242
|
+
phase: 'hooks',
|
|
243
|
+
status: 'error',
|
|
244
|
+
message: err.message,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
251
|
+
// Phase 3 — Prepare (unless --skip-prepare)
|
|
252
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
253
|
+
if (!flags.skipPrepare) {
|
|
254
|
+
section('Phase 3: Prepare')
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const prepareResult = await handlePrepare(context)
|
|
258
|
+
context.prepareResult = prepareResult
|
|
259
|
+
phases.push({
|
|
260
|
+
phase: 'prepare',
|
|
261
|
+
status: actionStatus(prepareResult.action),
|
|
262
|
+
message: prepareResult.message,
|
|
263
|
+
})
|
|
264
|
+
} catch (err) {
|
|
265
|
+
stepError(`Prepare script handling failed: ${err.message}`)
|
|
266
|
+
phases.push({
|
|
267
|
+
phase: 'prepare',
|
|
268
|
+
status: 'error',
|
|
269
|
+
message: err.message,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
275
|
+
// Phase 4 — Husky Init (unless --skip-hooks)
|
|
276
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
277
|
+
if (!flags.skipHooks) {
|
|
278
|
+
section('Phase 4: Husky Init')
|
|
279
|
+
|
|
280
|
+
if (!context.hasGit) {
|
|
281
|
+
stepWarn(
|
|
282
|
+
'Not a git repository \u2014 skipping husky init. ' +
|
|
283
|
+
'Run "git init" first, then re-run setup.',
|
|
284
|
+
)
|
|
285
|
+
phases.push({
|
|
286
|
+
phase: 'husky-init',
|
|
287
|
+
status: 'warn',
|
|
288
|
+
message: 'Skipped \u2014 not a git repository',
|
|
289
|
+
})
|
|
290
|
+
} else {
|
|
291
|
+
try {
|
|
292
|
+
const huskyResult = await initHusky(context)
|
|
293
|
+
context.huskyResult = huskyResult
|
|
294
|
+
phases.push({
|
|
295
|
+
phase: 'husky-init',
|
|
296
|
+
status: actionStatus(huskyResult.action),
|
|
297
|
+
message: huskyResult.message,
|
|
298
|
+
})
|
|
299
|
+
} catch (err) {
|
|
300
|
+
stepError(`Husky initialization failed: ${err.message}`)
|
|
301
|
+
phases.push({
|
|
302
|
+
phase: 'husky-init',
|
|
303
|
+
status: 'error',
|
|
304
|
+
message: err.message,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
311
|
+
// Phase 5 — Sync (unless --skip-sync)
|
|
312
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
313
|
+
if (!flags.skipSync) {
|
|
314
|
+
section('Phase 5: Sync')
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const syncResult = await runSyncPhase(context)
|
|
318
|
+
context.syncResult = syncResult
|
|
319
|
+
phases.push({
|
|
320
|
+
phase: 'sync',
|
|
321
|
+
status: actionStatus(syncResult.action),
|
|
322
|
+
message: syncResult.message,
|
|
323
|
+
})
|
|
324
|
+
} catch (err) {
|
|
325
|
+
stepError(`Sync phase failed: ${err.message}`)
|
|
326
|
+
phases.push({
|
|
327
|
+
phase: 'sync',
|
|
328
|
+
status: 'error',
|
|
329
|
+
message: err.message,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
335
|
+
// Summary
|
|
336
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
337
|
+
if (!flags.quiet) {
|
|
338
|
+
summary(phases)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
342
|
+
// Exit code
|
|
343
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
344
|
+
const hasErrors = phases.some(
|
|
345
|
+
(p) => p.status === 'error',
|
|
346
|
+
)
|
|
347
|
+
const hasWarnings = phases.some(
|
|
348
|
+
(p) => p.status === 'warn',
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if (hasErrors) return EXIT_CODES.FATAL
|
|
352
|
+
if (hasWarnings) return EXIT_CODES.WARNINGS
|
|
353
|
+
return EXIT_CODES.SUCCESS
|
|
354
|
+
}
|