@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.
@@ -20,3 +20,26 @@
20
20
  - unit-tester: Must be told to look in `scripts/` for source and test files, not `src/`
21
21
  - coder: Must be told the project uses ES module syntax (import/export, not require/module.exports)
22
22
  - implementer: Plans should target `scripts/` for production code and tests
23
+
24
+ ## Session: 2026-07-24
25
+
26
+ **Pipeline:** researcher → architect → implementer → coder (6 parallel batches) → unit-tester → tracker
27
+ **Coverage:** 88/88 tests passing across 4 test files (discover 21, hooks 16, prepare 27, index 24)
28
+ **TDD Iterations:** 0 (feature pipeline, not TDD)
29
+
30
+ **New knowledge:**
31
+ - **npm v12 blocks postinstall scripts from dependencies** — this is a critical ecosystem shift that affects ALL npm library distribution. Postinstall auto-sync is no longer viable; explicit setup commands are required
32
+ - **npx-based setup command pattern** (following Playwright, Prisma, Cypress convention) is the future-proof distribution mechanism for CLI tools that need to configure consumer projects
33
+ - **Husky v9 programmatic API** is dead simple: `import husky from 'husky'; husky(consumerRoot)` — no need to shell out or manually manage shim files
34
+ - **`child_process.spawn` for scripts with `process.exit()`** — importing sync.js (which calls `process.exit()`) directly into the setup process would kill the parent. Spawn as a child process to isolate exit behavior
35
+ - **Marker comment idempotency** — a first-line comment (`# Managed by @abarcenas/ai-workflow-template setup`) on hook files enables content-based detection for safe re-runs. This pattern should be used for any managed file that needs idempotent installation
36
+ - **6-batch parallel execution worked well**: Batch A (3 foundation: constants, utils, ui) → Batch B (5 core: discover, hooks, prepare, husky-init, sync-phase) → Batch C (2 orchestrator: index.js, bin/setup.js) → Batches D+E (3 config: pre-commit marker, post-merge marker, package.json) → Batch F (4 tests). Clear interface definitions were essential for parallelism
37
+ - **Duplicate-marker trap** — constants.js `TEMPLATE_HOOKS.content` must stay in sync with actual `.husky/` hook files. During T12 (post-merge marker), a mismatch was discovered between the template content in constants.js (which already had the marker via `buildHookContent()`) and the actual disk file. Solution: update constants.js template content whenever hook files are modified
38
+
39
+ **Agent tuning notes:**
40
+ - **researcher**: Should explicitly investigate npm ecosystem changes (version-specific behavior, deprecations) — the npm v12 postinstall discovery was the single most important finding that drove the architecture
41
+ - **architect**: Modular design with clear interface contracts (Context object shape, return types) paid off — all 5 Batch B modules could be implemented in parallel without coordination issues
42
+ - **implementer**: The parallel batch plan with explicit dependency edges (Batch A → B → C, D+E → F) was critical for the 6 sub-agent coder execution. Without this structure, 6 concurrent coders would have had race conditions on shared files
43
+ - **coder (parallel batches)**: Batch ordering discipline worked — foundation first, then core, then orchestrator. The truly independent batches (D: markers, F: tests) could run alongside later batches. Key lesson: define interfaces (Context shape, return types) before splitting work across parallel coders
44
+ - **unit-tester**: Using real temp directories (`mkdtempSync`) over mocking filesystem operations catches path resolution and permission issues that mocks would miss. Mock only environment-dependent utilities (CI detection, Node version), not filesystem
45
+ - **tracker**: Should capture ecosystem discoveries alongside implementation details — the npm v12 finding and marker idempotency pattern are valuable knowledge for future pipeline sessions
@@ -19,6 +19,9 @@ jobs:
19
19
  - name: Install dependencies
20
20
  run: npm ci
21
21
 
22
+ - name: Run unit tests
23
+ run: npm run test:unit
24
+
22
25
  - name: Publish to npm
23
26
  run: npm publish --access public
24
27
  env:
@@ -0,0 +1,23 @@
1
+ name: PR Unit Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: '22'
17
+ cache: 'npm'
18
+
19
+ - name: Install dependencies
20
+ run: npm ci
21
+
22
+ - name: Run unit tests
23
+ run: npm run test:unit
package/.husky/post-merge CHANGED
@@ -1,3 +1,4 @@
1
+ # Managed by @abarcenas/ai-workflow-template setup
1
2
  #!/bin/sh
2
3
  . "$(dirname "$0")/_/husky.sh"
3
4
 
package/.husky/pre-commit CHANGED
@@ -1,2 +1,4 @@
1
+ # Managed by @abarcenas/ai-workflow-template setup
2
+
1
3
  node scripts/bump-version.js
2
4
  node scripts/validate-memory-schema.js
package/README.md CHANGED
@@ -35,7 +35,53 @@ Install directly into the project that needs AI workflow configuration:
35
35
  npm install @abarcenas/ai-workflow-template
36
36
  ```
37
37
 
38
- The package runs `postinstall` automatically to sync configurations.
38
+ The package runs `postinstall` automatically to sync files. For full setup including git hooks, run the one-step setup command below.
39
+
40
+ ---
41
+
42
+ ## 🪝 Git Hook Setup
43
+
44
+ After installing the package, configure husky git hooks with one command:
45
+
46
+ ```bash
47
+ # One-step setup (fetches from npm registry automatically):
48
+ npx @abarcenas/ai-workflow-template
49
+
50
+ # Or if the package is already installed:
51
+ npx ai-workflow-setup
52
+ ```
53
+
54
+ ### What It Does
55
+
56
+ The setup command automatically:
57
+
58
+ | Phase | Action |
59
+ |-------|--------|
60
+ | 🔍 **Discover** | Detects git repo, existing hooks, CI mode |
61
+ | 🪝 **Hooks** | Installs `pre-commit` (version bump + validation) and `post-merge` (memory index update) |
62
+ | 📦 **Prepare** | Merges `"husky"` into your `package.json` prepare script |
63
+ | ⚙️ **Husky Init** | Initializes husky and sets `core.hooksPath` |
64
+ | 🔄 **Sync** | Copies agent configurations, skills, and memory bank scaffolding |
65
+
66
+ ### Options
67
+
68
+ ```bash
69
+ npx ai-workflow-setup --dry-run # Preview without changes
70
+ npx ai-workflow-setup --force # Overwrite existing hooks
71
+ npx ai-workflow-setup --skip-hooks # Skip hook installation
72
+ npx ai-workflow-setup --skip-sync # Skip file sync
73
+ npx ai-workflow-setup --help # Show all options
74
+ ```
75
+
76
+ ### Hook Merging
77
+
78
+ If you already have existing husky hooks, the setup command intelligently merges:
79
+ - **Your hooks are never overwritten** — new content is appended after a separator
80
+ - **Managed hooks are detected** via marker comments and updated idempotently
81
+ - **Complex hooks** trigger a warning with manual merge instructions
82
+ - **CI environments** auto-skip hook configuration
83
+
84
+ > **ℹ️ npm v12 note:** Future npm versions block automatic `postinstall` scripts. The explicit `npx setup` command is the recommended, future-proof approach.
39
85
 
40
86
  ---
41
87
 
@@ -416,6 +462,7 @@ npm run memory:normalize
416
462
  - 📖 `AGENTS.md` is the main instruction file agents should read
417
463
  - 🎯 Package assumes public npm registry
418
464
  - 🎭 Playwright MCP integration documented in `docs/` folder
465
+ - 🪝 Run `npx ai-workflow-setup` after install to configure git hooks (npm v12+ compatible)
419
466
 
420
467
  ---
421
468
 
package/bin/setup.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+ // bin/setup.js — CLI entry point for `npx ai-workflow-setup`
5
+ // ═════════════════════════════════════════════════════════════════════════════
6
+ //
7
+ // Minimal shim that delegates to the orchestrator (scripts/setup/index.js).
8
+ // Uses top-level await via ESM (requires "type": "module" in package.json).
9
+ //
10
+ // Exit codes:
11
+ // 0 — All phases completed successfully
12
+ // 1 — One or more phases had warnings / were skipped
13
+ // 2 — Fatal error (discovery failure or unexpected exception)
14
+ // ═════════════════════════════════════════════════════════════════════════════
15
+
16
+ try {
17
+ const { main } = await import('../scripts/setup/index.js')
18
+ const exitCode = await main(process.argv.slice(2))
19
+ process.exit(exitCode)
20
+ } catch (err) {
21
+ console.error('Fatal error:', err.message)
22
+ process.exit(2)
23
+ }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@abarcenas/ai-workflow-template",
3
- "version": "1.29.0",
3
+ "version": "1.32.0",
4
4
  "description": "A comprehensive template repository for AI-assisted development workflows using GitHub Copilot. This template provides a structured approach to leveraging AI assistance through pre-configured skills, prompts, agents, and coding standards.",
5
5
  "main": "index.js",
6
+ "bin": {
7
+ "ai-workflow-setup": "./bin/setup.js"
8
+ },
6
9
  "scripts": {
7
10
  "test": "playwright test",
8
11
  "test:headed": "playwright test --headed",
@@ -14,7 +17,7 @@
14
17
  "test:unit:watch": "vitest",
15
18
  "prepare": "husky",
16
19
  "sync": "node ./scripts/sync.js",
17
- "postinstall": "node ./scripts/sync.js && node ./scripts/normalize-memory.js",
20
+ "postinstall": "node ./scripts/sync.js && node ./scripts/normalize-memory.js && echo '' && echo '⚠️ IMPORTANT: Run npx ai-workflow-setup to configure git hooks' && echo ' (npm postinstall scripts will be blocked by npm v12+)'",
18
21
  "memory:search": "node ./scripts/memory-cli.js search",
19
22
  "memory:rebuild": "node ./scripts/memory-cli.js rebuild",
20
23
  "memory:update": "node ./scripts/memory-cli.js update",
@@ -33,9 +36,11 @@
33
36
  },
34
37
  "homepage": "https://github.com/abarcenas29/ai-workflow-template#readme",
35
38
  "files": [
39
+ "bin/",
36
40
  ".agents/",
37
41
  ".github/workflows/",
38
42
  "scripts/",
43
+ "scripts/setup/",
39
44
  ".opencode/agents/",
40
45
  ".opencode/commands/",
41
46
  ".husky/",
@@ -0,0 +1,226 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // constants.js — Shared constants for the setup command pipeline
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+
5
+ // ── Package identity ──────────────────────────────────────────────────────────
6
+
7
+ /** npm package name */
8
+ export const PACKAGE_NAME = '@abarcenas/ai-workflow-template'
9
+
10
+ /** CLI binary name (npm "bin" entry) */
11
+ export const BIN_NAME = 'ai-workflow-setup'
12
+
13
+ // ── Idempotency markers ───────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * First-line comment injected into every template hook file.
17
+ * Used to detect whether an existing consumer hook was installed by us
18
+ * (idempotency check in hooks.js, CASE C). Must be the first line of
19
+ * each template hook file.
20
+ */
21
+ export const HOOK_MARKER = '# Managed by @abarcenas/ai-workflow-template setup'
22
+
23
+ /**
24
+ * Separator inserted between a consumer's existing hook content and our
25
+ * appended content during hook merging (hooks.js, CASE D).
26
+ */
27
+ export const HOOK_MERGE_SEPARATOR = '# --- @abarcenas/ai-workflow-template ---'
28
+
29
+ // ── Template hook definitions ─────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Map of hook names → metadata for every git hook the setup command manages.
33
+ *
34
+ * Each entry carries:
35
+ * `source` — Relative path from the package root to the template file.
36
+ * Resolved at runtime by hooks.js.
37
+ * `content` — Exact content that will be written (or appended) to the
38
+ * consumer's hook file. This mirrors the actual file on disk.
39
+ * `description` — Human-readable purpose, shown in output messages.
40
+ */
41
+ export const TEMPLATE_HOOKS = {
42
+ 'pre-commit': {
43
+ source: '.husky/pre-commit',
44
+ content:
45
+ '# Managed by @abarcenas/ai-workflow-template setup\n\nnode scripts/bump-version.js\nnode scripts/validate-memory-schema.js\n',
46
+ description:
47
+ 'Auto-bump version + validate memory-bank schema before commits',
48
+ },
49
+ 'post-merge': {
50
+ source: '.husky/post-merge',
51
+ content: [
52
+ '# Managed by @abarcenas/ai-workflow-template setup',
53
+ '#!/bin/sh',
54
+ '. "$(dirname "$0")/_/husky.sh"',
55
+ '',
56
+ '# After git pull/merge, check if memory-bank files changed',
57
+ '# and update the vector index incrementally',
58
+ '',
59
+ 'if git diff HEAD@{1} --name-only 2>/dev/null | grep -q "^memory-bank\\|^docs/"; then',
60
+ ' echo "[memory-bank] Memory bank files changed. Updating vector index..."',
61
+ ' node scripts/memory-cli.js update 2>/dev/null || echo "[memory-bank] Index update skipped (service not installed)"',
62
+ 'fi',
63
+ '',
64
+ ].join('\n'),
65
+ description:
66
+ 'Auto-update memory bank vector index after pull/merge',
67
+ },
68
+ }
69
+
70
+ // ── CI/CD environment detection ───────────────────────────────────────────────
71
+
72
+ /**
73
+ * Environment variable names checked to determine if the command is running
74
+ * inside a CI/CD pipeline.
75
+ *
76
+ * When `isCI()` from utils.js returns true, the setup command skips git hook
77
+ * installation entirely (REQ-07) and only performs file sync + memory-bank
78
+ * scaffolding.
79
+ *
80
+ * @note `NODE_ENV` is included here but is evaluated as
81
+ * `process.env.NODE_ENV === 'production'` (equality check), whereas
82
+ * all other entries are evaluated as simple truthiness checks.
83
+ */
84
+ export const CI_ENV_VARS = [
85
+ 'CI',
86
+ 'GITHUB_ACTIONS',
87
+ 'GITLAB_CI',
88
+ 'CIRCLECI',
89
+ 'TRAVIS',
90
+ 'JENKINS_URL',
91
+ 'BITBUCKET_BUILD_NUMBER',
92
+ 'APPVEYOR',
93
+ 'CODEBUILD_BUILD_ID',
94
+ 'AZURE_HTTP_USER_AGENT',
95
+ 'NODE_ENV',
96
+ ]
97
+
98
+ // ── CLI flags ─────────────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Map of CLI argument strings (long and short forms) to their canonical
102
+ * camelCase property names on the flags object.
103
+ *
104
+ * Usage: `parseCliArgs()` in utils.js iterates over argv, looks up each
105
+ * element in this map, and sets the corresponding property to `true` on
106
+ * the returned flags object.
107
+ */
108
+ export const SUPPORTED_FLAGS = {
109
+ '--force': 'force',
110
+ '--dry-run': 'dryRun',
111
+ '--yes': 'yes',
112
+ '-y': 'yes',
113
+ '--skip-hooks': 'skipHooks',
114
+ '--skip-prepare': 'skipPrepare',
115
+ '--skip-sync': 'skipSync',
116
+ '--help': 'help',
117
+ '-h': 'help',
118
+ '--version': 'version',
119
+ '-v': 'version',
120
+ '--quiet': 'quiet',
121
+ '-q': 'quiet',
122
+ '--verbose': 'verbose',
123
+ '-V': 'verbose',
124
+ }
125
+
126
+ // ── Exit codes ────────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Numeric exit codes returned by the setup command.
130
+ *
131
+ * SUCCESS → All phases completed without errors or warnings
132
+ * WARNINGS → One or more phases had non-fatal warnings (exit code 1)
133
+ * FATAL → A required phase encountered an unrecoverable error (exit code 2)
134
+ */
135
+ export const EXIT_CODES = {
136
+ SUCCESS: 0,
137
+ WARNINGS: 1,
138
+ FATAL: 2,
139
+ }
140
+
141
+ // ── Required directories ──────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Relative paths (from the consumer project root) of directories that the
145
+ * setup command must ensure exist before running its pipeline.
146
+ */
147
+ export const REQUIRED_DIRS = [
148
+ '.husky',
149
+ 'memory-bank',
150
+ ]
151
+
152
+ // ── Output symbols ────────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Unicode characters used as status indicators in the CLI output.
156
+ * Referenced by the ui.js module.
157
+ */
158
+ export const UNICODE_CHARS = {
159
+ CHECK: '\u2713', // ✓
160
+ WARN: '\u26A0', // ⚠
161
+ CROSS: '\u2717', // ✗
162
+ BULLET: '\u2022', // •
163
+ ARROW: '\u2192', // →
164
+ }
165
+
166
+ // ── Memory bank stubs ─────────────────────────────────────────────────────────
167
+
168
+ /**
169
+ * Template content for scaffolding a fresh memory-bank/ directory.
170
+ * Exact mirror of the `memoryBankStubs` object in `scripts/sync.js`
171
+ * (lines 204–254). Each key is a filename; each value is the file content.
172
+ *
173
+ * Used by the setup command's sync-phase when the consumer project lacks
174
+ * a memory-bank/ directory.
175
+ */
176
+ export const MEMORY_BANK_STUBS = {
177
+ 'projectbrief.md': `# projectbrief
178
+
179
+ > Foundation document — defines core requirements and goals.
180
+
181
+ - **Purpose**: {{describe project purpose}}
182
+ - **Goals**: {{list project goals}}
183
+ - **Scope**: {{define scope}}
184
+ `,
185
+ 'productContext.md': `# productContext
186
+
187
+ > Why this project exists and what problems it solves.
188
+
189
+ - **Problem**: {{describe the problem}}
190
+ - **Solution**: {{describe the solution}}
191
+ - **User Experience**: {{describe UX goals}}
192
+ `,
193
+ 'activeContext.md': `# activeContext
194
+
195
+ > Current work focus, recent changes, next steps.
196
+
197
+ - **Current Focus**: {{current task or goal}}
198
+ - **Recent Changes**: {{list of recent changes}}
199
+ - **Next Steps**: {{next actions}}
200
+ `,
201
+ 'systemPatterns.md': `# systemPatterns
202
+
203
+ > Architecture, key decisions, design patterns.
204
+
205
+ - **Architecture**: {{describe architecture}}
206
+ - **Key Decisions**: {{list key technical decisions}}
207
+ - **Patterns**: {{list design patterns in use}}
208
+ `,
209
+ 'techContext.md': `# techContext
210
+
211
+ > Technologies, setup, constraints, dependencies.
212
+
213
+ - **Stack**: {{list technologies}}
214
+ - **Setup**: {{describe dev setup}}
215
+ - **Constraints**: {{list technical constraints}}
216
+ `,
217
+ 'progress.md': `# progress
218
+
219
+ > What works, what's left, current status.
220
+
221
+ - **Working**: {{what's implemented and working}}
222
+ - **To Build**: {{what remains}}
223
+ - **Status**: {{overall project status}}
224
+ - **Known Issues**: {{list known issues}}
225
+ `,
226
+ }
@@ -0,0 +1,236 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // discover.js — Consumer project state detection for the setup command pipeline
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+
5
+ import { resolve } from 'path'
6
+
7
+ import { HOOK_MARKER, TEMPLATE_HOOKS } from './constants.js'
8
+ import {
9
+ dirExists,
10
+ getConsumerRoot,
11
+ getNodeVersion,
12
+ isCI,
13
+ safeReadFile,
14
+ safeReadJson,
15
+ } from './utils.js'
16
+
17
+ // ── Defaults ─────────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Returns a default hook descriptor for cases where detection fails.
21
+ *
22
+ * @returns {{ exists: boolean, content: null, isManaged: boolean }}
23
+ */
24
+ function defaultHookEntry() {
25
+ return { exists: false, content: null, isManaged: false }
26
+ }
27
+
28
+ /**
29
+ * Builds a minimal Context object with every property initialised to a safe
30
+ * default. Callers can always destructure the returned Context without
31
+ * checking for undefined.
32
+ *
33
+ * @param {object} [flags={}]
34
+ * @returns {Context}
35
+ */
36
+ function createDefaultContext(flags = {}) {
37
+ /** @type {Record<string, { exists: boolean, content: string|null, isManaged: boolean }>} */
38
+ const existingHooks = {}
39
+ for (const name of Object.keys(TEMPLATE_HOOKS)) {
40
+ existingHooks[name] = defaultHookEntry()
41
+ }
42
+
43
+ return {
44
+ consumerRoot: '',
45
+ hasGit: false,
46
+ hasHuskyDir: false,
47
+ hasPackageJson: false,
48
+ consumerPackageJson: null,
49
+ existingHooks,
50
+ existingPrepare: null,
51
+ prepareIsSimple: false,
52
+ isCI: false,
53
+ nodeVersion: 0,
54
+ dryRun: !!flags.dryRun,
55
+ }
56
+ }
57
+
58
+ // ── Prepare script classifier ────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Determines whether an existing `prepare` script is "simple" enough for
62
+ * automatic `&& husky` appending.
63
+ *
64
+ * A script is considered **simple** when it is a single-line command or a basic
65
+ * `&&` chain. It is **complex** (and will not be auto-merged) when any of
66
+ * these apply:
67
+ *
68
+ * - Multi-line (contains `\n`)
69
+ * - Contains the `||` (OR) operator
70
+ * - Contains a semicolon `;` command separator
71
+ *
72
+ * @param {string|null} prepare - The current `prepare` script value.
73
+ * @returns {boolean} `true` if auto-merge is safe.
74
+ */
75
+ function isPrepareSimple(prepare) {
76
+ if (typeof prepare !== 'string' || prepare.trim() === '') return false
77
+
78
+ const val = prepare.trim()
79
+ if (val.includes('\n')) return false
80
+ if (val.includes('||')) return false
81
+ if (val.includes(';')) return false
82
+
83
+ return true
84
+ }
85
+
86
+ // ── Hook detection helpers ───────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Reads a single hook file from the consumer's `.husky/` directory and returns
90
+ * a descriptor containing the existence, raw content, and managed status.
91
+ *
92
+ * A hook is considered **managed** when its very first line (trimmed) matches
93
+ * the `HOOK_MARKER` constant.
94
+ *
95
+ * @param {string} consumerRoot - Absolute path to the consumer project root.
96
+ * @param {string} hookName - Hook filename (e.g. `'pre-commit'`).
97
+ * @returns {{ exists: boolean, content: string|null, isManaged: boolean }}
98
+ */
99
+ function detectHook(consumerRoot, hookName) {
100
+ try {
101
+ const hookPath = resolve(consumerRoot, '.husky', hookName)
102
+ const content = safeReadFile(hookPath)
103
+ const exists = content !== null
104
+
105
+ if (!exists) return { exists, content: null, isManaged: false }
106
+
107
+ const firstLine = content.split('\n')[0]
108
+ const isManaged =
109
+ firstLine != null && firstLine.trim() === HOOK_MARKER
110
+
111
+ return { exists, content, isManaged }
112
+ } catch {
113
+ return defaultHookEntry()
114
+ }
115
+ }
116
+
117
+ // ── Main export ──────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Discovers the consumer project's state and returns a Context object.
121
+ *
122
+ * The returned Context captures the environment before any modifications are
123
+ * made, so the orchestrator (and downstream pipeline modules) can decide which
124
+ * phases to run and how to handle each edge case.
125
+ *
126
+ * All detection steps are wrapped in individual try/catch blocks so that a
127
+ * failure in one area (e.g., reading package.json) does not prevent the rest
128
+ * of the discovery from completing. The Context is **always** returned, even
129
+ * if every detection step fails.
130
+ *
131
+ * @param {object} [flags={}] - Parsed CLI flags from `parseCliArgs()`.
132
+ * @param {boolean} [flags.dryRun] - When `true`, discovery notes dry-run mode.
133
+ * @returns {Promise<Context>} Resolved context object.
134
+ *
135
+ * @example
136
+ * ```js
137
+ * import { parseCliArgs } from './utils.js'
138
+ * import { discover } from './discover.js'
139
+ *
140
+ * const flags = parseCliArgs(process.argv.slice(2))
141
+ * const ctx = await discover(flags)
142
+ * console.log(ctx.hasGit) // true / false
143
+ * ```
144
+ */
145
+ export async function discover(flags = {}) {
146
+ const ctx = createDefaultContext(flags)
147
+
148
+ // ── 1. Resolve consumer root ──────────────────────────────────────────
149
+ try {
150
+ ctx.consumerRoot = getConsumerRoot()
151
+ } catch {
152
+ // If consumerRoot cannot be resolved we return early — no further
153
+ // detection is possible without a target directory.
154
+ ctx.nodeVersion = getNodeVersion()
155
+ ctx.isCI = isCI()
156
+ return ctx
157
+ }
158
+
159
+ if (!ctx.consumerRoot) {
160
+ ctx.nodeVersion = getNodeVersion()
161
+ ctx.isCI = isCI()
162
+ return ctx
163
+ }
164
+
165
+ // ── 2. Detect git repository ──────────────────────────────────────────
166
+ try {
167
+ ctx.hasGit = dirExists(resolve(ctx.consumerRoot, '.git'))
168
+ } catch {
169
+ // Non-fatal — hasGit stays false
170
+ }
171
+
172
+ // ── 3. Detect .husky/ directory ───────────────────────────────────────
173
+ try {
174
+ ctx.hasHuskyDir = dirExists(resolve(ctx.consumerRoot, '.husky'))
175
+ } catch {
176
+ // Non-fatal — hasHuskyDir stays false
177
+ }
178
+
179
+ // ── 4. Detect + parse package.json ────────────────────────────────────
180
+ try {
181
+ const pkgPath = resolve(ctx.consumerRoot, 'package.json')
182
+ const pkg = safeReadJson(pkgPath)
183
+ ctx.hasPackageJson = pkg !== undefined
184
+ ctx.consumerPackageJson = pkg ?? null
185
+ } catch {
186
+ // Non-fatal — hasPackageJson stays false, consumerPackageJson stays null
187
+ }
188
+
189
+ // ── 5. Detect existing hooks ──────────────────────────────────────────
190
+ // existingHooks is pre-initialised by createDefaultContext() so the loop
191
+ // below only overrides the entries it successfully reads. Any hook that
192
+ // cannot be read keeps its safe default (exists: false).
193
+ try {
194
+ for (const hookName of Object.keys(TEMPLATE_HOOKS)) {
195
+ ctx.existingHooks[hookName] = detectHook(ctx.consumerRoot, hookName)
196
+ }
197
+ } catch {
198
+ // Non-fatal — existingHooks already contains default entries
199
+ }
200
+
201
+ // ── 6. Extract existing prepare script ────────────────────────────────
202
+ try {
203
+ const prepare = ctx.consumerPackageJson?.scripts?.prepare
204
+ ctx.existingPrepare =
205
+ typeof prepare === 'string' && prepare.trim().length > 0
206
+ ? prepare.trim()
207
+ : null
208
+ } catch {
209
+ // Non-fatal — existingPrepare stays null
210
+ }
211
+
212
+ // ── 7. Classify prepare script simplicity ─────────────────────────────
213
+ try {
214
+ ctx.prepareIsSimple = isPrepareSimple(ctx.existingPrepare)
215
+ } catch {
216
+ ctx.prepareIsSimple = false
217
+ }
218
+
219
+ // ── 8. Detect CI environment ──────────────────────────────────────────
220
+ try {
221
+ ctx.isCI = isCI()
222
+ } catch {
223
+ ctx.isCI = false
224
+ }
225
+
226
+ // ── 9. Detect Node.js version ─────────────────────────────────────────
227
+ try {
228
+ ctx.nodeVersion = getNodeVersion()
229
+ } catch {
230
+ ctx.nodeVersion = 0
231
+ }
232
+
233
+ // NOTE: `ctx.dryRun` is already set by createDefaultContext(flags)
234
+
235
+ return ctx
236
+ }