bmad-module-ultracode-goal 0.1.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/.claude-plugin/marketplace.json +20 -0
- package/.gitattributes +16 -0
- package/.nvmrc +1 -0
- package/LICENSE +27 -0
- package/README.md +200 -0
- package/docs/_internal/RELEASING.md +101 -0
- package/docs/_internal/STABILITY.md +62 -0
- package/docs/architecture.md +77 -0
- package/docs/assets/ucg-logo.svg +73 -0
- package/docs/gate-model.md +109 -0
- package/docs/getting-started.md +51 -0
- package/docs/health-check.md +61 -0
- package/docs/how-it-works.md +73 -0
- package/docs/index.md +25 -0
- package/docs/parallel-mode.md +33 -0
- package/docs/troubleshooting.md +56 -0
- package/docs/why-ultracode-goal.md +34 -0
- package/package.json +100 -0
- package/skills/.gitkeep +0 -0
- package/skills/module.yaml +12 -0
- package/skills/ultracode-goal/SKILL.md +75 -0
- package/skills/ultracode-goal/assets/execute-epic.workflow.js +208 -0
- package/skills/ultracode-goal/customize.toml +47 -0
- package/skills/ultracode-goal/references/define-done.md +41 -0
- package/skills/ultracode-goal/references/execute.md +90 -0
- package/skills/ultracode-goal/references/finalize.md +64 -0
- package/skills/ultracode-goal/references/gate.md +70 -0
- package/skills/ultracode-goal/references/health-check.md +332 -0
- package/skills/ultracode-goal/references/ingest-and-scope.md +46 -0
- package/skills/ultracode-goal/references/preflight.md +100 -0
- package/skills/ultracode-goal/scripts/gate_eval.py +280 -0
- package/skills/ultracode-goal/scripts/health_check_fp.py +196 -0
- package/skills/ultracode-goal/scripts/hooks/budget_stop.py +162 -0
- package/skills/ultracode-goal/scripts/hooks/guard_pretooluse.py +174 -0
- package/skills/ultracode-goal/scripts/preflight_check.py +399 -0
- package/tools/cli/commands/install.js +36 -0
- package/tools/cli/commands/status.js +161 -0
- package/tools/cli/commands/uninstall.js +175 -0
- package/tools/cli/commands/update.js +65 -0
- package/tools/cli/lib/ide-skills.js +218 -0
- package/tools/cli/lib/installer.js +226 -0
- package/tools/cli/lib/manifest.js +100 -0
- package/tools/cli/lib/platform-codes.yaml +223 -0
- package/tools/cli/lib/ui.js +353 -0
- package/tools/cli/lib/version-check.js +86 -0
- package/tools/cli/ucg-cli.js +45 -0
- package/tools/ucg-npx-wrapper.js +36 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCG Installer UI - Banner, prompts, and success message.
|
|
3
|
+
* Uses @clack/prompts for terminal UI.
|
|
4
|
+
*
|
|
5
|
+
* Brand palette:
|
|
6
|
+
* indigo #6366F1 — primary
|
|
7
|
+
* light #818CF8 — accent, highlights
|
|
8
|
+
* dark #4F46E5 — frame, deep emphasis
|
|
9
|
+
* spark #A5B4FC — icons
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { intro, outro, text, multiselect, confirm, note, isCancel, cancel, log, select } = require('@clack/prompts');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const figlet = require('figlet');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const fs = require('fs-extra');
|
|
17
|
+
const yaml = require('js-yaml');
|
|
18
|
+
const { readManifest } = require('./manifest');
|
|
19
|
+
const { compareVersions } = require('./version-check');
|
|
20
|
+
const { getAvailablePlatforms, getDetectionMarkers } = require('./ide-skills');
|
|
21
|
+
|
|
22
|
+
const UCG_FOLDER = '_bmad/ucg';
|
|
23
|
+
|
|
24
|
+
// Brand colors
|
|
25
|
+
const brand = {
|
|
26
|
+
indigo: chalk.hex('#6366F1'),
|
|
27
|
+
light: chalk.hex('#818CF8'),
|
|
28
|
+
dark: chalk.hex('#4F46E5'),
|
|
29
|
+
spark: chalk.hex('#A5B4FC'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class UI {
|
|
33
|
+
displayBanner() {
|
|
34
|
+
const packageJson = require('../../../package.json');
|
|
35
|
+
const version = packageJson.version;
|
|
36
|
+
|
|
37
|
+
let logoLines;
|
|
38
|
+
try {
|
|
39
|
+
logoLines = figlet.textSync('UCG', { font: 'ANSI Shadow' }).trimEnd().split('\n');
|
|
40
|
+
// Remove trailing empty lines from figlet output
|
|
41
|
+
while (logoLines.length > 0 && !logoLines.at(-1).trim()) logoLines.pop();
|
|
42
|
+
} catch {
|
|
43
|
+
logoLines = [' U C G'];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const w = 72;
|
|
47
|
+
const frame = brand.dark;
|
|
48
|
+
const top = frame(' ╔' + '═'.repeat(w) + '╗');
|
|
49
|
+
const mid = frame(' ╟' + '─'.repeat(w) + '╢');
|
|
50
|
+
const bottom = frame(' ╚' + '═'.repeat(w) + '╝');
|
|
51
|
+
const rule = frame(' ' + '━'.repeat(w));
|
|
52
|
+
const row = (content) => {
|
|
53
|
+
// eslint-disable-next-line no-control-regex -- stripping ANSI escape codes for visual width calculation
|
|
54
|
+
const stripped = content.replaceAll(/\u001B\[\d+(?:;\d+)*m/g, '');
|
|
55
|
+
const pad = Math.max(0, w - stripped.length - 2);
|
|
56
|
+
return frame(' ║ ') + content + ' '.repeat(pad) + frame(' ║');
|
|
57
|
+
};
|
|
58
|
+
const empty = row('');
|
|
59
|
+
const indent = ' ';
|
|
60
|
+
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(top);
|
|
63
|
+
console.log(empty);
|
|
64
|
+
for (const line of logoLines) {
|
|
65
|
+
console.log(row(indent + brand.indigo.bold(line.replace(/\s+$/, ''))));
|
|
66
|
+
}
|
|
67
|
+
console.log(empty);
|
|
68
|
+
console.log(mid);
|
|
69
|
+
console.log(row(indent + chalk.white.bold('UltraCode Goal') + ' ' + brand.spark('◎') + ' ' + chalk.dim('Autonomous Epic Conductor')));
|
|
70
|
+
console.log(row(indent + chalk.dim('Run a BMAD Epic autonomously to a machine-checked Definition-of-Done.')));
|
|
71
|
+
console.log(row(indent + chalk.dim(`v${version} · MIT License · Open Source`)));
|
|
72
|
+
console.log(bottom);
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(rule);
|
|
75
|
+
console.log();
|
|
76
|
+
const resource = (label, url) => ' ' + chalk.dim(label.padEnd(10)) + brand.indigo(url);
|
|
77
|
+
console.log(resource('Docs', 'https://github.com/armelhbobdad/bmad-module-ultracode-goal/tree/main/docs'));
|
|
78
|
+
console.log(resource('GitHub', 'https://github.com/armelhbobdad/bmad-module-ultracode-goal'));
|
|
79
|
+
console.log(resource('Support', 'https://ko-fi.com/armelhbobdad'));
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(rule);
|
|
82
|
+
console.log();
|
|
83
|
+
|
|
84
|
+
intro(brand.indigo('UltraCode Goal Installer'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async detectInstallation(projectDir) {
|
|
88
|
+
const hasBmadUcg = await fs.pathExists(path.join(projectDir, UCG_FOLDER));
|
|
89
|
+
const hasBmadDir = await fs.pathExists(path.join(projectDir, '_bmad'));
|
|
90
|
+
|
|
91
|
+
if (hasBmadUcg) {
|
|
92
|
+
return { type: 'existing', folder: UCG_FOLDER };
|
|
93
|
+
}
|
|
94
|
+
if (hasBmadDir) {
|
|
95
|
+
return { type: 'bmad-ready', folder: UCG_FOLDER };
|
|
96
|
+
}
|
|
97
|
+
return { type: 'fresh', folder: UCG_FOLDER };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async promptInstall() {
|
|
101
|
+
this.displayBanner();
|
|
102
|
+
|
|
103
|
+
const projectDir = process.cwd();
|
|
104
|
+
const defaultProjectName = path.basename(projectDir);
|
|
105
|
+
const detection = await this.detectInstallation(projectDir);
|
|
106
|
+
const ucgFolder = detection.folder;
|
|
107
|
+
|
|
108
|
+
log.info(`Target: ${brand.light(projectDir)}`);
|
|
109
|
+
|
|
110
|
+
let action = 'fresh';
|
|
111
|
+
|
|
112
|
+
if (detection.type === 'existing') {
|
|
113
|
+
const existingManifest = await readManifest(projectDir);
|
|
114
|
+
const installedVersion = existingManifest?.version || null;
|
|
115
|
+
const incomingVersion = require('../../../package.json').version;
|
|
116
|
+
|
|
117
|
+
let versionLabel;
|
|
118
|
+
let updateOptionLabel;
|
|
119
|
+
if (!installedVersion) {
|
|
120
|
+
versionLabel = `version unknown → v${incomingVersion}`;
|
|
121
|
+
updateOptionLabel = `Update — Install v${incomingVersion}, keep config.yaml`;
|
|
122
|
+
} else if (installedVersion === incomingVersion) {
|
|
123
|
+
versionLabel = `v${installedVersion} — already at this version`;
|
|
124
|
+
updateOptionLabel = `Update — Reinstall v${incomingVersion}, keep config.yaml`;
|
|
125
|
+
} else if (compareVersions(installedVersion, incomingVersion)) {
|
|
126
|
+
versionLabel = `v${installedVersion} → v${incomingVersion} available`;
|
|
127
|
+
updateOptionLabel = `Update — Upgrade from v${installedVersion} to v${incomingVersion}, keep config.yaml`;
|
|
128
|
+
} else {
|
|
129
|
+
versionLabel = `v${installedVersion} → v${incomingVersion}, DOWNGRADE`;
|
|
130
|
+
updateOptionLabel = `Update — Downgrade to v${incomingVersion}, keep config.yaml`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
log.warn(`Found existing installation at ${chalk.white(UCG_FOLDER + '/')} (${versionLabel})`);
|
|
134
|
+
|
|
135
|
+
const choice = await select({
|
|
136
|
+
message: 'What would you like to do?',
|
|
137
|
+
options: [
|
|
138
|
+
{ label: updateOptionLabel, value: 'update' },
|
|
139
|
+
{ label: 'Fresh install — Remove everything and start over', value: 'fresh' },
|
|
140
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (isCancel(choice) || choice === 'cancel') {
|
|
145
|
+
cancel('Installation cancelled.');
|
|
146
|
+
return { cancelled: true };
|
|
147
|
+
}
|
|
148
|
+
action = choice;
|
|
149
|
+
} else {
|
|
150
|
+
log.info(`The skill will be installed in ${chalk.white(ucgFolder + '/')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (action === 'update') {
|
|
154
|
+
log.info('Existing config.yaml will be preserved.');
|
|
155
|
+
return {
|
|
156
|
+
projectDir,
|
|
157
|
+
ucgFolder,
|
|
158
|
+
_detection: detection,
|
|
159
|
+
_action: action,
|
|
160
|
+
cancelled: false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Load saved config to pre-populate defaults on fresh reinstall
|
|
165
|
+
const savedConfig = await this.loadSavedConfig(projectDir, ucgFolder);
|
|
166
|
+
if (savedConfig) {
|
|
167
|
+
log.info('Previous configuration detected — defaults pre-populated.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Build IDE options from platform-codes.yaml
|
|
171
|
+
const platforms = getAvailablePlatforms();
|
|
172
|
+
const ideOptions = platforms.map((p) => ({
|
|
173
|
+
label: p.preferred ? `${p.label} (Recommended)` : p.label,
|
|
174
|
+
value: p.value,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
// Pre-check IDEs: saved config takes priority, then auto-detect from directories
|
|
178
|
+
const savedIdes = savedConfig?.ides || [];
|
|
179
|
+
let initialIdes = [];
|
|
180
|
+
if (savedIdes.length > 0) {
|
|
181
|
+
initialIdes = savedIdes;
|
|
182
|
+
} else {
|
|
183
|
+
const detectedIdes = await this.detectIdes(projectDir);
|
|
184
|
+
if (detectedIdes.length > 0) {
|
|
185
|
+
initialIdes = detectedIdes;
|
|
186
|
+
log.info(`Auto-detected IDEs: ${brand.light(detectedIdes.join(', '))}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Mark initially selected IDEs
|
|
191
|
+
for (const opt of ideOptions) {
|
|
192
|
+
opt.initialSelected = initialIdes.includes(opt.value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Project name
|
|
196
|
+
const project_name = await text({
|
|
197
|
+
message: 'Project name:',
|
|
198
|
+
initialValue: savedConfig?.project_name || defaultProjectName,
|
|
199
|
+
});
|
|
200
|
+
if (isCancel(project_name)) {
|
|
201
|
+
cancel('Installation cancelled.');
|
|
202
|
+
return { cancelled: true };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// IDE selection
|
|
206
|
+
const ides = await multiselect({
|
|
207
|
+
message: 'Which tools/IDEs are you using?',
|
|
208
|
+
options: ideOptions,
|
|
209
|
+
initialValues: initialIdes,
|
|
210
|
+
required: true,
|
|
211
|
+
});
|
|
212
|
+
if (isCancel(ides)) {
|
|
213
|
+
cancel('Installation cancelled.');
|
|
214
|
+
return { cancelled: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Learning material
|
|
218
|
+
const install_learning = await confirm({
|
|
219
|
+
message: 'Install learning & reference material?',
|
|
220
|
+
initialValue: true,
|
|
221
|
+
});
|
|
222
|
+
if (isCancel(install_learning)) {
|
|
223
|
+
cancel('Installation cancelled.');
|
|
224
|
+
return { cancelled: true };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
projectDir,
|
|
229
|
+
project_name,
|
|
230
|
+
ides,
|
|
231
|
+
install_learning,
|
|
232
|
+
ucgFolder,
|
|
233
|
+
_detection: detection,
|
|
234
|
+
_action: action,
|
|
235
|
+
cancelled: false,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async detectIdes(projectDir) {
|
|
240
|
+
const markers = getDetectionMarkers();
|
|
241
|
+
const detected = [];
|
|
242
|
+
for (const [ide, paths] of Object.entries(markers)) {
|
|
243
|
+
for (const p of paths) {
|
|
244
|
+
if (await fs.pathExists(path.join(projectDir, p))) {
|
|
245
|
+
detected.push(ide);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return detected;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async loadSavedConfig(projectDir, ucgFolder) {
|
|
254
|
+
const configPath = path.join(projectDir, ucgFolder, 'config.yaml');
|
|
255
|
+
try {
|
|
256
|
+
if (await fs.pathExists(configPath)) {
|
|
257
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
258
|
+
return yaml.load(content) || null;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// ignore parse errors
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
displaySuccess(ucgFolder, ides = [], action = 'fresh') {
|
|
267
|
+
// Build per-platform lookup tables from platform-codes.yaml
|
|
268
|
+
const ideNames = {};
|
|
269
|
+
const idePrefixes = {};
|
|
270
|
+
for (const p of getAvailablePlatforms()) {
|
|
271
|
+
ideNames[p.value] = p.label;
|
|
272
|
+
idePrefixes[p.value] = p.skillInvocationPrefix; // null = auto-invoke only
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const selectedIdes = Array.isArray(ides) && ides.length > 0 ? ides : [];
|
|
276
|
+
|
|
277
|
+
let ideDisplay;
|
|
278
|
+
if (selectedIdes.length === 0) {
|
|
279
|
+
ideDisplay = 'your IDE';
|
|
280
|
+
} else if (selectedIdes.length === 1) {
|
|
281
|
+
ideDisplay = ideNames[selectedIdes[0]] || 'your IDE';
|
|
282
|
+
} else {
|
|
283
|
+
ideDisplay = selectedIdes.map((ide) => ideNames[ide] || ide).join(' or ');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Build per-IDE invocation hints. Skills with a prefix get a literal
|
|
287
|
+
// command; auto-invoke IDEs get a chat-based instruction.
|
|
288
|
+
const invocations = selectedIdes.map((ide) => {
|
|
289
|
+
const name = ideNames[ide] || ide;
|
|
290
|
+
const prefix = idePrefixes[ide];
|
|
291
|
+
if (prefix) {
|
|
292
|
+
return { ide: name, command: `${prefix}ultracode-goal`, auto: false };
|
|
293
|
+
}
|
|
294
|
+
return { ide: name, command: null, auto: true };
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Compose the activate line shown in steps and outro.
|
|
298
|
+
let activateLine;
|
|
299
|
+
if (invocations.length === 0) {
|
|
300
|
+
// Update flow with no IDE list — show both common forms
|
|
301
|
+
activateLine = `${brand.light('/ultracode-goal')} ${chalk.dim('(Claude Code)')} ${chalk.dim('·')} ${brand.light('$ultracode-goal')} ${chalk.dim('(Codex)')}`;
|
|
302
|
+
} else if (invocations.length === 1) {
|
|
303
|
+
const inv = invocations[0];
|
|
304
|
+
activateLine = inv.auto ? chalk.dim(`${inv.ide} auto-loads ultracode-goal`) : brand.light(inv.command);
|
|
305
|
+
} else {
|
|
306
|
+
// Mixed: show one segment per IDE
|
|
307
|
+
activateLine = invocations
|
|
308
|
+
.map((inv) =>
|
|
309
|
+
inv.auto
|
|
310
|
+
? `${chalk.dim('auto')} ${chalk.dim('(' + inv.ide + ')')}`
|
|
311
|
+
: `${brand.light(inv.command)} ${chalk.dim('(' + inv.ide + ')')}`,
|
|
312
|
+
)
|
|
313
|
+
.join(' ');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let noteTitle;
|
|
317
|
+
let noteBody;
|
|
318
|
+
|
|
319
|
+
if (action === 'update') {
|
|
320
|
+
noteTitle = brand.indigo.bold('Update complete!');
|
|
321
|
+
noteBody = [
|
|
322
|
+
`${chalk.white.bold('What Changed')}`,
|
|
323
|
+
'UCG files have been refreshed.',
|
|
324
|
+
'Your config.yaml is preserved.',
|
|
325
|
+
'',
|
|
326
|
+
`${chalk.white.bold('Next Steps')}`,
|
|
327
|
+
`1. Reload the skill in ${ideDisplay}: ${activateLine}`,
|
|
328
|
+
`2. Resume or relaunch your Epic — the decision log recovers run state`,
|
|
329
|
+
].join('\n');
|
|
330
|
+
} else {
|
|
331
|
+
noteTitle = brand.indigo.bold('Installation complete!');
|
|
332
|
+
noteBody = [
|
|
333
|
+
`${chalk.white.bold('Get Started')}`,
|
|
334
|
+
`1. Open this folder in ${ideDisplay}`,
|
|
335
|
+
`2. Activate the conductor: ${activateLine}`,
|
|
336
|
+
`3. Name the Epic — UCG preflights it to a hard, remediated green light`,
|
|
337
|
+
`4. The run advances only when the deterministic gate reads PASS`,
|
|
338
|
+
'',
|
|
339
|
+
`${chalk.white.bold('Note')}`,
|
|
340
|
+
`UCG merges PreToolUse/Stop hooks into ${chalk.white('.claude/settings.local.json')}`,
|
|
341
|
+
`(machine-local, gitignored) at preflight — see SECURITY.md in the repo.`,
|
|
342
|
+
].join('\n');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
note(noteBody, noteTitle);
|
|
346
|
+
|
|
347
|
+
outro(
|
|
348
|
+
`${brand.spark('◎')} Skill: ${chalk.white('ultracode-goal')} ${chalk.dim('(Autonomous Epic Conductor)')}\n${brand.dark('⚡')} Docs: ${brand.indigo('https://github.com/armelhbobdad/bmad-module-ultracode-goal/tree/main/docs')}\n${brand.light('▶')} Launch an Epic: ${activateLine}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = { UI };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCG Version Check
|
|
3
|
+
* Async, non-blocking check against the npm registry.
|
|
4
|
+
* Returns a promise that resolves to an update notice string (or null).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const https = require('node:https');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = 'bmad-module-ultracode-goal';
|
|
11
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
12
|
+
const TIMEOUT_MS = 3000;
|
|
13
|
+
|
|
14
|
+
function fetchLatestVersion() {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const req = https.get(REGISTRY_URL, { timeout: TIMEOUT_MS }, (res) => {
|
|
17
|
+
if (res.statusCode !== 200) {
|
|
18
|
+
resolve(null);
|
|
19
|
+
res.resume();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let data = '';
|
|
24
|
+
res.on('data', (chunk) => {
|
|
25
|
+
data += chunk;
|
|
26
|
+
});
|
|
27
|
+
res.on('end', () => {
|
|
28
|
+
try {
|
|
29
|
+
const json = JSON.parse(data);
|
|
30
|
+
resolve(json.version || null);
|
|
31
|
+
} catch {
|
|
32
|
+
resolve(null);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
req.on('error', () => resolve(null));
|
|
38
|
+
req.on('timeout', () => {
|
|
39
|
+
req.destroy();
|
|
40
|
+
resolve(null);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareVersions(current, latest) {
|
|
46
|
+
const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
|
|
47
|
+
const [cMajor, cMinor, cPatch] = parse(current);
|
|
48
|
+
const [lMajor, lMinor, lPatch] = parse(latest);
|
|
49
|
+
|
|
50
|
+
if (lMajor > cMajor) return true;
|
|
51
|
+
if (lMajor === cMajor && lMinor > cMinor) return true;
|
|
52
|
+
if (lMajor === cMajor && lMinor === cMinor && lPatch > cPatch) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start an async version check. Call the returned function after your
|
|
58
|
+
* command finishes to print the update notice (if any).
|
|
59
|
+
*/
|
|
60
|
+
function startVersionCheck(currentVersion) {
|
|
61
|
+
const checkPromise = fetchLatestVersion().then((latestVersion) => {
|
|
62
|
+
if (!latestVersion || !compareVersions(currentVersion, latestVersion)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
'\n' +
|
|
67
|
+
chalk.hex('#6366F1')(` Update available: ${chalk.dim(currentVersion)} → ${chalk.hex('#818CF8').bold(latestVersion)}`) +
|
|
68
|
+
'\n' +
|
|
69
|
+
chalk.dim(` Run: npx bmad-module-ultracode-goal@latest install`) +
|
|
70
|
+
'\n'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return async function printIfReady() {
|
|
75
|
+
try {
|
|
76
|
+
const notice = await checkPromise;
|
|
77
|
+
if (notice) {
|
|
78
|
+
process.stderr.write(notice);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Never block or fail the CLI for a version check
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { startVersionCheck, compareVersions };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const installCommand = require('./commands/install');
|
|
3
|
+
const statusCommand = require('./commands/status');
|
|
4
|
+
const uninstallCommand = require('./commands/uninstall');
|
|
5
|
+
const updateCommand = require('./commands/update');
|
|
6
|
+
const { startVersionCheck } = require('./lib/version-check');
|
|
7
|
+
|
|
8
|
+
// Fix for stdin issues when running through npm on Windows
|
|
9
|
+
if (process.stdin.isTTY) {
|
|
10
|
+
try {
|
|
11
|
+
process.stdin.resume();
|
|
12
|
+
process.stdin.setEncoding('utf8');
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
process.stdin.on('error', () => {});
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// Silently ignore - some environments may not support these operations
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const packageJson = require('../../package.json');
|
|
22
|
+
|
|
23
|
+
// Start async version check (non-blocking)
|
|
24
|
+
const printUpdateNotice = startVersionCheck(packageJson.version);
|
|
25
|
+
|
|
26
|
+
program.version(packageJson.version).description('UltraCode Goal — Autonomous Epic Execution to a Machine-Checked Definition-of-Done');
|
|
27
|
+
|
|
28
|
+
for (const command of [installCommand, updateCommand, statusCommand, uninstallCommand]) {
|
|
29
|
+
const cmd = program.command(command.command).description(command.description);
|
|
30
|
+
for (const option of command.options || []) {
|
|
31
|
+
cmd.option(...option);
|
|
32
|
+
}
|
|
33
|
+
// Wrap action to print update notice after command completes
|
|
34
|
+
const originalAction = command.action;
|
|
35
|
+
cmd.action(async (...args) => {
|
|
36
|
+
await originalAction(...args);
|
|
37
|
+
await printUpdateNotice();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
program.parse(process.argv);
|
|
42
|
+
|
|
43
|
+
if (process.argv.slice(2).length === 0) {
|
|
44
|
+
program.outputHelp();
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UCG CLI - Direct execution wrapper for npx
|
|
5
|
+
* Ensures proper execution when run via npx from npm registry.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('node:child_process');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
|
|
12
|
+
// Check if we're running in an npx temporary directory
|
|
13
|
+
const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');
|
|
14
|
+
|
|
15
|
+
if (isNpxExecution) {
|
|
16
|
+
// Running via npx - spawn child process to preserve user's working directory
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const cliPath = path.join(__dirname, 'cli', 'ucg-cli.js');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(cliPath)) {
|
|
21
|
+
console.error('Error: Could not find ucg-cli.js at', cliPath);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
execSync(`node "${cliPath}" ${args.join(' ')}`, {
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
process.exit(error.status || 1);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// Local execution - use require
|
|
35
|
+
require('./cli/ucg-cli.js');
|
|
36
|
+
}
|