buildflow-dev 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/commands/install.js +236 -92
- package/src/commands/status.js +36 -1
- package/src/commands/update.js +20 -10
- package/src/utils/checkVersion.js +72 -0
- package/src/utils/welcome.js +24 -1
- package/templates/CLAUDE.md +14 -0
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -26,6 +26,9 @@ const TOOLS = {
|
|
|
26
26
|
return hasCli || hasClaudeDir
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
+
isInstalledLocal() { return existsSync(join(process.cwd(), '.claude', 'commands', 'buildflow-start.md')) },
|
|
30
|
+
isInstalledGlobal() { return existsSync(join(homedir(), '.claude', 'commands', 'buildflow-start.md')) },
|
|
31
|
+
|
|
29
32
|
installGlobal(commandFiles) {
|
|
30
33
|
const dir = join(homedir(), '.claude', 'commands')
|
|
31
34
|
mkdirSync(dir, { recursive: true })
|
|
@@ -56,17 +59,29 @@ const TOOLS = {
|
|
|
56
59
|
docsUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
57
60
|
|
|
58
61
|
detect() {
|
|
59
|
-
try { which.sync('gemini'); return true } catch { return false }
|
|
62
|
+
const hasCli = (() => { try { which.sync('gemini'); return true } catch { return false } })()
|
|
63
|
+
// Also check config dir — binary may not be on PATH in all shells
|
|
64
|
+
const hasGeminiDir = existsSync(join(homedir(), '.gemini'))
|
|
65
|
+
return hasCli || hasGeminiDir
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
isInstalledLocal() {
|
|
69
|
+
const cmdFile = existsSync(join(process.cwd(), '.gemini', 'commands', 'start.md'))
|
|
70
|
+
const ctxFile = existsSync(join(process.cwd(), 'GEMINI.md')) &&
|
|
71
|
+
readFileSafe(join(process.cwd(), 'GEMINI.md')).includes('BuildFlow')
|
|
72
|
+
return cmdFile || ctxFile
|
|
73
|
+
},
|
|
74
|
+
isInstalledGlobal() {
|
|
75
|
+
const cmdFile = existsSync(join(homedir(), '.gemini', 'commands', 'start.md'))
|
|
76
|
+
const ctxFile = existsSync(join(homedir(), '.gemini', 'GEMINI.md')) &&
|
|
77
|
+
readFileSafe(join(homedir(), '.gemini', 'GEMINI.md')).includes('BuildFlow')
|
|
78
|
+
return cmdFile || ctxFile
|
|
60
79
|
},
|
|
61
80
|
|
|
62
81
|
installGlobal(commandFiles) {
|
|
63
82
|
const dir = join(homedir(), '.gemini', 'commands')
|
|
64
83
|
mkdirSync(dir, { recursive: true })
|
|
65
|
-
|
|
66
|
-
const existingContent = existsSync(contextPath) ? readFileSync(contextPath, 'utf8') : ''
|
|
67
|
-
if (!existingContent.includes('## BuildFlow Commands')) {
|
|
68
|
-
writeFileSync(contextPath, existingContent + '\n\n' + geminiContextBlock(commandFiles))
|
|
69
|
-
}
|
|
84
|
+
patchGeminiContext(join(homedir(), '.gemini', 'GEMINI.md'), commandFiles)
|
|
70
85
|
for (const [name, content] of Object.entries(commandFiles)) {
|
|
71
86
|
writeFileSync(join(dir, `${name}.md`), content)
|
|
72
87
|
}
|
|
@@ -76,18 +91,14 @@ const TOOLS = {
|
|
|
76
91
|
installLocal(commandFiles) {
|
|
77
92
|
const dir = join(process.cwd(), '.gemini', 'commands')
|
|
78
93
|
mkdirSync(dir, { recursive: true })
|
|
79
|
-
|
|
80
|
-
const existingContent = existsSync(contextPath) ? readFileSync(contextPath, 'utf8') : ''
|
|
81
|
-
if (!existingContent.includes('## BuildFlow Commands')) {
|
|
82
|
-
writeFileSync(contextPath, existingContent + '\n\n' + geminiContextBlock(commandFiles))
|
|
83
|
-
}
|
|
94
|
+
patchGeminiContext(join(process.cwd(), 'GEMINI.md'), commandFiles)
|
|
84
95
|
for (const [name, content] of Object.entries(commandFiles)) {
|
|
85
96
|
writeFileSync(join(dir, `${name}.md`), content)
|
|
86
97
|
}
|
|
87
98
|
return dir
|
|
88
99
|
},
|
|
89
100
|
|
|
90
|
-
triggerNote: 'In Gemini CLI, type "/" or
|
|
101
|
+
triggerNote: 'In Gemini CLI, type "/buildflow-start" or ask Gemini to run a buildflow command',
|
|
91
102
|
},
|
|
92
103
|
|
|
93
104
|
codex: {
|
|
@@ -98,35 +109,40 @@ const TOOLS = {
|
|
|
98
109
|
docsUrl: 'https://github.com/openai/codex',
|
|
99
110
|
|
|
100
111
|
detect() {
|
|
101
|
-
try { which.sync('codex'); return true } catch { return false }
|
|
112
|
+
const hasCli = (() => { try { which.sync('codex'); return true } catch { return false } })()
|
|
113
|
+
const hasCodexDir = existsSync(join(homedir(), '.codex'))
|
|
114
|
+
return hasCli || hasCodexDir
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
isInstalledLocal() { return existsSync(join(process.cwd(), '.codex', 'instructions', 'buildflow-start.md')) },
|
|
118
|
+
isInstalledGlobal() { return existsSync(join(homedir(), '.codex', 'instructions', 'buildflow-start.md')) },
|
|
119
|
+
|
|
120
|
+
installGlobal(commandFiles) {
|
|
121
|
+
const dir = join(homedir(), '.codex', 'instructions')
|
|
122
|
+
const skillsDir = join(homedir(), '.codex', 'skills')
|
|
123
|
+
mkdirSync(dir, { recursive: true })
|
|
124
|
+
for (const [name, content] of Object.entries(commandFiles)) {
|
|
125
|
+
writeFileSync(join(dir, `buildflow-${name}.md`), content)
|
|
126
|
+
writeCodexSkill(skillsDir, name, content)
|
|
127
|
+
}
|
|
128
|
+
patchAgentsMd(join(homedir(), '.codex', 'AGENTS.md'), 'global')
|
|
129
|
+
return `${dir} + ${skillsDir}`
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
installLocal(commandFiles) {
|
|
133
|
+
const dir = join(process.cwd(), '.codex', 'instructions')
|
|
134
|
+
const skillsDir = join(process.cwd(), '.codex', 'skills')
|
|
135
|
+
mkdirSync(dir, { recursive: true })
|
|
136
|
+
for (const [name, content] of Object.entries(commandFiles)) {
|
|
137
|
+
writeFileSync(join(dir, `buildflow-${name}.md`), content)
|
|
138
|
+
writeCodexSkill(skillsDir, name, content)
|
|
139
|
+
}
|
|
140
|
+
patchAgentsMd(join(process.cwd(), 'AGENTS.md'), 'local')
|
|
141
|
+
return `${dir} + ${skillsDir}`
|
|
102
142
|
},
|
|
103
143
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const skillsDir = join(homedir(), '.codex', 'skills')
|
|
107
|
-
mkdirSync(dir, { recursive: true })
|
|
108
|
-
for (const [name, content] of Object.entries(commandFiles)) {
|
|
109
|
-
writeFileSync(join(dir, `buildflow-${name}.md`), content)
|
|
110
|
-
writeCodexSkill(skillsDir, name, content)
|
|
111
|
-
}
|
|
112
|
-
patchAgentsMd(join(homedir(), '.codex', 'AGENTS.md'), 'global')
|
|
113
|
-
return `${dir} + ${skillsDir}`
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
installLocal(commandFiles) {
|
|
117
|
-
const dir = join(process.cwd(), '.codex', 'instructions')
|
|
118
|
-
const skillsDir = join(process.cwd(), '.codex', 'skills')
|
|
119
|
-
mkdirSync(dir, { recursive: true })
|
|
120
|
-
for (const [name, content] of Object.entries(commandFiles)) {
|
|
121
|
-
writeFileSync(join(dir, `buildflow-${name}.md`), content)
|
|
122
|
-
writeCodexSkill(skillsDir, name, content)
|
|
123
|
-
}
|
|
124
|
-
patchAgentsMd(join(process.cwd(), 'AGENTS.md'), 'local')
|
|
125
|
-
return `${dir} + ${skillsDir}`
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
triggerNote: 'In Codex CLI, use $buildflow-start or say "use buildflow-start". Slash menu commands are not exposed by Codex.',
|
|
129
|
-
},
|
|
144
|
+
triggerNote: 'In Codex CLI, use $buildflow-start or say "use buildflow-start". Slash menu commands are not exposed by Codex.',
|
|
145
|
+
},
|
|
130
146
|
|
|
131
147
|
cursor: {
|
|
132
148
|
id: 'cursor',
|
|
@@ -143,6 +159,9 @@ const TOOLS = {
|
|
|
143
159
|
return hasCursorApp || hasCursorDir
|
|
144
160
|
},
|
|
145
161
|
|
|
162
|
+
isInstalledLocal() { return existsSync(join(process.cwd(), '.cursor', 'rules', 'buildflow.mdc')) },
|
|
163
|
+
isInstalledGlobal() { return this.isInstalledLocal() },
|
|
164
|
+
|
|
146
165
|
installGlobal(commandFiles) {
|
|
147
166
|
return this.installLocal(commandFiles)
|
|
148
167
|
},
|
|
@@ -175,6 +194,11 @@ const TOOLS = {
|
|
|
175
194
|
)
|
|
176
195
|
},
|
|
177
196
|
|
|
197
|
+
isInstalledLocal() {
|
|
198
|
+
return readFileSafe(join(process.cwd(), '.clinerules')).includes('BuildFlow')
|
|
199
|
+
},
|
|
200
|
+
isInstalledGlobal() { return this.isInstalledLocal() },
|
|
201
|
+
|
|
178
202
|
installGlobal(commandFiles) {
|
|
179
203
|
return this.installLocal(commandFiles)
|
|
180
204
|
},
|
|
@@ -198,6 +222,9 @@ const TOOLS = {
|
|
|
198
222
|
return existsSync(join(homedir(), '.continue', 'config.json'))
|
|
199
223
|
},
|
|
200
224
|
|
|
225
|
+
isInstalledLocal() { return existsSync(join(process.cwd(), '.continue', 'buildflow', 'start.md')) },
|
|
226
|
+
isInstalledGlobal() { return existsSync(join(homedir(), '.continue', 'buildflow', 'start.md')) },
|
|
227
|
+
|
|
201
228
|
installGlobal(commandFiles) {
|
|
202
229
|
const dir = join(homedir(), '.continue', 'buildflow')
|
|
203
230
|
mkdirSync(dir, { recursive: true })
|
|
@@ -222,41 +249,64 @@ const TOOLS = {
|
|
|
222
249
|
|
|
223
250
|
}
|
|
224
251
|
|
|
252
|
+
const UPDATE_CHECK_INSTRUCTION = `
|
|
253
|
+
## BuildFlow Update Check (Run Every Session)
|
|
254
|
+
|
|
255
|
+
At the very start of every session, before anything else:
|
|
256
|
+
1. Check if \`.buildflow/core/UPDATE.md\` exists
|
|
257
|
+
2. If it does, show the user this notice:
|
|
258
|
+
> "BuildFlow update available — run \`npx buildflow-dev update\` in your terminal to upgrade."
|
|
259
|
+
Then show the contents of UPDATE.md so the user sees the version details.
|
|
260
|
+
3. If the file does not exist, proceed silently.
|
|
261
|
+
`
|
|
262
|
+
|
|
225
263
|
function geminiContextBlock(commandFiles) {
|
|
226
264
|
const commandList = Object.keys(commandFiles)
|
|
227
265
|
.map(name => `- \`/buildflow-${name}\`: see .gemini/commands/${name}.md`)
|
|
228
266
|
.join('\n')
|
|
229
|
-
return `## BuildFlow Commands
|
|
267
|
+
return `## BuildFlow Commands
|
|
268
|
+
|
|
269
|
+
When the user types a /buildflow-* command, load and execute the corresponding file from .gemini/commands/.
|
|
270
|
+
|
|
271
|
+
${commandList}
|
|
272
|
+
${UPDATE_CHECK_INSTRUCTION}`
|
|
230
273
|
}
|
|
231
274
|
|
|
232
|
-
function patchAgentsMd(filePath, scope) {
|
|
233
|
-
const existing =
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
275
|
+
function patchAgentsMd(filePath, scope) {
|
|
276
|
+
const existing = readFileSafe(filePath)
|
|
277
|
+
const dir = scope === 'global' ? '~/.codex/instructions/' : '.codex/instructions/'
|
|
278
|
+
const block = `## BuildFlow Instructions\n\nWhen the user types $buildflow-<command> or /buildflow-<command>, load the matching file from ${dir} and follow those instructions.\n\nAvailable commands: start, think, plan, build, check, ship, onboard, modify, refactor, audit, status, explain, back, help\n${UPDATE_CHECK_INSTRUCTION}`
|
|
279
|
+
if (existing.includes('## BuildFlow Instructions')) {
|
|
280
|
+
const updated = existing.replace(
|
|
281
|
+
/## BuildFlow Instructions[\s\S]*?(?=\n## |\n# |$)/,
|
|
282
|
+
block
|
|
283
|
+
)
|
|
284
|
+
writeFileSync(filePath, updated)
|
|
285
|
+
} else {
|
|
286
|
+
writeFileSync(filePath, existing + (existing ? '\n\n' : '') + block)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function writeCodexSkill(skillsDir, name, commandContent) {
|
|
291
|
+
const skillName = `buildflow-${name}`
|
|
292
|
+
const skillDir = join(skillsDir, skillName)
|
|
293
|
+
mkdirSync(skillDir, { recursive: true })
|
|
294
|
+
writeFileSync(join(skillDir, 'SKILL.md'), codexSkillContent(skillName, commandContent))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function codexSkillContent(skillName, commandContent) {
|
|
298
|
+
const description = extractFrontmatterValue(commandContent, 'description') || `Run ${skillName}`
|
|
299
|
+
return `---\nname: "${skillName}"\ndescription: "${escapeYamlString(description)}"\nmetadata:\n short-description: "${escapeYamlString(description)}"\n---\n\n<objective>\nExecute the BuildFlow workflow below end-to-end.\nTreat any user text after $${skillName} as arguments for this workflow.\n</objective>\n\n<workflow>\n${commandContent}\n</workflow>\n`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function extractFrontmatterValue(content, key) {
|
|
303
|
+
const match = content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))
|
|
304
|
+
return match?.[1]?.trim().replace(/^["']|["']$/g, '')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function escapeYamlString(value) {
|
|
308
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
309
|
+
}
|
|
260
310
|
|
|
261
311
|
function cursorRulesContent(commandFiles) {
|
|
262
312
|
const commandDescriptions = Object.keys(commandFiles)
|
|
@@ -271,7 +321,7 @@ alwaysApply: false
|
|
|
271
321
|
# BuildFlow v3.0
|
|
272
322
|
|
|
273
323
|
You are integrated with BuildFlow, an adaptive development orchestration system.
|
|
274
|
-
|
|
324
|
+
${UPDATE_CHECK_INSTRUCTION}
|
|
275
325
|
## Available Commands
|
|
276
326
|
|
|
277
327
|
When the user types @buildflow-<command> or references a buildflow command, execute the corresponding workflow:
|
|
@@ -308,7 +358,7 @@ function clineRulesContent(commandFiles) {
|
|
|
308
358
|
.map(name => `- /buildflow-${name}`)
|
|
309
359
|
.join('\n')
|
|
310
360
|
return `# BuildFlow v3.0 Rules for Cline
|
|
311
|
-
|
|
361
|
+
${UPDATE_CHECK_INSTRUCTION}
|
|
312
362
|
## Slash Commands
|
|
313
363
|
|
|
314
364
|
When the user types any of the following commands, load the corresponding instruction file from .buildflow/commands/:
|
|
@@ -371,6 +421,27 @@ function readdirSafe(dir) {
|
|
|
371
421
|
}
|
|
372
422
|
}
|
|
373
423
|
|
|
424
|
+
function readFileSafe(filePath) {
|
|
425
|
+
try { return readFileSync(filePath, 'utf8') } catch { return '' }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Writes or replaces the BuildFlow block in a GEMINI.md context file.
|
|
429
|
+
// Always overwrites the block so updates pick up new commands.
|
|
430
|
+
function patchGeminiContext(contextPath, commandFiles) {
|
|
431
|
+
const existing = readFileSafe(contextPath)
|
|
432
|
+
const block = geminiContextBlock(commandFiles)
|
|
433
|
+
if (existing.includes('## BuildFlow Commands')) {
|
|
434
|
+
// Replace old block — from the marker to the next top-level ## or end of file
|
|
435
|
+
const updated = existing.replace(
|
|
436
|
+
/## BuildFlow Commands[\s\S]*?(?=\n## |\n# |$)/,
|
|
437
|
+
block.trimStart()
|
|
438
|
+
)
|
|
439
|
+
writeFileSync(contextPath, updated)
|
|
440
|
+
} else {
|
|
441
|
+
writeFileSync(contextPath, existing + (existing ? '\n\n' : '') + block)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
374
445
|
function detectAppName() {
|
|
375
446
|
const pkgPath = join(process.cwd(), 'package.json')
|
|
376
447
|
if (existsSync(pkgPath)) {
|
|
@@ -384,6 +455,55 @@ function detectAppName() {
|
|
|
384
455
|
return process.cwd().split(/[/\\]/).pop() || 'my-project'
|
|
385
456
|
}
|
|
386
457
|
|
|
458
|
+
export async function refreshInstalledTools(opts = {}) {
|
|
459
|
+
const commandFiles = loadCommandTemplates()
|
|
460
|
+
const commandCount = Object.keys(commandFiles).length
|
|
461
|
+
const results = []
|
|
462
|
+
|
|
463
|
+
for (const tool of Object.values(TOOLS)) {
|
|
464
|
+
const local = tool.isInstalledLocal()
|
|
465
|
+
const global = tool.isInstalledGlobal()
|
|
466
|
+
if (!local && !global) continue
|
|
467
|
+
|
|
468
|
+
const sp = ora(` ${tool.icon} Refreshing ${tool.name}...`).start()
|
|
469
|
+
try {
|
|
470
|
+
if (local) tool.installLocal(commandFiles)
|
|
471
|
+
if (global && !local) tool.installGlobal(commandFiles)
|
|
472
|
+
sp.succeed(chalk.green(` ${tool.icon} ${tool.name}`) + chalk.dim(` — ${commandCount} commands refreshed`))
|
|
473
|
+
results.push({ tool, success: true })
|
|
474
|
+
} catch (err) {
|
|
475
|
+
sp.fail(chalk.red(` ${tool.icon} ${tool.name} — ${err.message}`))
|
|
476
|
+
results.push({ tool, success: false, error: err })
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (results.length === 0) {
|
|
481
|
+
console.log(chalk.yellow(' No previously installed tools found.'))
|
|
482
|
+
console.log(chalk.dim(' Run: buildflow install\n'))
|
|
483
|
+
} else {
|
|
484
|
+
const failed = results.filter(r => !r.success)
|
|
485
|
+
if (failed.length > 0) {
|
|
486
|
+
console.log(chalk.red('\n Some tools failed to update:'))
|
|
487
|
+
for (const { tool, error } of failed) {
|
|
488
|
+
console.log(chalk.red(` ✗ ${tool.name}: ${error.message}`))
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return results
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function getToolStatus() {
|
|
497
|
+
return Object.values(TOOLS).map(tool => ({
|
|
498
|
+
id: tool.id,
|
|
499
|
+
name: tool.name,
|
|
500
|
+
icon: tool.icon,
|
|
501
|
+
detected: tool.detect(),
|
|
502
|
+
installedLocal: tool.isInstalledLocal(),
|
|
503
|
+
installedGlobal: tool.isInstalledGlobal(),
|
|
504
|
+
}))
|
|
505
|
+
}
|
|
506
|
+
|
|
387
507
|
function loadCommandTemplates() {
|
|
388
508
|
const templatesDir = join(__dirname, '../../templates/commands')
|
|
389
509
|
const commands = {}
|
|
@@ -418,17 +538,30 @@ export async function run(opts = {}) {
|
|
|
418
538
|
const detectedList = Object.entries(detected).filter(([, found]) => found)
|
|
419
539
|
const notFoundList = Object.entries(detected).filter(([, found]) => !found)
|
|
420
540
|
|
|
541
|
+
// Tools detected on the system but BuildFlow not yet installed into them
|
|
542
|
+
const newlyDetected = detectedList.filter(([id]) => {
|
|
543
|
+
const t = TOOLS[id]
|
|
544
|
+
return !t.isInstalledLocal() && !t.isInstalledGlobal()
|
|
545
|
+
})
|
|
546
|
+
|
|
421
547
|
if (detectedList.length > 0) {
|
|
422
548
|
console.log(chalk.green(' ✓ Detected on your system:'))
|
|
423
549
|
for (const [id] of detectedList) {
|
|
424
550
|
const t = TOOLS[id]
|
|
425
|
-
|
|
551
|
+
const installed = t.isInstalledLocal() || t.isInstalledGlobal()
|
|
552
|
+
const badge = installed ? chalk.dim(' (already installed)') : chalk.yellow(' (not yet installed)')
|
|
553
|
+
console.log(chalk.green(` ${t.icon} ${t.name}`) + badge)
|
|
426
554
|
}
|
|
427
555
|
} else {
|
|
428
556
|
console.log(chalk.yellow(' ⚠ No AI tools detected automatically.'))
|
|
429
557
|
console.log(chalk.dim(' You can still install for tools not on this list.\n'))
|
|
430
558
|
}
|
|
431
559
|
|
|
560
|
+
if (newlyDetected.length > 0) {
|
|
561
|
+
console.log(chalk.yellow(`\n ${newlyDetected.length} tool(s) detected but BuildFlow not installed yet.`))
|
|
562
|
+
console.log(chalk.dim(' They are pre-selected below.\n'))
|
|
563
|
+
}
|
|
564
|
+
|
|
432
565
|
if (notFoundList.length > 0) {
|
|
433
566
|
console.log(chalk.dim('\n Not detected (can still install):'))
|
|
434
567
|
for (const [id] of notFoundList) {
|
|
@@ -453,11 +586,19 @@ export async function run(opts = {}) {
|
|
|
453
586
|
return
|
|
454
587
|
}
|
|
455
588
|
} else {
|
|
456
|
-
const choices = Object.entries(TOOLS).map(([id, tool]) =>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
589
|
+
const choices = Object.entries(TOOLS).map(([id, tool]) => {
|
|
590
|
+
const isInstalled = tool.isInstalledLocal() || tool.isInstalledGlobal()
|
|
591
|
+
const statusBadge = !detected[id]
|
|
592
|
+
? ''
|
|
593
|
+
: isInstalled
|
|
594
|
+
? chalk.dim(' (reinstall)')
|
|
595
|
+
: chalk.yellow(' ← new')
|
|
596
|
+
return {
|
|
597
|
+
name: id,
|
|
598
|
+
message: `${tool.icon} ${tool.name}${statusBadge}`,
|
|
599
|
+
hint: tool.description,
|
|
600
|
+
}
|
|
601
|
+
})
|
|
461
602
|
|
|
462
603
|
const { tools } = await prompt({
|
|
463
604
|
type: 'multiselect',
|
|
@@ -465,7 +606,10 @@ export async function run(opts = {}) {
|
|
|
465
606
|
message: 'Which AI tools do you want to install BuildFlow into?',
|
|
466
607
|
hint: '(Space to select, Enter to confirm)',
|
|
467
608
|
choices,
|
|
468
|
-
|
|
609
|
+
// Pre-select newly detected tools (detected but not yet installed)
|
|
610
|
+
initial: newlyDetected.length > 0
|
|
611
|
+
? newlyDetected.map(([id]) => id)
|
|
612
|
+
: detectedList.map(([id]) => id),
|
|
469
613
|
})
|
|
470
614
|
|
|
471
615
|
if (!tools || tools.length === 0) {
|
|
@@ -561,18 +705,18 @@ export async function run(opts = {}) {
|
|
|
561
705
|
}
|
|
562
706
|
}
|
|
563
707
|
|
|
564
|
-
if (succeeded.length > 0) {
|
|
565
|
-
console.log(chalk.bold('\n Next steps:\n'))
|
|
566
|
-
console.log(chalk.white(' 1. Open your AI tool in this project'))
|
|
567
|
-
if (succeeded.every(({ tool }) => tool.id === 'codex')) {
|
|
568
|
-
console.log(chalk.white(' 2. Restart Codex CLI so new skills are loaded'))
|
|
569
|
-
console.log(chalk.white(' 3. Start with: ') + chalk.cyan('$buildflow-start'))
|
|
570
|
-
console.log(chalk.white(' Or for existing projects: ') + chalk.cyan('$buildflow-onboard'))
|
|
571
|
-
} else {
|
|
572
|
-
console.log(chalk.white(' 2. Type "/" to see BuildFlow commands'))
|
|
573
|
-
console.log(chalk.white(' 3. Start with: ') + chalk.cyan('/buildflow-start'))
|
|
574
|
-
console.log(chalk.white(' Or for existing projects: ') + chalk.cyan('/buildflow-onboard'))
|
|
575
|
-
}
|
|
576
|
-
console.log('')
|
|
577
|
-
}
|
|
578
|
-
}
|
|
708
|
+
if (succeeded.length > 0) {
|
|
709
|
+
console.log(chalk.bold('\n Next steps:\n'))
|
|
710
|
+
console.log(chalk.white(' 1. Open your AI tool in this project'))
|
|
711
|
+
if (succeeded.every(({ tool }) => tool.id === 'codex')) {
|
|
712
|
+
console.log(chalk.white(' 2. Restart Codex CLI so new skills are loaded'))
|
|
713
|
+
console.log(chalk.white(' 3. Start with: ') + chalk.cyan('$buildflow-start'))
|
|
714
|
+
console.log(chalk.white(' Or for existing projects: ') + chalk.cyan('$buildflow-onboard'))
|
|
715
|
+
} else {
|
|
716
|
+
console.log(chalk.white(' 2. Type "/" to see BuildFlow commands'))
|
|
717
|
+
console.log(chalk.white(' 3. Start with: ') + chalk.cyan('/buildflow-start'))
|
|
718
|
+
console.log(chalk.white(' Or for existing projects: ') + chalk.cyan('/buildflow-onboard'))
|
|
719
|
+
}
|
|
720
|
+
console.log('')
|
|
721
|
+
}
|
|
722
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
3
3
|
import { join } from 'path'
|
|
4
|
+
import { getToolStatus } from './install.js'
|
|
4
5
|
|
|
5
6
|
export async function run(opts = {}) {
|
|
6
7
|
const base = join(process.cwd(), '.buildflow')
|
|
@@ -35,10 +36,44 @@ export async function run(opts = {}) {
|
|
|
35
36
|
onboarded === 'n/a' ? chalk.dim('N/A (greenfield)') :
|
|
36
37
|
chalk.yellow('No — run /buildflow-onboard')))
|
|
37
38
|
|
|
38
|
-
const hasDebt = debt.includes('## Active') && !debt.includes('
|
|
39
|
+
const hasDebt = debt.includes('## Active') && !debt.includes('*None')
|
|
39
40
|
console.log(chalk.dim(' Security debt: ') +
|
|
40
41
|
(hasDebt ? chalk.red('⚠ Issues pending — see .buildflow/security/DEBT.md') : chalk.green('Clean')))
|
|
41
42
|
|
|
43
|
+
// ── AI Tool Status ──────────────────────────────────────────────────────────
|
|
44
|
+
console.log(chalk.dim('\n AI Tools:\n'))
|
|
45
|
+
|
|
46
|
+
const tools = getToolStatus()
|
|
47
|
+
const uninstalled = []
|
|
48
|
+
|
|
49
|
+
for (const t of tools) {
|
|
50
|
+
if (!t.detected) {
|
|
51
|
+
console.log(chalk.dim(` ${t.icon} ${t.name}`) + chalk.dim(' — not installed on this machine'))
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const local = t.installedLocal
|
|
56
|
+
const global = t.installedGlobal
|
|
57
|
+
|
|
58
|
+
if (local || global) {
|
|
59
|
+
const scope = local && global ? 'local + global' : local ? 'local' : 'global'
|
|
60
|
+
console.log(chalk.green(` ${t.icon} ${t.name}`) + chalk.dim(` ✓ BuildFlow installed (${scope})`))
|
|
61
|
+
} else {
|
|
62
|
+
console.log(chalk.yellow(` ${t.icon} ${t.name}`) + chalk.yellow(' ⚠ Detected but BuildFlow not installed'))
|
|
63
|
+
uninstalled.push(t)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (uninstalled.length > 0) {
|
|
68
|
+
console.log('')
|
|
69
|
+
console.log(chalk.yellow(' Some tools are missing BuildFlow commands.'))
|
|
70
|
+
console.log(chalk.dim(' Run one of:'))
|
|
71
|
+
for (const t of uninstalled) {
|
|
72
|
+
console.log(chalk.white(` buildflow install --tool ${t.id}`))
|
|
73
|
+
}
|
|
74
|
+
console.log(chalk.dim(' Or: buildflow install (interactive)'))
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
if (opts.verbose) {
|
|
43
78
|
console.log(chalk.dim('\n .buildflow/ structure:'))
|
|
44
79
|
const walk = (dir, prefix = ' ') => {
|
package/src/commands/update.js
CHANGED
|
@@ -3,7 +3,8 @@ import ora from 'ora'
|
|
|
3
3
|
import { existsSync, readFileSync } from 'fs'
|
|
4
4
|
import { join, dirname } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
-
import {
|
|
6
|
+
import { refreshInstalledTools } from './install.js'
|
|
7
|
+
import { checkVersion, clearUpdateNotice } from '../utils/checkVersion.js'
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
10
|
|
|
@@ -15,21 +16,30 @@ export async function run(opts = {}) {
|
|
|
15
16
|
return
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'))
|
|
20
|
+
|
|
18
21
|
if (opts.check) {
|
|
19
22
|
console.log('\n' + chalk.bold.white(' BuildFlow Update Check\n'))
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const sp = ora('Checking npm registry...').start()
|
|
24
|
+
const update = await checkVersion(pkg.version)
|
|
25
|
+
sp.stop()
|
|
26
|
+
|
|
27
|
+
if (update) {
|
|
28
|
+
console.log(chalk.yellow(` Update available: ${update.current} → ${update.latest}`))
|
|
29
|
+
console.log(chalk.white(' Run: npx buildflow-dev update\n'))
|
|
30
|
+
} else {
|
|
31
|
+
console.log(chalk.green(` ✓ Up to date (v${pkg.version})\n`))
|
|
32
|
+
}
|
|
24
33
|
return
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
console.log('\n' + chalk.bold.white(' Updating BuildFlow...\n'))
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
await
|
|
31
|
-
|
|
38
|
+
// Re-push latest command templates to all previously installed tools
|
|
39
|
+
await refreshInstalledTools()
|
|
40
|
+
|
|
41
|
+
// Clear the update notice now that we've updated
|
|
42
|
+
clearUpdateNotice()
|
|
32
43
|
|
|
33
|
-
console.log(
|
|
34
|
-
await runInstall({ yes: opts.yes })
|
|
44
|
+
console.log(chalk.green(`\n ✓ BuildFlow updated to v${pkg.version}\n`))
|
|
35
45
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
|
|
4
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/buildflow-dev/latest'
|
|
5
|
+
const UPDATE_FILE = '.buildflow/core/UPDATE.md'
|
|
6
|
+
|
|
7
|
+
export async function checkVersion(currentVersion) {
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(REGISTRY_URL, {
|
|
10
|
+
signal: AbortSignal.timeout(3000),
|
|
11
|
+
headers: { Accept: 'application/json' },
|
|
12
|
+
})
|
|
13
|
+
if (!res.ok) return null
|
|
14
|
+
|
|
15
|
+
const { version: latest } = await res.json()
|
|
16
|
+
const updatePath = join(process.cwd(), UPDATE_FILE)
|
|
17
|
+
|
|
18
|
+
if (latest === currentVersion) {
|
|
19
|
+
// Remove stale notice if we're already up to date
|
|
20
|
+
if (existsSync(updatePath)) unlinkSync(updatePath)
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Only write the file if .buildflow/ exists (i.e. project is initialized)
|
|
25
|
+
const coreDir = join(process.cwd(), '.buildflow', 'core')
|
|
26
|
+
if (!existsSync(coreDir)) return { current: currentVersion, latest }
|
|
27
|
+
|
|
28
|
+
mkdirSync(coreDir, { recursive: true })
|
|
29
|
+
writeFileSync(updatePath, buildUpdateNotice(currentVersion, latest))
|
|
30
|
+
|
|
31
|
+
return { current: currentVersion, latest }
|
|
32
|
+
} catch {
|
|
33
|
+
return null // Network timeout or offline — silently skip
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearUpdateNotice() {
|
|
38
|
+
const updatePath = join(process.cwd(), UPDATE_FILE)
|
|
39
|
+
if (existsSync(updatePath)) unlinkSync(updatePath)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildUpdateNotice(current, latest) {
|
|
43
|
+
return `# BuildFlow Update Available
|
|
44
|
+
|
|
45
|
+
> A new version of BuildFlow is available.
|
|
46
|
+
> Your AI tool detected this file at session start.
|
|
47
|
+
|
|
48
|
+
| | Version |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Installed** | ${current} |
|
|
51
|
+
| **Latest** | ${latest} |
|
|
52
|
+
|
|
53
|
+
## How to Update
|
|
54
|
+
|
|
55
|
+
Run this in your terminal:
|
|
56
|
+
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
npx buildflow-dev update
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
Or if installed globally:
|
|
62
|
+
|
|
63
|
+
\`\`\`bash
|
|
64
|
+
npm install -g buildflow-dev@latest
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
After updating, this notice will disappear automatically.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
*Generated by BuildFlow CLI · ${new Date().toISOString().split('T')[0]}*
|
|
71
|
+
`
|
|
72
|
+
}
|
package/src/utils/welcome.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import { existsSync, readFileSync } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { checkVersion } from './checkVersion.js'
|
|
6
|
+
import { getToolStatus } from '../commands/install.js'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
4
9
|
|
|
5
10
|
export async function showWelcome() {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'))
|
|
12
|
+
// Fire-and-forget version check — writes UPDATE.md if behind, doesn't block output
|
|
13
|
+
checkVersion(pkg.version).then(update => {
|
|
14
|
+
if (update) {
|
|
15
|
+
console.log(chalk.yellow(`\n Update available: ${update.current} → ${update.latest}`))
|
|
16
|
+
console.log(chalk.dim(' Run: npx buildflow-dev update\n'))
|
|
17
|
+
}
|
|
18
|
+
}).catch(() => {})
|
|
6
19
|
const isInitialized = existsSync(join(process.cwd(), '.buildflow'))
|
|
7
20
|
|
|
8
21
|
console.log('\n' + chalk.bold.white(' BuildFlow v3.0'))
|
|
@@ -56,6 +69,16 @@ export async function showWelcome() {
|
|
|
56
69
|
console.log(chalk.white(' buildflow status ') + chalk.dim(' Show project status'))
|
|
57
70
|
console.log(chalk.white(' buildflow audit ') + chalk.dim(' Run security audit from terminal'))
|
|
58
71
|
console.log(chalk.white(' buildflow update ') + chalk.dim(' Update BuildFlow commands'))
|
|
72
|
+
|
|
73
|
+
// Surface any newly installed AI tools that don't have BuildFlow yet
|
|
74
|
+
const uninstalled = getToolStatus().filter(t => t.detected && !t.installedLocal && !t.installedGlobal)
|
|
75
|
+
if (uninstalled.length > 0) {
|
|
76
|
+
console.log('')
|
|
77
|
+
console.log(chalk.yellow(` New AI tool(s) detected — BuildFlow not installed yet:`))
|
|
78
|
+
for (const t of uninstalled) {
|
|
79
|
+
console.log(chalk.yellow(` ${t.icon} ${t.name}`) + chalk.dim(` → buildflow install --tool ${t.id}`))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
59
82
|
} else {
|
|
60
83
|
console.log(chalk.yellow(' Not initialized in this directory.\n'))
|
|
61
84
|
console.log(chalk.bold(' Get started:\n'))
|
package/templates/CLAUDE.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
This project uses **BuildFlow v3.0** for adaptive AI-powered development orchestration.
|
|
4
4
|
|
|
5
|
+
## Session Start Checklist (Run Every Time)
|
|
6
|
+
|
|
7
|
+
Before doing anything else at the start of every session:
|
|
8
|
+
|
|
9
|
+
1. **Check for updates** — if `.buildflow/core/UPDATE.md` exists, show the user this notice:
|
|
10
|
+
> "BuildFlow update available — run `npx buildflow-dev update` in your terminal."
|
|
11
|
+
Then display the contents of UPDATE.md so the user sees the version details.
|
|
12
|
+
If the file does not exist, proceed silently.
|
|
13
|
+
|
|
14
|
+
2. **Load memory** — read `.buildflow/memory/light.md` for project context
|
|
15
|
+
3. **Load state** — read `.buildflow/core/state.md` for current phase and status
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
5
19
|
## Quick Start
|
|
6
20
|
|
|
7
21
|
Type `/` in Claude Code to see available commands:
|