@abarcenas/ai-workflow-template 1.28.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.
@@ -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
+ }