@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
|
@@ -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
|
|
@@ -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
package/.husky/pre-commit
CHANGED
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
|
|
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.
|
|
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
|
+
}
|