@abarcenas/ai-workflow-template 1.45.0 → 1.46.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/config/README.md +43 -0
- package/.agents/config/domain.md +37 -0
- package/.agents/instructions/learned-knowledge.instructions.md +110 -0
- package/.agents/instructions/memory-bank.instructions.md +11 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +74 -0
- package/.agents/skills/grill-me/SKILL.md +7 -0
- package/.agents/skills/grill-with-docs/SKILL.md +7 -0
- package/.agents/skills/grilling/SKILL.md +28 -0
- package/.agents/skills/prototype/LOGIC.md +67 -0
- package/.agents/skills/prototype/SKILL.md +26 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.github/workflows/npm-publish.yml +10 -0
- package/.github/workflows/pr-test.yml +31 -0
- package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
- package/.opencode/agents/scope.agent.md +299 -0
- package/AGENTS.md +42 -3
- package/README.md +5 -2
- package/package.json +1 -1
- package/scripts/bump-version.test.js +238 -0
- package/scripts/mcp/playwright-mcp-launcher.js +55 -29
- package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
- package/scripts/mcp-memory-server.js +93 -76
- package/scripts/mcp-memory-server.test.js +398 -0
- package/scripts/memory-cli.test.js +297 -0
- package/scripts/memory-index.js +119 -31
- package/scripts/memory-index.test.js +1173 -0
- package/scripts/normalize-memory.js +26 -1
- package/scripts/normalize-memory.test.js +753 -0
- package/scripts/setup/discover.test.js +124 -2
- package/scripts/setup/hooks.test.js +188 -1
- package/scripts/setup/husky-init.test.js +311 -0
- package/scripts/setup/index.js +35 -5
- package/scripts/setup/index.test.js +279 -0
- package/scripts/setup/knowledgebase.test.js +100 -0
- package/scripts/setup/skills-phase.js +568 -0
- package/scripts/setup/skills-phase.test.js +1274 -0
- package/scripts/setup/sync-phase.js +6 -1
- package/scripts/setup/sync-phase.test.js +180 -2
- package/scripts/setup/ui.test.js +579 -0
- package/scripts/setup/utils.test.js +586 -0
- package/scripts/sync-unit.test.js +1342 -0
- package/scripts/sync.js +540 -246
- package/scripts/sync.test.js +283 -0
- package/scripts/vocab-sync.test.js +88 -0
- package/skills-lock.json +90 -0
package/scripts/sync.js
CHANGED
|
@@ -1,28 +1,197 @@
|
|
|
1
1
|
import { createHash } from 'crypto'
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'fs'
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path'
|
|
4
14
|
import { fileURLToPath } from 'url'
|
|
5
15
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const consumerRoot = process.env.INIT_CWD ? resolve(process.env.INIT_CWD) : process.cwd()
|
|
10
|
-
const targetAgentsDir = resolve(consumerRoot, '.agents')
|
|
11
|
-
const manifestPath = resolve(consumerRoot, '.agents-sync-manifest.json')
|
|
12
|
-
const excludedRelativePaths = new Set([
|
|
16
|
+
const defaultPackageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
17
|
+
|
|
18
|
+
export const excludedRelativePaths = new Set([
|
|
13
19
|
'instructions/learned-knowledge.instructions.md',
|
|
14
20
|
])
|
|
15
|
-
const args = new Set(process.argv.slice(2))
|
|
16
|
-
const forceOverwrite = args.has('--force')
|
|
17
|
-
const dryRun = args.has('--dry-run')
|
|
18
|
-
const isVerbose = process.env.AI_WORKFLOW_VERBOSE === '1'
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
export const openCodeExcluded = new Set([
|
|
23
|
+
'node_modules',
|
|
24
|
+
'package.json',
|
|
25
|
+
'package-lock.json',
|
|
26
|
+
'bun.lock',
|
|
27
|
+
'.gitignore',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
export const rootFiles = ['AGENTS.md', 'opencode.json', 'skills-lock.json']
|
|
31
|
+
|
|
32
|
+
export const scriptsToSync = [
|
|
33
|
+
'memory-cli.js',
|
|
34
|
+
'memory-index.js',
|
|
35
|
+
'bump-version.js',
|
|
36
|
+
'validate-memory-schema.js',
|
|
37
|
+
'vocab-sync.js',
|
|
38
|
+
'mcp-memory-server.js',
|
|
39
|
+
'mcp/playwright-mcp-launcher.js',
|
|
40
|
+
// Knowledgebase scripts
|
|
41
|
+
'knowledgebase-cli.js',
|
|
42
|
+
'knowledgebase-index.js',
|
|
43
|
+
'mcp-knowledgebase-server.js',
|
|
44
|
+
'knowledgebase-init.sql',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
export const memoryBankStubs = {
|
|
48
|
+
'projectbrief.md': `# projectbrief
|
|
49
|
+
|
|
50
|
+
> Foundation document — defines core requirements and goals.
|
|
51
|
+
|
|
52
|
+
- **Purpose**: {{describe project purpose}}
|
|
53
|
+
- **Goals**: {{list project goals}}
|
|
54
|
+
- **Scope**: {{define scope}}
|
|
55
|
+
`,
|
|
56
|
+
'productContext.md': `# productContext
|
|
57
|
+
|
|
58
|
+
> Why this project exists and what problems it solves.
|
|
59
|
+
|
|
60
|
+
- **Problem**: {{describe the problem}}
|
|
61
|
+
- **Solution**: {{describe the solution}}
|
|
62
|
+
- **User Experience**: {{describe UX goals}}
|
|
63
|
+
`,
|
|
64
|
+
'activeContext.md': `# activeContext
|
|
65
|
+
|
|
66
|
+
> Current work focus, recent changes, next steps.
|
|
67
|
+
|
|
68
|
+
- **Current Focus**: {{current task or goal}}
|
|
69
|
+
- **Recent Changes**: {{list of recent changes}}
|
|
70
|
+
- **Next Steps**: {{next actions}}
|
|
71
|
+
`,
|
|
72
|
+
'systemPatterns.md': `# systemPatterns
|
|
73
|
+
|
|
74
|
+
> Architecture, key decisions, design patterns.
|
|
75
|
+
|
|
76
|
+
- **Architecture**: {{describe architecture}}
|
|
77
|
+
- **Key Decisions**: {{list key technical decisions}}
|
|
78
|
+
- **Patterns**: {{list design patterns in use}}
|
|
79
|
+
`,
|
|
80
|
+
'techContext.md': `# techContext
|
|
81
|
+
|
|
82
|
+
> Technologies, setup, constraints, dependencies.
|
|
83
|
+
|
|
84
|
+
- **Stack**: {{list technologies}}
|
|
85
|
+
- **Setup**: {{describe dev setup}}
|
|
86
|
+
- **Constraints**: {{list technical constraints}}
|
|
87
|
+
`,
|
|
88
|
+
'progress.md': `# progress
|
|
89
|
+
|
|
90
|
+
> What works, what's left, current status.
|
|
91
|
+
|
|
92
|
+
- **Working**: {{what's implemented and working}}
|
|
93
|
+
- **To Build**: {{what remains}}
|
|
94
|
+
- **Status**: {{overall project status}}
|
|
95
|
+
- **Known Issues**: {{list known issues}}
|
|
96
|
+
`,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const agentInstructionsStubs = {
|
|
100
|
+
'learned-knowledge.instructions.md': `# Learned Knowledge
|
|
101
|
+
|
|
102
|
+
Cross-session knowledge accumulated by agent pipelines. Each session records discoveries, patterns, gotchas, and agent tuning notes for future reference.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
`,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parses the flags supported by the sync entry point. Unknown flags are ignored
|
|
110
|
+
// so that consumer tooling can pass extra arguments without breaking a sync.
|
|
111
|
+
export function parseArgs(argv = [], env = process.env) {
|
|
112
|
+
const args = new Set(argv)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
forceOverwrite: args.has('--force'),
|
|
116
|
+
dryRun: args.has('--dry-run'),
|
|
117
|
+
// Pruning is opt-in and off by default: syncing never deletes anything unless
|
|
118
|
+
// the caller explicitly passes --prune. Even then, only manifest-tracked files
|
|
119
|
+
// whose package source has disappeared AND whose content is unmodified are removed.
|
|
120
|
+
prune: args.has('--prune'),
|
|
121
|
+
isVerbose: env.AI_WORKFLOW_VERBOSE === '1',
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Resolves every directory and flag the sync pass works with. Tests pass an
|
|
126
|
+
// explicit packageRoot/consumerRoot; production derives the package root from
|
|
127
|
+
// this module's location and the consumer root from INIT_CWD (npm) or cwd.
|
|
128
|
+
export function createSyncContext(options = {}) {
|
|
129
|
+
const {
|
|
130
|
+
packageRoot,
|
|
131
|
+
consumerRoot,
|
|
132
|
+
manifestPath,
|
|
133
|
+
argv = process.argv.slice(2),
|
|
134
|
+
env = process.env,
|
|
135
|
+
} = options
|
|
136
|
+
|
|
137
|
+
const resolvedPackageRoot = packageRoot
|
|
138
|
+
? resolve(packageRoot)
|
|
139
|
+
: defaultPackageRoot
|
|
140
|
+
const resolvedConsumerRoot = consumerRoot
|
|
141
|
+
? resolve(consumerRoot)
|
|
142
|
+
: env.INIT_CWD
|
|
143
|
+
? resolve(env.INIT_CWD)
|
|
144
|
+
: process.cwd()
|
|
145
|
+
|
|
146
|
+
const sourceAgentsDir = resolve(resolvedPackageRoot, '.agents')
|
|
147
|
+
const targetAgentsDir = resolve(resolvedConsumerRoot, '.agents')
|
|
148
|
+
const sourceOpenCodeDir = resolve(resolvedPackageRoot, '.opencode')
|
|
149
|
+
const sourceScriptsDir = resolve(resolvedPackageRoot, 'scripts')
|
|
150
|
+
const targetScriptsDir = resolve(resolvedConsumerRoot, 'scripts')
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
packageRoot: resolvedPackageRoot,
|
|
154
|
+
consumerRoot: resolvedConsumerRoot,
|
|
155
|
+
sourceAgentsDir,
|
|
156
|
+
targetAgentsDir,
|
|
157
|
+
sourceOpenCodeDir,
|
|
158
|
+
sourceScriptsDir,
|
|
159
|
+
targetScriptsDir,
|
|
160
|
+
manifestPath: manifestPath
|
|
161
|
+
? resolve(manifestPath)
|
|
162
|
+
: resolve(resolvedConsumerRoot, '.agents-sync-manifest.json'),
|
|
163
|
+
...parseArgs(argv, env),
|
|
164
|
+
pruneChannels: {
|
|
165
|
+
agents: {
|
|
166
|
+
prefix: '',
|
|
167
|
+
packageDir: sourceAgentsDir,
|
|
168
|
+
consumerDir: targetAgentsDir,
|
|
169
|
+
},
|
|
170
|
+
opencode: {
|
|
171
|
+
prefix: '__opencode__/',
|
|
172
|
+
packageDir: sourceOpenCodeDir,
|
|
173
|
+
consumerDir: resolve(resolvedConsumerRoot, '.opencode'),
|
|
174
|
+
},
|
|
175
|
+
scripts: {
|
|
176
|
+
prefix: '__scripts__/',
|
|
177
|
+
packageDir: sourceScriptsDir,
|
|
178
|
+
consumerDir: targetScriptsDir,
|
|
179
|
+
},
|
|
180
|
+
root: {
|
|
181
|
+
prefix: '__root__/',
|
|
182
|
+
packageDir: resolvedPackageRoot,
|
|
183
|
+
consumerDir: resolvedConsumerRoot,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function hashFile(filePath) {
|
|
21
190
|
const contents = readFileSync(filePath)
|
|
22
191
|
return createHash('sha256').update(contents).digest('hex')
|
|
23
192
|
}
|
|
24
193
|
|
|
25
|
-
function collectFiles(directory) {
|
|
194
|
+
export function collectFiles(directory) {
|
|
26
195
|
const entries = readdirSync(directory, { withFileTypes: true })
|
|
27
196
|
const files = []
|
|
28
197
|
|
|
@@ -41,7 +210,7 @@ function collectFiles(directory) {
|
|
|
41
210
|
return files
|
|
42
211
|
}
|
|
43
212
|
|
|
44
|
-
function readManifest() {
|
|
213
|
+
export function readManifest(manifestPath) {
|
|
45
214
|
if (!existsSync(manifestPath)) {
|
|
46
215
|
return { version: 1, files: {} }
|
|
47
216
|
}
|
|
@@ -62,7 +231,7 @@ function readManifest() {
|
|
|
62
231
|
}
|
|
63
232
|
}
|
|
64
233
|
|
|
65
|
-
function writeManifest(manifest) {
|
|
234
|
+
export function writeManifest(manifestPath, manifest) {
|
|
66
235
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
67
236
|
}
|
|
68
237
|
|
|
@@ -70,7 +239,7 @@ function ensureParentDirectory(filePath) {
|
|
|
70
239
|
mkdirSync(dirname(filePath), { recursive: true })
|
|
71
240
|
}
|
|
72
241
|
|
|
73
|
-
function syncFile(sourceFile, targetFile) {
|
|
242
|
+
function syncFile(sourceFile, targetFile, dryRun) {
|
|
74
243
|
if (dryRun) {
|
|
75
244
|
return
|
|
76
245
|
}
|
|
@@ -79,311 +248,436 @@ function syncFile(sourceFile, targetFile) {
|
|
|
79
248
|
copyFileSync(sourceFile, targetFile)
|
|
80
249
|
}
|
|
81
250
|
|
|
82
|
-
|
|
251
|
+
// A file is safe to overwrite when the manifest recorded a hash for it and the
|
|
252
|
+
// consumer copy still matches that hash. Missing recorded hashes mean the file
|
|
253
|
+
// is not tracked, so local edits are assumed and the copy is skipped.
|
|
254
|
+
export function isUntouched(recordedHash, existingHash) {
|
|
255
|
+
return recordedHash !== null && existingHash === recordedHash
|
|
256
|
+
}
|
|
83
257
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
258
|
+
export function isPathInside(baseDir, candidatePath) {
|
|
259
|
+
const rel = relative(baseDir, candidatePath)
|
|
260
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
|
|
87
261
|
}
|
|
88
262
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let
|
|
263
|
+
// Resolves a manifest key to its package path (for the "still shipped?" check)
|
|
264
|
+
// and its consumer path (the prune candidate). Returns null for keys that the
|
|
265
|
+
// sync layer does not own, and for paths that escape their channel root.
|
|
266
|
+
export function resolvePruneEntry(manifestKey, { pruneChannels, packageRoot }) {
|
|
267
|
+
let channel
|
|
268
|
+
let relativePath
|
|
269
|
+
|
|
270
|
+
if (manifestKey.startsWith('__opencode__/')) {
|
|
271
|
+
channel = pruneChannels.opencode
|
|
272
|
+
relativePath = manifestKey.slice(channel.prefix.length)
|
|
273
|
+
// Never touch paths the .opencode copy pass deliberately ignores (e.g. node_modules).
|
|
274
|
+
if (
|
|
275
|
+
openCodeExcluded.has(relativePath) ||
|
|
276
|
+
openCodeExcluded.has(relativePath.split('/')[0])
|
|
277
|
+
) {
|
|
278
|
+
return null
|
|
279
|
+
}
|
|
280
|
+
} else if (manifestKey.startsWith('__scripts__/')) {
|
|
281
|
+
channel = pruneChannels.scripts
|
|
282
|
+
relativePath = manifestKey.slice(channel.prefix.length)
|
|
283
|
+
} else if (manifestKey.startsWith('__root__/')) {
|
|
284
|
+
channel = pruneChannels.root
|
|
285
|
+
relativePath = manifestKey.slice(channel.prefix.length)
|
|
286
|
+
} else if (manifestKey.startsWith('__')) {
|
|
287
|
+
// Unknown internal namespace — never prune (scaffold-only or future keys).
|
|
288
|
+
return null
|
|
289
|
+
} else {
|
|
290
|
+
channel = pruneChannels.agents
|
|
291
|
+
relativePath = manifestKey
|
|
292
|
+
if (excludedRelativePaths.has(relativePath)) {
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
}
|
|
94
296
|
|
|
95
|
-
|
|
297
|
+
if (!relativePath) {
|
|
298
|
+
return null
|
|
299
|
+
}
|
|
96
300
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
301
|
+
const packagePaths = [resolve(channel.packageDir, relativePath)]
|
|
302
|
+
if (channel === pruneChannels.root && relativePath === 'opencode.json') {
|
|
303
|
+
// opencode.json is scaffolded from the example when the package does not ship
|
|
304
|
+
// it directly (auto-copy pass above). Either source keeps the file alive.
|
|
305
|
+
packagePaths.push(resolve(packageRoot, 'opencode.mcp.example.json'))
|
|
101
306
|
}
|
|
102
307
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!existsSync(targetFile)) {
|
|
110
|
-
if (isVerbose) console.error(` … syncing: .agents/${relativePath}`)
|
|
111
|
-
syncFile(sourceFile, targetFile)
|
|
112
|
-
manifest.files[trackedKey] = sourceHash
|
|
113
|
-
copied += 1
|
|
114
|
-
added += 1
|
|
115
|
-
continue
|
|
308
|
+
const consumerPath = resolve(channel.consumerDir, relativePath)
|
|
309
|
+
if (!isPathInside(channel.packageDir, packagePaths[0])) {
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
if (!isPathInside(channel.consumerDir, consumerPath)) {
|
|
313
|
+
return null
|
|
116
314
|
}
|
|
117
315
|
|
|
118
|
-
|
|
316
|
+
return { packagePaths, consumerPath }
|
|
317
|
+
}
|
|
119
318
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
continue
|
|
319
|
+
export function isRegularFile(filePath) {
|
|
320
|
+
try {
|
|
321
|
+
return statSync(filePath).isFile()
|
|
322
|
+
} catch {
|
|
323
|
+
return false
|
|
126
324
|
}
|
|
127
|
-
|
|
128
|
-
skipped += 1
|
|
129
|
-
console.warn(`[ai-workflow-template] Skipped locally modified file: ${relativePath}`)
|
|
130
325
|
}
|
|
131
326
|
|
|
132
|
-
|
|
327
|
+
export function syncProject(context) {
|
|
328
|
+
const {
|
|
329
|
+
packageRoot,
|
|
330
|
+
consumerRoot,
|
|
331
|
+
sourceAgentsDir,
|
|
332
|
+
targetAgentsDir,
|
|
333
|
+
sourceOpenCodeDir,
|
|
334
|
+
sourceScriptsDir,
|
|
335
|
+
targetScriptsDir,
|
|
336
|
+
manifestPath,
|
|
337
|
+
forceOverwrite,
|
|
338
|
+
dryRun,
|
|
339
|
+
prune,
|
|
340
|
+
isVerbose,
|
|
341
|
+
} = context
|
|
342
|
+
|
|
343
|
+
if (!existsSync(sourceAgentsDir)) {
|
|
344
|
+
console.warn('[ai-workflow-template] No .agents directory found to sync.')
|
|
345
|
+
return { exitCode: 0, copied: 0, added: 0, scaffolded: 0, skipped: 0 }
|
|
346
|
+
}
|
|
133
347
|
|
|
134
|
-
const
|
|
135
|
-
|
|
348
|
+
const manifest = readManifest(manifestPath)
|
|
349
|
+
// Snapshot before syncing: prune only considers entries that existed at the start
|
|
350
|
+
// of this run, so files scaffolded/copied now are never candidates in the same run.
|
|
351
|
+
const preexistingManifestKeys = Object.keys(manifest.files)
|
|
352
|
+
const sourceFiles = collectFiles(sourceAgentsDir)
|
|
353
|
+
let copied = 0
|
|
354
|
+
let skipped = 0
|
|
355
|
+
let added = 0
|
|
136
356
|
|
|
137
|
-
|
|
138
|
-
const openCodeFiles = collectFiles(sourceOpenCodeDir)
|
|
357
|
+
/* ── Sync .agents/ directory ────────────────────────────────── */
|
|
139
358
|
|
|
140
|
-
for (const sourceFile of
|
|
141
|
-
const relativePath = relative(
|
|
142
|
-
if (
|
|
359
|
+
for (const sourceFile of sourceFiles) {
|
|
360
|
+
const relativePath = relative(sourceAgentsDir, sourceFile)
|
|
361
|
+
if (excludedRelativePaths.has(relativePath)) {
|
|
143
362
|
continue
|
|
144
363
|
}
|
|
145
364
|
|
|
146
|
-
const targetFile = resolve(
|
|
365
|
+
const targetFile = resolve(targetAgentsDir, relativePath)
|
|
147
366
|
const sourceHash = hashFile(sourceFile)
|
|
148
367
|
const existingHash = existsSync(targetFile) ? hashFile(targetFile) : null
|
|
149
|
-
const trackedKey =
|
|
368
|
+
const trackedKey = relativePath
|
|
150
369
|
const recordedHash = manifest.files[trackedKey] ?? null
|
|
151
370
|
|
|
152
371
|
if (!existsSync(targetFile)) {
|
|
153
|
-
if (isVerbose) console.error(` … syncing: .
|
|
154
|
-
syncFile(sourceFile, targetFile)
|
|
372
|
+
if (isVerbose) console.error(` … syncing: .agents/${relativePath}`)
|
|
373
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
155
374
|
manifest.files[trackedKey] = sourceHash
|
|
156
375
|
copied += 1
|
|
157
376
|
added += 1
|
|
158
377
|
continue
|
|
159
378
|
}
|
|
160
379
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (isVerbose) console.error(` … syncing: .opencode/${relativePath}`)
|
|
165
|
-
syncFile(sourceFile, targetFile)
|
|
380
|
+
if (forceOverwrite || isUntouched(recordedHash, existingHash)) {
|
|
381
|
+
if (isVerbose) console.error(` … syncing: .agents/${relativePath}`)
|
|
382
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
166
383
|
manifest.files[trackedKey] = sourceHash
|
|
167
384
|
copied += 1
|
|
168
385
|
continue
|
|
169
386
|
}
|
|
170
387
|
|
|
171
388
|
skipped += 1
|
|
172
|
-
console.warn(`[ai-workflow-template] Skipped locally modified
|
|
389
|
+
console.warn(`[ai-workflow-template] Skipped locally modified file: ${relativePath}`)
|
|
173
390
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/* ── Sync scripts/ directory ──────────────────────────────── */
|
|
177
|
-
|
|
178
|
-
const scriptsToSync = [
|
|
179
|
-
'memory-cli.js',
|
|
180
|
-
'memory-index.js',
|
|
181
|
-
'bump-version.js',
|
|
182
|
-
'validate-memory-schema.js',
|
|
183
|
-
'vocab-sync.js',
|
|
184
|
-
'mcp-memory-server.js',
|
|
185
|
-
'mcp/playwright-mcp-launcher.js',
|
|
186
|
-
// Knowledgebase scripts
|
|
187
|
-
'knowledgebase-cli.js',
|
|
188
|
-
'knowledgebase-index.js',
|
|
189
|
-
'mcp-knowledgebase-server.js',
|
|
190
|
-
'knowledgebase-init.sql',
|
|
191
|
-
]
|
|
192
|
-
|
|
193
|
-
const sourceScriptsDir = resolve(packageRoot, 'scripts')
|
|
194
|
-
const targetScriptsDir = resolve(consumerRoot, 'scripts')
|
|
195
|
-
let scriptsCopied = 0
|
|
196
|
-
let scriptsSkipped = 0
|
|
197
|
-
|
|
198
|
-
for (const scriptRelPath of scriptsToSync) {
|
|
199
|
-
const sourceFile = resolve(sourceScriptsDir, scriptRelPath)
|
|
200
|
-
const targetFile = resolve(targetScriptsDir, scriptRelPath)
|
|
201
391
|
|
|
202
|
-
|
|
203
|
-
|
|
392
|
+
/* ── Sync .opencode/ directory ────────────────────────────── */
|
|
393
|
+
|
|
394
|
+
if (existsSync(sourceOpenCodeDir)) {
|
|
395
|
+
const openCodeFiles = collectFiles(sourceOpenCodeDir)
|
|
396
|
+
|
|
397
|
+
for (const sourceFile of openCodeFiles) {
|
|
398
|
+
const relativePath = relative(sourceOpenCodeDir, sourceFile)
|
|
399
|
+
if (openCodeExcluded.has(relativePath) || openCodeExcluded.has(relativePath.split('/')[0])) {
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const targetFile = resolve(consumerRoot, '.opencode', relativePath)
|
|
404
|
+
const sourceHash = hashFile(sourceFile)
|
|
405
|
+
const existingHash = existsSync(targetFile) ? hashFile(targetFile) : null
|
|
406
|
+
const trackedKey = `__opencode__/${relativePath}`
|
|
407
|
+
const recordedHash = manifest.files[trackedKey] ?? null
|
|
408
|
+
|
|
409
|
+
if (!existsSync(targetFile)) {
|
|
410
|
+
if (isVerbose) console.error(` … syncing: .opencode/${relativePath}`)
|
|
411
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
412
|
+
manifest.files[trackedKey] = sourceHash
|
|
413
|
+
copied += 1
|
|
414
|
+
added += 1
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (forceOverwrite || isUntouched(recordedHash, existingHash)) {
|
|
419
|
+
if (isVerbose) console.error(` … syncing: .opencode/${relativePath}`)
|
|
420
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
421
|
+
manifest.files[trackedKey] = sourceHash
|
|
422
|
+
copied += 1
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
skipped += 1
|
|
427
|
+
console.warn(`[ai-workflow-template] Skipped locally modified opencode file: ${relativePath}`)
|
|
428
|
+
}
|
|
204
429
|
}
|
|
205
430
|
|
|
206
|
-
|
|
207
|
-
const existingHash = existsSync(targetFile) ? hashFile(targetFile) : null
|
|
208
|
-
const trackedKey = `__scripts__/${scriptRelPath}`
|
|
209
|
-
const recordedHash = manifest.files[trackedKey] ?? null
|
|
210
|
-
|
|
211
|
-
if (!existsSync(targetFile)) {
|
|
212
|
-
if (isVerbose) console.error(` … syncing: scripts/${scriptRelPath}`)
|
|
213
|
-
syncFile(sourceFile, targetFile)
|
|
214
|
-
manifest.files[trackedKey] = sourceHash
|
|
215
|
-
scriptsCopied += 1
|
|
216
|
-
copied += 1
|
|
217
|
-
added += 1
|
|
218
|
-
continue
|
|
219
|
-
}
|
|
431
|
+
/* ── Sync scripts/ directory ──────────────────────────────── */
|
|
220
432
|
|
|
221
|
-
|
|
433
|
+
let scriptsCopied = 0
|
|
434
|
+
let scriptsSkipped = 0
|
|
222
435
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
manifest.files[trackedKey] = sourceHash
|
|
227
|
-
scriptsCopied += 1
|
|
228
|
-
copied += 1
|
|
229
|
-
continue
|
|
230
|
-
}
|
|
436
|
+
for (const scriptRelPath of scriptsToSync) {
|
|
437
|
+
const sourceFile = resolve(sourceScriptsDir, scriptRelPath)
|
|
438
|
+
const targetFile = resolve(targetScriptsDir, scriptRelPath)
|
|
231
439
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
440
|
+
if (!existsSync(sourceFile)) {
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
236
443
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
444
|
+
const sourceHash = hashFile(sourceFile)
|
|
445
|
+
const existingHash = existsSync(targetFile) ? hashFile(targetFile) : null
|
|
446
|
+
const trackedKey = `__scripts__/${scriptRelPath}`
|
|
447
|
+
const recordedHash = manifest.files[trackedKey] ?? null
|
|
242
448
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
manifest.files[trackedKey] = sourceHash
|
|
253
|
-
copied += 1
|
|
254
|
-
added += 1
|
|
255
|
-
continue
|
|
256
|
-
}
|
|
449
|
+
if (!existsSync(targetFile)) {
|
|
450
|
+
if (isVerbose) console.error(` … syncing: scripts/${scriptRelPath}`)
|
|
451
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
452
|
+
manifest.files[trackedKey] = sourceHash
|
|
453
|
+
scriptsCopied += 1
|
|
454
|
+
copied += 1
|
|
455
|
+
added += 1
|
|
456
|
+
continue
|
|
457
|
+
}
|
|
257
458
|
|
|
258
|
-
|
|
459
|
+
if (forceOverwrite || isUntouched(recordedHash, existingHash)) {
|
|
460
|
+
if (isVerbose) console.error(` … syncing: scripts/${scriptRelPath}`)
|
|
461
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
462
|
+
manifest.files[trackedKey] = sourceHash
|
|
463
|
+
scriptsCopied += 1
|
|
464
|
+
copied += 1
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
259
467
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
manifest.files[trackedKey] = sourceHash
|
|
264
|
-
copied += 1
|
|
265
|
-
continue
|
|
468
|
+
scriptsSkipped += 1
|
|
469
|
+
skipped += 1
|
|
470
|
+
console.warn(`[ai-workflow-template] Skipped locally modified script file: scripts/${scriptRelPath}`)
|
|
266
471
|
}
|
|
267
472
|
|
|
268
|
-
|
|
269
|
-
console.warn(`[ai-workflow-template] Skipped locally modified root file: ${rootFile}`)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/* ── Scaffold memory-bank/ directory ──────────────────────────── */
|
|
473
|
+
/* ── Sync root files ────────────────────────────────────────── */
|
|
273
474
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
- **Purpose**: {{describe project purpose}}
|
|
280
|
-
- **Goals**: {{list project goals}}
|
|
281
|
-
- **Scope**: {{define scope}}
|
|
282
|
-
`,
|
|
283
|
-
'productContext.md': `# productContext
|
|
475
|
+
for (const rootFile of rootFiles) {
|
|
476
|
+
const sourceFile = resolve(packageRoot, rootFile)
|
|
477
|
+
if (!existsSync(sourceFile)) {
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
284
480
|
|
|
285
|
-
|
|
481
|
+
const targetFile = resolve(consumerRoot, rootFile)
|
|
482
|
+
const sourceHash = hashFile(sourceFile)
|
|
483
|
+
const existingHash = existsSync(targetFile) ? hashFile(targetFile) : null
|
|
484
|
+
const trackedKey = `__root__/${rootFile}`
|
|
485
|
+
const recordedHash = manifest.files[trackedKey] ?? null
|
|
286
486
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
487
|
+
if (!existsSync(targetFile)) {
|
|
488
|
+
if (isVerbose) console.error(` … syncing: ${rootFile}`)
|
|
489
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
490
|
+
manifest.files[trackedKey] = sourceHash
|
|
491
|
+
copied += 1
|
|
492
|
+
added += 1
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
292
495
|
|
|
293
|
-
|
|
496
|
+
if (forceOverwrite || isUntouched(recordedHash, existingHash)) {
|
|
497
|
+
if (isVerbose) console.error(` … syncing: ${rootFile}`)
|
|
498
|
+
syncFile(sourceFile, targetFile, dryRun)
|
|
499
|
+
manifest.files[trackedKey] = sourceHash
|
|
500
|
+
copied += 1
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
294
503
|
|
|
295
|
-
|
|
296
|
-
-
|
|
297
|
-
|
|
298
|
-
`,
|
|
299
|
-
'systemPatterns.md': `# systemPatterns
|
|
504
|
+
skipped += 1
|
|
505
|
+
console.warn(`[ai-workflow-template] Skipped locally modified root file: ${rootFile}`)
|
|
506
|
+
}
|
|
300
507
|
|
|
301
|
-
|
|
508
|
+
/* ── Scaffold memory-bank/ directory ──────────────────────────── */
|
|
302
509
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
- **Patterns**: {{list design patterns in use}}
|
|
306
|
-
`,
|
|
307
|
-
'techContext.md': `# techContext
|
|
510
|
+
const memoryBankDir = resolve(consumerRoot, 'memory-bank')
|
|
511
|
+
let scaffolded = 0
|
|
308
512
|
|
|
309
|
-
|
|
513
|
+
for (const [fileName, fileContent] of Object.entries(memoryBankStubs)) {
|
|
514
|
+
const targetFile = resolve(memoryBankDir, fileName)
|
|
515
|
+
const trackedKey = `__memory-bank__/${fileName}`
|
|
310
516
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
`,
|
|
315
|
-
'progress.md': `# progress
|
|
517
|
+
if (existsSync(targetFile)) {
|
|
518
|
+
continue
|
|
519
|
+
}
|
|
316
520
|
|
|
317
|
-
|
|
521
|
+
ensureParentDirectory(targetFile)
|
|
522
|
+
if (isVerbose) console.error(` … scaffolding: memory-bank/${fileName}`)
|
|
523
|
+
writeFileSync(targetFile, fileContent, 'utf8')
|
|
524
|
+
manifest.files[trackedKey] = hashFile(targetFile)
|
|
525
|
+
scaffolded += 1
|
|
526
|
+
}
|
|
318
527
|
|
|
319
|
-
|
|
320
|
-
- **To Build**: {{what remains}}
|
|
321
|
-
- **Status**: {{overall project status}}
|
|
322
|
-
- **Known Issues**: {{list known issues}}
|
|
323
|
-
`,
|
|
324
|
-
}
|
|
528
|
+
/* ── Scaffold .agents/instructions/ directory ───────────────── */
|
|
325
529
|
|
|
326
|
-
const
|
|
327
|
-
|
|
530
|
+
for (const [fileName, fileContent] of Object.entries(agentInstructionsStubs)) {
|
|
531
|
+
const targetFile = resolve(targetAgentsDir, 'instructions', fileName)
|
|
532
|
+
const trackedKey = `__agents-instructions__/${fileName}`
|
|
328
533
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
534
|
+
if (existsSync(targetFile)) {
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
332
537
|
|
|
333
|
-
|
|
334
|
-
|
|
538
|
+
ensureParentDirectory(targetFile)
|
|
539
|
+
if (isVerbose) console.error(` … scaffolding: .agents/instructions/${fileName}`)
|
|
540
|
+
writeFileSync(targetFile, fileContent, 'utf8')
|
|
541
|
+
manifest.files[trackedKey] = hashFile(targetFile)
|
|
542
|
+
scaffolded += 1
|
|
335
543
|
}
|
|
336
544
|
|
|
337
|
-
|
|
338
|
-
if (isVerbose) console.error(` … scaffolding: memory-bank/${fileName}`)
|
|
339
|
-
writeFileSync(targetFile, fileContent, 'utf8')
|
|
340
|
-
manifest.files[trackedKey] = hashFile(targetFile)
|
|
341
|
-
scaffolded += 1
|
|
342
|
-
}
|
|
545
|
+
/* ── Auto-copy opencode.mcp.example.json → opencode.json ────── */
|
|
343
546
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
'learned-knowledge.instructions.md': `# Learned Knowledge
|
|
547
|
+
const exampleMcpFile = resolve(packageRoot, 'opencode.mcp.example.json')
|
|
548
|
+
const targetMcpFile = resolve(consumerRoot, 'opencode.json')
|
|
549
|
+
const mcpTrackedKey = '__root__/opencode.json'
|
|
348
550
|
|
|
349
|
-
|
|
551
|
+
if (existsSync(exampleMcpFile) && !existsSync(targetMcpFile)) {
|
|
552
|
+
if (isVerbose) console.error(' … scaffolding: opencode.json from example')
|
|
553
|
+
copyFileSync(exampleMcpFile, targetMcpFile)
|
|
554
|
+
manifest.files[mcpTrackedKey] = hashFile(targetMcpFile)
|
|
555
|
+
scaffolded += 1
|
|
556
|
+
}
|
|
350
557
|
|
|
351
|
-
|
|
558
|
+
/* ── Prune stale files (opt-in via --prune) ─────────────────── */
|
|
559
|
+
|
|
560
|
+
// Every prunable manifest namespace maps onto the same package/consumer roots
|
|
561
|
+
// used by the copy passes above. Scaffold-only namespaces (__memory-bank__,
|
|
562
|
+
// __agents-instructions__) have no package source and are therefore never
|
|
563
|
+
// prunable — their keys fall through to the "unknown namespace" guard below.
|
|
564
|
+
let pruned = 0
|
|
565
|
+
let pruneSkipped = 0
|
|
566
|
+
|
|
567
|
+
if (prune) {
|
|
568
|
+
for (const manifestKey of preexistingManifestKeys) {
|
|
569
|
+
const entry = resolvePruneEntry(manifestKey, context)
|
|
570
|
+
if (!entry) {
|
|
571
|
+
continue
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Still shipped by the package? Package-owned — never prunable (CON-05, D-10).
|
|
575
|
+
if (entry.packagePaths.some((packagePath) => existsSync(packagePath))) {
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const displayPath = relative(consumerRoot, entry.consumerPath)
|
|
580
|
+
|
|
581
|
+
// Consumer file is already gone: drop the stale bookkeeping entry, delete nothing.
|
|
582
|
+
if (!existsSync(entry.consumerPath)) {
|
|
583
|
+
if (!dryRun) {
|
|
584
|
+
delete manifest.files[manifestKey]
|
|
585
|
+
}
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Files only — never delete a directory, symlink target, or unreadable path.
|
|
590
|
+
if (!isRegularFile(entry.consumerPath)) {
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const recordedHash = manifest.files[manifestKey]
|
|
595
|
+
if (typeof recordedHash !== 'string' || recordedHash.length === 0) {
|
|
596
|
+
pruneSkipped += 1
|
|
597
|
+
console.warn(`[ai-workflow-template] Skipped pruning untracked file: ${displayPath}`)
|
|
598
|
+
continue
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Locally modified (or unverifiable): keep the file AND its manifest entry so
|
|
602
|
+
// a later run can still see it. Deleting here would destroy user work.
|
|
603
|
+
if (hashFile(entry.consumerPath) !== recordedHash) {
|
|
604
|
+
pruneSkipped += 1
|
|
605
|
+
console.warn(`[ai-workflow-template] Skipped pruning locally modified file: ${displayPath}`)
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
pruned += 1
|
|
610
|
+
if (dryRun) {
|
|
611
|
+
console.log(`[ai-workflow-template] Would prune: ${displayPath}`)
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
rmSync(entry.consumerPath, { force: true })
|
|
616
|
+
delete manifest.files[manifestKey]
|
|
617
|
+
|
|
618
|
+
// Directories are never deleted. Report an emptied one and leave it in place.
|
|
619
|
+
const parentDir = dirname(entry.consumerPath)
|
|
620
|
+
if (existsSync(parentDir) && readdirSync(parentDir).length === 0) {
|
|
621
|
+
console.log(
|
|
622
|
+
`[ai-workflow-template] Left empty directory in place: ${relative(consumerRoot, parentDir)}`,
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
352
626
|
|
|
353
|
-
|
|
354
|
-
|
|
627
|
+
const summaryVerb = dryRun ? 'Would prune' : 'Pruned'
|
|
628
|
+
console.log(
|
|
629
|
+
`[ai-workflow-template] ${summaryVerb} ${pruned} stale file(s); skipped ${pruneSkipped} locally modified.`,
|
|
630
|
+
)
|
|
631
|
+
}
|
|
355
632
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
633
|
+
if (!dryRun) {
|
|
634
|
+
writeManifest(manifestPath, manifest)
|
|
635
|
+
}
|
|
359
636
|
|
|
360
|
-
|
|
361
|
-
|
|
637
|
+
const modeLabel = forceOverwrite ? 'force' : dryRun ? 'dry-run' : 'safe'
|
|
638
|
+
console.log(`[ai-workflow-template] Synced ${copied} files (${added} new), scaffolded ${scaffolded} files, skipped ${skipped} modified files. Mode: ${modeLabel}.`)
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
exitCode: 0,
|
|
642
|
+
copied,
|
|
643
|
+
added,
|
|
644
|
+
skipped,
|
|
645
|
+
scaffolded,
|
|
646
|
+
pruned,
|
|
647
|
+
pruneSkipped,
|
|
648
|
+
scriptsCopied,
|
|
649
|
+
scriptsSkipped,
|
|
362
650
|
}
|
|
651
|
+
}
|
|
363
652
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
scaffolded += 1
|
|
653
|
+
// Runs a complete sync pass. Accepts explicit roots/argv/env so the module can
|
|
654
|
+
// be exercised in-process by tests without touching the real repository.
|
|
655
|
+
export function runSync(options = {}) {
|
|
656
|
+
return syncProject(createSyncContext(options))
|
|
369
657
|
}
|
|
370
658
|
|
|
371
|
-
|
|
659
|
+
// True only when this module is the script Node was asked to execute. This keeps
|
|
660
|
+
// `node scripts/sync.js` working while making the module safe to import in tests.
|
|
661
|
+
// Compares symlink-resolved paths because macOS reports /var scripts as
|
|
662
|
+
// /private/var through import.meta.url.
|
|
663
|
+
export function isEntryPoint(moduleUrl = import.meta.url, argv1 = process.argv[1]) {
|
|
664
|
+
if (!argv1) {
|
|
665
|
+
return false
|
|
666
|
+
}
|
|
372
667
|
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
668
|
+
const modulePath = fileURLToPath(moduleUrl)
|
|
669
|
+
const argvPath = resolve(argv1)
|
|
670
|
+
if (modulePath === argvPath) {
|
|
671
|
+
return true
|
|
672
|
+
}
|
|
376
673
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
674
|
+
try {
|
|
675
|
+
return realpathSync(modulePath) === realpathSync(argvPath)
|
|
676
|
+
} catch {
|
|
677
|
+
return false
|
|
678
|
+
}
|
|
382
679
|
}
|
|
383
680
|
|
|
384
|
-
if (
|
|
385
|
-
|
|
681
|
+
if (isEntryPoint()) {
|
|
682
|
+
runSync()
|
|
386
683
|
}
|
|
387
|
-
|
|
388
|
-
const modeLabel = forceOverwrite ? 'force' : dryRun ? 'dry-run' : 'safe'
|
|
389
|
-
console.log(`[ai-workflow-template] Synced ${copied} files (${added} new), scaffolded ${scaffolded} files, skipped ${skipped} modified files. Mode: ${modeLabel}.`)
|