@longph2102/v-flow 1.5.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.md +265 -0
- package/CHANGELOG.md +318 -0
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/agents/ba-agent.md +437 -0
- package/agents/ba-critic-agent.md +156 -0
- package/agents/ba-to-ptyc-agent.md +112 -0
- package/agents/bugfix-analyst-agent.md +221 -0
- package/agents/constitute-agent.md +155 -0
- package/agents/help-agent.md +168 -0
- package/agents/implement-agent.md +220 -0
- package/agents/import-ba-docs-agent.md +164 -0
- package/agents/master-check-agent.md +228 -0
- package/agents/metrics-agent.md +180 -0
- package/agents/operations-agent.md +123 -0
- package/agents/plan-agent.md +218 -0
- package/agents/prototype-agent.md +191 -0
- package/agents/retrospective-agent.md +196 -0
- package/agents/review-agent.md +210 -0
- package/agents/sprint-agent.md +191 -0
- package/agents/status-agent.md +186 -0
- package/agents/sync-agent.md +201 -0
- package/agents/test-agent.md +166 -0
- package/agents/understand-agent.md +339 -0
- package/cli/commands/check.js +96 -0
- package/cli/commands/dev-quiz.js +107 -0
- package/cli/commands/doctor.js +348 -0
- package/cli/commands/feature.js +259 -0
- package/cli/commands/hooks.js +163 -0
- package/cli/commands/init.js +189 -0
- package/cli/commands/log.js +199 -0
- package/cli/commands/plugin.js +230 -0
- package/cli/commands/score-card.js +203 -0
- package/cli/commands/status.js +269 -0
- package/cli/commands/sync.js +59 -0
- package/cli/commands/upgrade.js +150 -0
- package/cli/commands/validate.js +1259 -0
- package/cli/commands/watch.js +151 -0
- package/cli/index.js +46 -0
- package/cli/lib/ac-test-gate.js +89 -0
- package/cli/lib/activity-log.js +209 -0
- package/cli/lib/cli-error.js +183 -0
- package/cli/lib/constitution-lint.js +561 -0
- package/cli/lib/dev-quiz-grade.js +127 -0
- package/cli/lib/governance.js +78 -0
- package/cli/lib/hook-targets.js +167 -0
- package/cli/lib/i18n.js +375 -0
- package/cli/lib/knowledge-oracle.js +379 -0
- package/cli/lib/logger.js +203 -0
- package/cli/lib/module-card-lint.js +304 -0
- package/cli/lib/module-card-score.js +223 -0
- package/cli/lib/plugins.js +481 -0
- package/cli/lib/scanner.js +692 -0
- package/cli/lib/sync-core.js +232 -0
- package/cli/lib/sync-targets.js +84 -0
- package/cli/lib/templates.js +268 -0
- package/cli/lib/yaml-parser.js +203 -0
- package/commands/v.ba-critic.md +101 -0
- package/commands/v.ba-to-ptyc.md +71 -0
- package/commands/v.bugfix.md +86 -0
- package/commands/v.check.md +131 -0
- package/commands/v.constitute.md +87 -0
- package/commands/v.constitution.md +84 -0
- package/commands/v.fork.md +127 -0
- package/commands/v.help.md +73 -0
- package/commands/v.hotfix.md +200 -0
- package/commands/v.implement.md +92 -0
- package/commands/v.import-ba-docs.md +222 -0
- package/commands/v.metrics.md +74 -0
- package/commands/v.operations.md +70 -0
- package/commands/v.plan.md +78 -0
- package/commands/v.prototype.md +121 -0
- package/commands/v.quickfix.md +169 -0
- package/commands/v.retrospective.md +80 -0
- package/commands/v.review.md +78 -0
- package/commands/v.rewind.md +127 -0
- package/commands/v.specify.md +118 -0
- package/commands/v.sprint.md +75 -0
- package/commands/v.status.md +62 -0
- package/commands/v.sync.md +81 -0
- package/commands/v.test.md +67 -0
- package/commands/v.understand.md +112 -0
- package/package.json +65 -0
- package/skills/_shared/constitution-reader/SKILL.md +109 -0
- package/skills/_shared/constitution-reader/config.json +52 -0
- package/skills/_shared/constitution-reader/examples/good/b1-phase-output.md +48 -0
- package/skills/_shared/constitution-reader/gotchas.md +46 -0
- package/skills/_shared/context-reader/SKILL.md +111 -0
- package/skills/_shared/context-reader/config.json +54 -0
- package/skills/_shared/context-reader/examples/good/legacy-nodejs-output.md +35 -0
- package/skills/_shared/context-reader/gotchas.md +49 -0
- package/skills/_shared/ears-notation/SKILL.md +63 -0
- package/skills/_shared/ears-notation/config.json +55 -0
- package/skills/_shared/ears-notation/examples/good/plan-test-interpretation.md +29 -0
- package/skills/_shared/ears-notation/gotchas.md +43 -0
- package/skills/check/cross-validator/SKILL.md +206 -0
- package/skills/check/cross-validator/config.json +33 -0
- package/skills/check/cross-validator/examples/good/validation-report-pass-with-concerns.md +105 -0
- package/skills/check/cross-validator/gotchas.md +43 -0
- package/skills/implement/constitution-enforcer/SKILL.md +134 -0
- package/skills/implement/constitution-enforcer/config.json +16 -0
- package/skills/implement/constitution-enforcer/examples/bad/vague-report.md +42 -0
- package/skills/implement/constitution-enforcer/examples/good/compliance-report.md +57 -0
- package/skills/implement/constitution-enforcer/gotchas.md +26 -0
- package/skills/implement/constitution-enforcer/scripts/check-constitution.sh +88 -0
- package/skills/implement/no-go-zone-guard/SKILL.md +173 -0
- package/skills/implement/no-go-zone-guard/config.json +28 -0
- package/skills/implement/no-go-zone-guard/examples/good/adapter-workaround.md +46 -0
- package/skills/implement/no-go-zone-guard/gotchas.md +27 -0
- package/skills/implement/no-go-zone-guard/scripts/check-nogo-zones.sh +148 -0
- package/skills/implement/no-go-zone-guard/scripts/nogo-precommit.sh +96 -0
- package/skills/implement/tdd-driver/SKILL.md +159 -0
- package/skills/implement/tdd-driver/config.json +33 -0
- package/skills/implement/tdd-driver/examples/good/tdd-cycle-product-repo.md +81 -0
- package/skills/implement/tdd-driver/gotchas.md +34 -0
- package/skills/metrics/metrics-collector/SKILL.md +133 -0
- package/skills/metrics/metrics-collector/config.json +16 -0
- package/skills/metrics/metrics-collector/examples/bad/incomplete-report.md +48 -0
- package/skills/metrics/metrics-collector/examples/good/full-metrics-report.md +101 -0
- package/skills/metrics/metrics-collector/gotchas.md +26 -0
- package/skills/operations/incident-runbook/SKILL.md +167 -0
- package/skills/operations/incident-runbook/config.json +21 -0
- package/skills/operations/incident-runbook/examples/bad/vague-incident-report.md +48 -0
- package/skills/operations/incident-runbook/examples/good/p1-hotfix-response.md +119 -0
- package/skills/operations/incident-runbook/gotchas.md +26 -0
- package/skills/plan/architecture-designer/SKILL.md +228 -0
- package/skills/plan/architecture-designer/config.json +32 -0
- package/skills/plan/architecture-designer/examples/bad/vague-plan.md +62 -0
- package/skills/plan/architecture-designer/examples/good/expand-contract-migration.md +56 -0
- package/skills/plan/architecture-designer/examples/good/plan-structure.md +58 -0
- package/skills/plan/architecture-designer/gotchas.md +45 -0
- package/skills/plan/task-breakdown/SKILL.md +208 -0
- package/skills/plan/task-breakdown/config.json +26 -0
- package/skills/plan/task-breakdown/examples/bad/vague-tasks.md +77 -0
- package/skills/plan/task-breakdown/examples/good/spike-clarify-tasks.md +66 -0
- package/skills/plan/task-breakdown/examples/good/tasks-login-feature.md +111 -0
- package/skills/plan/task-breakdown/gotchas.md +39 -0
- package/skills/prototype/LOGIC.md +240 -0
- package/skills/prototype/SKILL.md +185 -0
- package/skills/prototype/UI.md +407 -0
- package/skills/prototype/config.json +104 -0
- package/skills/prototype/examples/bad/prototype-notes.md +68 -0
- package/skills/prototype/examples/good/prototype-notes-ui.md +109 -0
- package/skills/prototype/examples/good/prototype-notes.md +67 -0
- package/skills/prototype/gotchas.md +128 -0
- package/skills/prototype/scripts/check-flow-state.ps1 +112 -0
- package/skills/prototype/scripts/check-flow-state.sh +104 -0
- package/skills/prototype/scripts/check-prototype-cleanup.ps1 +124 -0
- package/skills/prototype/scripts/check-prototype-cleanup.sh +109 -0
- package/skills/prototype/scripts/check-prototype-notes.ps1 +107 -0
- package/skills/prototype/scripts/check-prototype-notes.sh +102 -0
- package/skills/review/adversarial-reviewer/SKILL.md +137 -0
- package/skills/review/adversarial-reviewer/config.json +32 -0
- package/skills/review/adversarial-reviewer/examples/good/review-report-template.md +56 -0
- package/skills/review/adversarial-reviewer/gotchas.md +46 -0
- package/skills/review/adversarial-reviewer/scripts/quick-security-scan.sh +52 -0
- package/skills/specify/ba-bpmn-doc-gen/SKILL.md +108 -0
- package/skills/specify/ba-bpmn-doc-gen/reference/reference-bpmn-generation.md +528 -0
- package/skills/specify/ba-bpmn-doc-gen/reference/reference-drawio-flowchart.md +466 -0
- package/skills/specify/ba-critic/SKILL.md +172 -0
- package/skills/specify/ba-critic/config.json +32 -0
- package/skills/specify/ba-critic/examples/good/critic-report-round1.md +51 -0
- package/skills/specify/ba-critic/gotchas.md +40 -0
- package/skills/specify/ba-critic/scripts/check-spec-quality.sh +72 -0
- package/skills/specify/ba-doc-generator/SKILL.md +102 -0
- package/skills/specify/ba-doc-generator/references/template-clevel.md +84 -0
- package/skills/specify/ba-doc-generator/references/template-compliance.md +83 -0
- package/skills/specify/ba-doc-generator/references/template-dev.md +138 -0
- package/skills/specify/ba-doc-generator/references/template-partner.md +167 -0
- package/skills/specify/ba-doc-generator/references/template-pm.md +92 -0
- package/skills/specify/ba-doc-generator/references/template-review.md +114 -0
- package/skills/specify/ba-doc-generator/references/template-tester.md +108 -0
- package/skills/specify/ba-doc-generator/references/template-user.md +98 -0
- package/skills/specify/bugfix-analyst/SKILL.md +296 -0
- package/skills/specify/bugfix-analyst/config.json +41 -0
- package/skills/specify/bugfix-analyst/examples/bad/common-mistakes.md +71 -0
- package/skills/specify/bugfix-analyst/examples/good/email-validation-bugfix.md +53 -0
- package/skills/specify/bugfix-analyst/gotchas.md +51 -0
- package/skills/specify/ears-writer/SKILL.md +129 -0
- package/skills/specify/ears-writer/config.json +20 -0
- package/skills/specify/ears-writer/examples/bad/common-mistakes.md +17 -0
- package/skills/specify/ears-writer/examples/good/login-requirements.md +41 -0
- package/skills/specify/ears-writer/gotchas.md +43 -0
- package/skills/specify/ears-writer/scripts/check-ears-compliance.sh +51 -0
- package/skills/test/test-case-generator/SKILL.md +161 -0
- package/skills/test/test-case-generator/config.json +33 -0
- package/skills/test/test-case-generator/examples/good/test-cases-login.md +104 -0
- package/skills/test/test-case-generator/gotchas.md +43 -0
- package/skills/understand/ba-docs-scanner/SKILL.md +239 -0
- package/skills/understand/ba-docs-scanner/config.json +47 -0
- package/skills/understand/ba-docs-scanner/examples/good/work-order-br-extract.md +28 -0
- package/skills/understand/ba-docs-scanner/gotchas.md +44 -0
- package/skills/understand/ba-docs-scanner/merge-rules.md +47 -0
- package/skills/understand/codebase-scanner/SKILL.md +260 -0
- package/skills/understand/codebase-scanner/config.json +56 -0
- package/skills/understand/codebase-scanner/examples/good/menu-module-output.md +44 -0
- package/skills/understand/codebase-scanner/gotchas.md +42 -0
- package/skills/understand/codebase-scanner/scripts/scan-project-structure.sh +64 -0
- package/templates/DESIGN.md +456 -0
- package/templates/agent-command-template.yaml +240 -0
- package/templates/agent-config-template.md +170 -0
- package/templates/agent-definition-template.md +145 -0
- package/templates/agent-metrics-template.md +150 -0
- package/templates/api-contract-template.md +72 -0
- package/templates/bugfix-report-template.md +195 -0
- package/templates/bugfix-spec-template.md +134 -0
- package/templates/code-review-report-template.md +119 -0
- package/templates/constitution-template.md +234 -0
- package/templates/context-template.md +94 -0
- package/templates/data-model-template.md +95 -0
- package/templates/decision-log-template.md +92 -0
- package/templates/flow-state-template.yaml +208 -0
- package/templates/github/workflows/v-flow-validate.yml +30 -0
- package/templates/knowledge/adr-template.md +70 -0
- package/templates/knowledge/api-contract-template.md +140 -0
- package/templates/knowledge/domain-glossary.md +29 -0
- package/templates/knowledge/golden-tests-readme.md +115 -0
- package/templates/knowledge/lessons-learned.md +41 -0
- package/templates/knowledge/patterns.md +103 -0
- package/templates/module-card/SKILL.md +85 -0
- package/templates/module-card/api-specs.md +96 -0
- package/templates/module-card/business-quiz.md +119 -0
- package/templates/module-card/cross-service.md +125 -0
- package/templates/module-card/db.md +85 -0
- package/templates/module-card/dev-quiz.md +62 -0
- package/templates/module-card/permissions.md +83 -0
- package/templates/module-card/state-diagram.md +64 -0
- package/templates/module-card/tech-context.md +90 -0
- package/templates/module-card/ui-flows.md +91 -0
- package/templates/module-card/use-cases.md +142 -0
- package/templates/module-template.yaml +161 -0
- package/templates/operations-report-template.md +108 -0
- package/templates/plan-template.md +308 -0
- package/templates/prototype-notes-template.md +116 -0
- package/templates/ptyc/PTYC.template.docx +0 -0
- package/templates/ptyc/ptyc.meta.example.yaml +44 -0
- package/templates/retrospective-report-template.md +136 -0
- package/templates/security-review-template.md +84 -0
- package/templates/session-template.md +167 -0
- package/templates/spec-review-log-template.md +75 -0
- package/templates/spec-template.md +229 -0
- package/templates/sprint-status-template.md +101 -0
- package/templates/tasks-template.md +275 -0
- package/templates/test-cases-template.md +124 -0
- package/templates/ux-checklist-template.md +79 -0
- package/templates/validation-report-template.md +125 -0
- package/templates/vflow-config-template.yaml +22 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v-flow watch — tool-agnostic activity safety net.
|
|
3
|
+
*
|
|
4
|
+
* Watches `.v-flow/**` and logs every file create/modify/delete to
|
|
5
|
+
* `.v-flow/logs/`. Because it observes the real artifacts agents produce, it
|
|
6
|
+
* captures activity from ANY AI tool (even ones without hooks) and from manual
|
|
7
|
+
* edits — a deterministic fallback complementing per-tool hooks.
|
|
8
|
+
*
|
|
9
|
+
* Portable recursive watch: watches each directory individually (and adds
|
|
10
|
+
* watchers for new ones) instead of relying on fs.watch({recursive:true}),
|
|
11
|
+
* which is unavailable on Linux before Node 20.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const chalk = require('chalk');
|
|
17
|
+
const { withErrorHandler, requireInit } = require('../lib/cli-error');
|
|
18
|
+
const { isInitialized, getVFlowDir } = require('../lib/templates');
|
|
19
|
+
const activityLog = require('../lib/activity-log');
|
|
20
|
+
|
|
21
|
+
// First path segment under .v-flow/ to never watch. `logs` is mandatory (we
|
|
22
|
+
// write there — watching it would loop). The others are gitignored runtime
|
|
23
|
+
// copies that would only add noise.
|
|
24
|
+
const EXCLUDE_TOP = new Set(['logs', 'templates', 'skills']);
|
|
25
|
+
const IGNORE_BASENAMES = new Set(['.DS_Store']);
|
|
26
|
+
const DEBOUNCE_MS = 200;
|
|
27
|
+
|
|
28
|
+
module.exports = function (program) {
|
|
29
|
+
program
|
|
30
|
+
.command('watch')
|
|
31
|
+
.description('Watch .v-flow/ and log every file change (tool-agnostic safety net)')
|
|
32
|
+
.option('--timeout <seconds>', 'Stop automatically after N seconds (0 = run until Ctrl-C)', '0')
|
|
33
|
+
.action(
|
|
34
|
+
withErrorHandler((options) => {
|
|
35
|
+
const projectRoot = process.cwd();
|
|
36
|
+
requireInit(projectRoot, isInitialized);
|
|
37
|
+
const vflowDir = getVFlowDir(projectRoot);
|
|
38
|
+
|
|
39
|
+
const watchers = [];
|
|
40
|
+
const known = new Set(); // paths seen → distinguish created vs modified
|
|
41
|
+
const timers = new Map(); // debounce per path
|
|
42
|
+
let stopped = false;
|
|
43
|
+
|
|
44
|
+
const relTop = (full) => path.relative(vflowDir, full).split(path.sep)[0];
|
|
45
|
+
const isExcluded = (full) => EXCLUDE_TOP.has(relTop(full));
|
|
46
|
+
|
|
47
|
+
function watchDir(dir) {
|
|
48
|
+
let watcher;
|
|
49
|
+
try {
|
|
50
|
+
watcher = fs.watch(dir, (eventType, filename) => {
|
|
51
|
+
if (!filename) return;
|
|
52
|
+
const full = path.join(dir, filename);
|
|
53
|
+
if (isExcluded(full) || IGNORE_BASENAMES.has(path.basename(full))) return;
|
|
54
|
+
schedule(full);
|
|
55
|
+
});
|
|
56
|
+
} catch (_) {
|
|
57
|
+
return; // dir vanished before we could watch it
|
|
58
|
+
}
|
|
59
|
+
watcher.on('error', () => {});
|
|
60
|
+
watchers.push(watcher);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// emit(full) is called for each newly-discovered path so a directory
|
|
64
|
+
// created together with its files in one tick still logs those files.
|
|
65
|
+
function addTree(dir, emit) {
|
|
66
|
+
watchDir(dir);
|
|
67
|
+
let entries = [];
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const ent of entries) {
|
|
74
|
+
const full = path.join(dir, ent.name);
|
|
75
|
+
if (isExcluded(full) || IGNORE_BASENAMES.has(ent.name)) continue;
|
|
76
|
+
const isNew = !known.has(full);
|
|
77
|
+
known.add(full);
|
|
78
|
+
if (isNew && emit) emit(full);
|
|
79
|
+
if (ent.isDirectory()) addTree(full, emit);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function schedule(full) {
|
|
84
|
+
if (timers.has(full)) clearTimeout(timers.get(full));
|
|
85
|
+
timers.set(
|
|
86
|
+
full,
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
timers.delete(full);
|
|
89
|
+
handleChange(full);
|
|
90
|
+
}, DEBOUNCE_MS)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function logFs(full, action) {
|
|
95
|
+
const rel = path.relative(vflowDir, full);
|
|
96
|
+
activityLog.event({ type: 'fs', action, path: rel }, `[fs] ${action} ${rel}`);
|
|
97
|
+
const colors = { created: chalk.green, modified: chalk.yellow, deleted: chalk.red };
|
|
98
|
+
const color = colors[action] || chalk.white;
|
|
99
|
+
console.log(`${chalk.dim(new Date().toISOString())} ${color(action.padEnd(8))} ${rel}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleChange(full) {
|
|
103
|
+
const exists = fs.existsSync(full);
|
|
104
|
+
if (exists && !known.has(full)) {
|
|
105
|
+
known.add(full);
|
|
106
|
+
logFs(full, 'created');
|
|
107
|
+
try {
|
|
108
|
+
if (fs.statSync(full).isDirectory()) addTree(full, (child) => logFs(child, 'created'));
|
|
109
|
+
} catch (_) {
|
|
110
|
+
/* ignore */
|
|
111
|
+
}
|
|
112
|
+
} else if (exists) {
|
|
113
|
+
logFs(full, 'modified');
|
|
114
|
+
} else if (known.has(full)) {
|
|
115
|
+
known.delete(full);
|
|
116
|
+
logFs(full, 'deleted');
|
|
117
|
+
}
|
|
118
|
+
// else: spurious event for an unknown, non-existent path — ignore
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stop(reason) {
|
|
122
|
+
if (stopped) return;
|
|
123
|
+
stopped = true;
|
|
124
|
+
for (const t of timers.values()) clearTimeout(t);
|
|
125
|
+
for (const w of watchers) {
|
|
126
|
+
try {
|
|
127
|
+
w.close();
|
|
128
|
+
} catch (_) {
|
|
129
|
+
/* ignore */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
activityLog.event({ type: 'watch', event: 'stop', reason }, `[watch] stop (${reason})`);
|
|
133
|
+
console.log(chalk.dim(`\n🛑 watch stopped (${reason})`));
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
addTree(vflowDir);
|
|
138
|
+
activityLog.event({ type: 'watch', event: 'start' }, '[watch] start');
|
|
139
|
+
console.log(chalk.cyan.bold(`\n👀 Watching ${path.relative(projectRoot, vflowDir) || '.v-flow'}/ — logging changes to .v-flow/logs/`));
|
|
140
|
+
console.log(chalk.dim(` (excluding: ${[...EXCLUDE_TOP].join(', ')}) — Ctrl-C to stop\n`));
|
|
141
|
+
|
|
142
|
+
process.on('SIGINT', () => stop('SIGINT'));
|
|
143
|
+
process.on('SIGTERM', () => stop('SIGTERM'));
|
|
144
|
+
|
|
145
|
+
const timeout = parseInt(options.timeout, 10) || 0;
|
|
146
|
+
if (timeout > 0) {
|
|
147
|
+
setTimeout(() => stop(`timeout ${timeout}s`), timeout * 1000);
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
};
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const pkg = require('../package.json');
|
|
5
|
+
const activityLog = require('./lib/activity-log');
|
|
6
|
+
|
|
7
|
+
// Capture all CLI output + persist a per-command record to .v-flow/logs/.
|
|
8
|
+
activityLog.install();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('v-flow')
|
|
12
|
+
.description('🚀 V Flow CLI — Agentic AI Software Development Process')
|
|
13
|
+
.version(pkg.version)
|
|
14
|
+
// ── Global Options ──
|
|
15
|
+
.option('--verbose', 'Show detailed output')
|
|
16
|
+
.option('--lang <language>', 'Set language (vi, en)', process.env.VFLOW_LANG || 'vi');
|
|
17
|
+
|
|
18
|
+
// Make global options accessible to all commands via environment
|
|
19
|
+
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
20
|
+
const opts = program.opts();
|
|
21
|
+
if (opts.verbose) {
|
|
22
|
+
process.env.VFLOW_VERBOSE = '1';
|
|
23
|
+
}
|
|
24
|
+
if (opts.lang) {
|
|
25
|
+
process.env.VFLOW_LANG = opts.lang;
|
|
26
|
+
}
|
|
27
|
+
activityLog.setCommand(actionCommand.name(), process.argv.slice(2));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Register commands
|
|
31
|
+
require('./commands/init')(program);
|
|
32
|
+
require('./commands/check')(program);
|
|
33
|
+
require('./commands/status')(program);
|
|
34
|
+
require('./commands/feature')(program);
|
|
35
|
+
require('./commands/sync')(program);
|
|
36
|
+
require('./commands/doctor')(program);
|
|
37
|
+
require('./commands/upgrade')(program);
|
|
38
|
+
require('./commands/validate')(program);
|
|
39
|
+
require('./commands/score-card')(program);
|
|
40
|
+
require('./commands/dev-quiz')(program);
|
|
41
|
+
require('./commands/plugin')(program);
|
|
42
|
+
require('./commands/log')(program);
|
|
43
|
+
require('./commands/hooks')(program);
|
|
44
|
+
require('./commands/watch')(program);
|
|
45
|
+
|
|
46
|
+
program.parse();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AC-PASS Gate — verify every spec Acceptance Criterion (AC-NN) is satisfied by
|
|
3
|
+
* a PASSING automated test, by reading a JUnit XML test report.
|
|
4
|
+
*
|
|
5
|
+
* This is the "living-spec" leg of validation: existing checks confirm each AC
|
|
6
|
+
* is *referenced* by a task/test-case on paper; this one reaches into the real
|
|
7
|
+
* test runner and fails when an AC has no PASSING test behind it.
|
|
8
|
+
*
|
|
9
|
+
* Convention: a testcase "covers" AC-NN when the AC id token appears in its
|
|
10
|
+
* `name`/`classname` (e.g. `it('AC-03: rejects expired token', ...)`), reusing
|
|
11
|
+
* the same AC-NN ids V Flow already threads through spec → task → test-case.
|
|
12
|
+
*
|
|
13
|
+
* JUnit XML is the universal interchange format — Jest (jest-junit), Vitest,
|
|
14
|
+
* pytest, go-junit-report, Maven Surefire, etc. all emit it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse JUnit XML into a flat list of testcases.
|
|
19
|
+
* A case counts as failed if it contains a <failure>/<error> child, skipped if
|
|
20
|
+
* it contains <skipped>. Anything else is a pass.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} xml
|
|
23
|
+
* @returns {Array<{name,classname,label,passed,failed,skipped}>}
|
|
24
|
+
*/
|
|
25
|
+
function parseJUnitResults(xml) {
|
|
26
|
+
const cases = [];
|
|
27
|
+
if (!xml || typeof xml !== 'string') return cases;
|
|
28
|
+
|
|
29
|
+
// Match <testcase .../> (self-closing) or <testcase ...>...</testcase>.
|
|
30
|
+
const reCase = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = reCase.exec(xml)) !== null) {
|
|
33
|
+
const attrs = m[1] || '';
|
|
34
|
+
const inner = m[2] || '';
|
|
35
|
+
const name = (attrs.match(/\bname\s*=\s*"([^"]*)"/) || [])[1] || '';
|
|
36
|
+
const classname = (attrs.match(/\bclassname\s*=\s*"([^"]*)"/) || [])[1] || '';
|
|
37
|
+
const failed = /<(?:failure|error)\b/.test(inner);
|
|
38
|
+
const skipped = /<skipped\b/.test(inner);
|
|
39
|
+
cases.push({
|
|
40
|
+
name,
|
|
41
|
+
classname,
|
|
42
|
+
label: `${classname} ${name}`.trim(),
|
|
43
|
+
passed: !failed && !skipped,
|
|
44
|
+
failed,
|
|
45
|
+
skipped,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return cases;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract unique AC-NN ids from arbitrary text.
|
|
53
|
+
* @param {string} text
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
function extractAcIds(text) {
|
|
57
|
+
return [...new Set((text || '').match(/\bAC-\d+\b/g) || [])];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Classify each spec AC against parsed test cases.
|
|
62
|
+
*
|
|
63
|
+
* @param {string[]} specAcs - AC ids declared in the spec (e.g. ['AC-01','AC-02'])
|
|
64
|
+
* @param {Array} cases - output of parseJUnitResults
|
|
65
|
+
* @returns {{ total:number, passed:string[], failing:string[], missing:string[] }}
|
|
66
|
+
* passed = AC referenced by ≥1 PASSING test
|
|
67
|
+
* failing = AC referenced only by failing/skipped tests (no passing one)
|
|
68
|
+
* missing = AC referenced by no test at all
|
|
69
|
+
*/
|
|
70
|
+
function checkAcTestPass(specAcs, cases) {
|
|
71
|
+
const acs = [...new Set(specAcs || [])];
|
|
72
|
+
const list = cases || [];
|
|
73
|
+
const passed = [];
|
|
74
|
+
const failing = [];
|
|
75
|
+
const missing = [];
|
|
76
|
+
|
|
77
|
+
for (const ac of acs) {
|
|
78
|
+
// Word-boundary match so AC-1 does not match AC-12.
|
|
79
|
+
const re = new RegExp(`\\b${ac}\\b`);
|
|
80
|
+
const refs = list.filter((c) => re.test(c.label));
|
|
81
|
+
if (refs.length === 0) missing.push(ac);
|
|
82
|
+
else if (refs.some((c) => c.passed)) passed.push(ac);
|
|
83
|
+
else failing.push(ac);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { total: acs.length, passed, failing, missing };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { parseJUnitResults, extractAcIds, checkAcTestPass };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Log — Persistent, file-based audit of V-Flow usage.
|
|
3
|
+
*
|
|
4
|
+
* Records two surfaces into <projectRoot>/.v-flow/logs/:
|
|
5
|
+
* • CLI runs — every `v-flow <cmd>` invocation (args, exit code, duration,
|
|
6
|
+
* full console transcript). Captured automatically from
|
|
7
|
+
* cli/index.js via install().
|
|
8
|
+
* • Agent steps — structured events appended by the `v-flow log` command,
|
|
9
|
+
* which the agentic workflow (AGENTS.md) calls at phase
|
|
10
|
+
* boundaries.
|
|
11
|
+
*
|
|
12
|
+
* Output files:
|
|
13
|
+
* activity.jsonl — one JSON object per line (machine-readable / grep / jq).
|
|
14
|
+
* activity.log — human-readable mirror.
|
|
15
|
+
*
|
|
16
|
+
* Design notes:
|
|
17
|
+
* • Best-effort — every failure is swallowed; logging must NEVER break the CLI.
|
|
18
|
+
* • Synchronous appends — survive process.exit()/crashes without an async flush.
|
|
19
|
+
* • Disabled via VFLOW_NO_LOG=1. Custom path via VFLOW_LOG_DIR / VFLOW_LOG_FILE.
|
|
20
|
+
* • Never auto-creates .v-flow/ (that would corrupt isInitialized()); only logs
|
|
21
|
+
* into the project when .v-flow/ already exists (or an explicit override set).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const util = require('util');
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line no-control-regex
|
|
29
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
30
|
+
const stripAnsi = (s) => String(s).replace(ANSI, '');
|
|
31
|
+
|
|
32
|
+
let _installed = false;
|
|
33
|
+
let _finalized = false;
|
|
34
|
+
const _disabled = process.env.VFLOW_NO_LOG === '1';
|
|
35
|
+
let _startTime = null;
|
|
36
|
+
let _command = null; // { name, invocation }
|
|
37
|
+
const _transcript = []; // buffered console output lines
|
|
38
|
+
|
|
39
|
+
// Commands that write their own log entries — skip the auto CLI command record
|
|
40
|
+
// (avoids double-logging and, for `watch`, a huge buffered transcript).
|
|
41
|
+
const SELF_LOGGING = new Set(['log', 'watch']);
|
|
42
|
+
|
|
43
|
+
function nowIso() {
|
|
44
|
+
return new Date().toISOString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeVersion() {
|
|
48
|
+
try {
|
|
49
|
+
return require('../../package.json').version;
|
|
50
|
+
} catch (_) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the log directory.
|
|
57
|
+
* Priority: VFLOW_LOG_DIR > dir(VFLOW_LOG_FILE) > <cwd>/.v-flow/logs (only if
|
|
58
|
+
* .v-flow already exists). Returns null when logging should be skipped.
|
|
59
|
+
*/
|
|
60
|
+
function resolveLogDir() {
|
|
61
|
+
if (process.env.VFLOW_LOG_DIR) return process.env.VFLOW_LOG_DIR;
|
|
62
|
+
if (process.env.VFLOW_LOG_FILE) return path.dirname(process.env.VFLOW_LOG_FILE);
|
|
63
|
+
const vflow = path.join(process.cwd(), '.v-flow');
|
|
64
|
+
if (fs.existsSync(vflow)) return path.join(vflow, 'logs');
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function jsonlPath(dir) {
|
|
69
|
+
if (process.env.VFLOW_LOG_FILE) return process.env.VFLOW_LOG_FILE;
|
|
70
|
+
return path.join(dir, 'activity.jsonl');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logPath(dir) {
|
|
74
|
+
if (process.env.VFLOW_LOG_FILE) {
|
|
75
|
+
return `${process.env.VFLOW_LOG_FILE.replace(/\.jsonl?$/i, '') }.log`;
|
|
76
|
+
}
|
|
77
|
+
return path.join(dir, 'activity.log');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Append one structured entry to activity.jsonl, plus an optional human-readable
|
|
82
|
+
* line to activity.log. Safe to call anytime — no-op if disabled or no dir.
|
|
83
|
+
* `ts` is added automatically.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} entry Fields to record (without ts).
|
|
86
|
+
* @param {string} [humanLine] Suffix for the .log mirror (ts is prepended).
|
|
87
|
+
*/
|
|
88
|
+
function event(entry, humanLine) {
|
|
89
|
+
if (_disabled) return;
|
|
90
|
+
try {
|
|
91
|
+
const dir = resolveLogDir();
|
|
92
|
+
if (!dir) return;
|
|
93
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
94
|
+
const record = { ts: nowIso(), ...entry };
|
|
95
|
+
fs.appendFileSync(jsonlPath(dir), `${JSON.stringify(record)}\n`);
|
|
96
|
+
if (humanLine) {
|
|
97
|
+
fs.appendFileSync(logPath(dir), `${record.ts} ${humanLine}\n`);
|
|
98
|
+
}
|
|
99
|
+
} catch (_) {
|
|
100
|
+
/* logging must never break the CLI */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Install console capture + the exit hook that persists the CLI command record.
|
|
106
|
+
* Call once from cli/index.js. No-op if disabled.
|
|
107
|
+
*/
|
|
108
|
+
function install() {
|
|
109
|
+
if (_installed || _disabled) return;
|
|
110
|
+
_installed = true;
|
|
111
|
+
_startTime = Date.now();
|
|
112
|
+
|
|
113
|
+
const orig = {
|
|
114
|
+
log: console.log.bind(console),
|
|
115
|
+
info: console.info.bind(console),
|
|
116
|
+
warn: console.warn.bind(console),
|
|
117
|
+
error: console.error.bind(console),
|
|
118
|
+
debug: console.debug.bind(console),
|
|
119
|
+
};
|
|
120
|
+
const tee = (fn) => (...args) => {
|
|
121
|
+
try {
|
|
122
|
+
_transcript.push(stripAnsi(util.format(...args)));
|
|
123
|
+
} catch (_) {
|
|
124
|
+
/* ignore formatting errors */
|
|
125
|
+
}
|
|
126
|
+
fn(...args);
|
|
127
|
+
};
|
|
128
|
+
console.log = tee(orig.log);
|
|
129
|
+
console.info = tee(orig.info);
|
|
130
|
+
console.warn = tee(orig.warn);
|
|
131
|
+
console.error = tee(orig.error);
|
|
132
|
+
console.debug = tee(orig.debug);
|
|
133
|
+
|
|
134
|
+
process.on('exit', (code) => finalize(code));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Record which command is running. Called from index.js preAction hook.
|
|
139
|
+
*/
|
|
140
|
+
function setCommand(name, invocation) {
|
|
141
|
+
_command = { name, invocation: invocation || [] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Persist the CLI command record (jsonl + transcript). Idempotent; runs on exit.
|
|
146
|
+
*/
|
|
147
|
+
function finalize(exitCode) {
|
|
148
|
+
if (_finalized || _disabled) return;
|
|
149
|
+
_finalized = true;
|
|
150
|
+
// Only persist when a real subcommand ran. Skip help/version/no-op and any
|
|
151
|
+
// self-logging command (avoids double/circular logging).
|
|
152
|
+
if (!_command || SELF_LOGGING.has(_command.name)) return;
|
|
153
|
+
try {
|
|
154
|
+
const dir = resolveLogDir();
|
|
155
|
+
if (!dir) return;
|
|
156
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
157
|
+
const durationMs = _startTime ? Date.now() - _startTime : null;
|
|
158
|
+
const code = typeof exitCode === 'number' ? exitCode : process.exitCode || 0;
|
|
159
|
+
const record = {
|
|
160
|
+
ts: nowIso(),
|
|
161
|
+
type: 'cli',
|
|
162
|
+
cmd: _command.name,
|
|
163
|
+
invocation: _command.invocation,
|
|
164
|
+
exitCode: code,
|
|
165
|
+
durationMs,
|
|
166
|
+
cwd: process.cwd(),
|
|
167
|
+
version: safeVersion(),
|
|
168
|
+
};
|
|
169
|
+
fs.appendFileSync(jsonlPath(dir), `${JSON.stringify(record)}\n`);
|
|
170
|
+
const label = `v-flow ${record.invocation.join(' ')}`.trim();
|
|
171
|
+
const header = `\n── ${record.ts} ${label} (exit ${code}, ${durationMs}ms) ──\n`;
|
|
172
|
+
const body = _transcript.length ? `${_transcript.join('\n')}\n` : '';
|
|
173
|
+
fs.appendFileSync(logPath(dir), `${header}${body}`);
|
|
174
|
+
} catch (_) {
|
|
175
|
+
/* never break the CLI */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read the last N entries from activity.jsonl (best-effort). Used by `v-flow log --tail`.
|
|
181
|
+
* @returns {object[]} parsed records (oldest → newest)
|
|
182
|
+
*/
|
|
183
|
+
function tail(n = 20) {
|
|
184
|
+
try {
|
|
185
|
+
const dir = resolveLogDir();
|
|
186
|
+
if (!dir) return [];
|
|
187
|
+
const file = jsonlPath(dir);
|
|
188
|
+
if (!fs.existsSync(file)) return [];
|
|
189
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
190
|
+
return lines.slice(-n).map((line) => {
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(line);
|
|
193
|
+
} catch (_) {
|
|
194
|
+
return { raw: line };
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
} catch (_) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
install,
|
|
204
|
+
setCommand,
|
|
205
|
+
finalize,
|
|
206
|
+
event,
|
|
207
|
+
tail,
|
|
208
|
+
resolveLogDir,
|
|
209
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Error Utilities — Unified error handling for V-Flow CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error formatting, exit codes, and error types
|
|
5
|
+
* across all CLI commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
// ============================================================
|
|
11
|
+
// ERROR TYPES
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base CLI error with exit code and user-friendly message
|
|
16
|
+
*/
|
|
17
|
+
class CliError extends Error {
|
|
18
|
+
constructor(message, { exitCode = 1, hint = null, cause = null } = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'CliError';
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
this.hint = hint;
|
|
23
|
+
if (cause) this.cause = cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Project not initialized error
|
|
29
|
+
*/
|
|
30
|
+
class NotInitializedError extends CliError {
|
|
31
|
+
constructor() {
|
|
32
|
+
super('V Flow is not initialized (.v-flow/ not found)', {
|
|
33
|
+
exitCode: 1,
|
|
34
|
+
hint: 'Run: v-flow init',
|
|
35
|
+
});
|
|
36
|
+
this.name = 'NotInitializedError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Feature not found error
|
|
42
|
+
*/
|
|
43
|
+
class FeatureNotFoundError extends CliError {
|
|
44
|
+
constructor(featureName) {
|
|
45
|
+
super(`Feature not found: ${featureName}`, {
|
|
46
|
+
exitCode: 1,
|
|
47
|
+
hint: 'Run: v-flow feature list to see available features',
|
|
48
|
+
});
|
|
49
|
+
this.name = 'FeatureNotFoundError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validation error (bad input, missing argument, etc.)
|
|
55
|
+
*/
|
|
56
|
+
class ValidationError extends CliError {
|
|
57
|
+
constructor(message, hint = null) {
|
|
58
|
+
super(message, { exitCode: 1, hint });
|
|
59
|
+
this.name = 'ValidationError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// ERROR HANDLER
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wrap a command action with consistent error handling.
|
|
69
|
+
*
|
|
70
|
+
* Usage:
|
|
71
|
+
* program.command('foo').action(withErrorHandler(async (options) => { ... }));
|
|
72
|
+
*
|
|
73
|
+
* @param {Function} actionFn - async function(options) to wrap
|
|
74
|
+
* @returns {Function} wrapped function
|
|
75
|
+
*/
|
|
76
|
+
function withErrorHandler(actionFn) {
|
|
77
|
+
return async (...args) => {
|
|
78
|
+
try {
|
|
79
|
+
await actionFn(...args);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof CliError) {
|
|
82
|
+
console.error(chalk.red(`\n❌ ${err.message}`));
|
|
83
|
+
if (err.hint) {
|
|
84
|
+
console.error(chalk.dim(` → ${err.hint}`));
|
|
85
|
+
}
|
|
86
|
+
console.error('');
|
|
87
|
+
process.exit(err.exitCode);
|
|
88
|
+
} else {
|
|
89
|
+
// Unexpected error — show stack in verbose mode
|
|
90
|
+
console.error(chalk.red(`\n❌ Unexpected error: ${err.message}`));
|
|
91
|
+
if (process.env.VFLOW_DEBUG || process.argv.includes('--debug')) {
|
|
92
|
+
console.error(chalk.dim(err.stack));
|
|
93
|
+
} else {
|
|
94
|
+
console.error(chalk.dim(' → Re-run with VFLOW_DEBUG=1 for full stack trace'));
|
|
95
|
+
}
|
|
96
|
+
console.error('');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// GUARD UTILITIES
|
|
105
|
+
// ============================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Assert project is initialized, throw NotInitializedError if not.
|
|
109
|
+
* @param {string} projectRoot
|
|
110
|
+
* @param {Function} isInitialized - from templates.js
|
|
111
|
+
*/
|
|
112
|
+
function requireInit(projectRoot, isInitialized) {
|
|
113
|
+
if (!isInitialized(projectRoot)) {
|
|
114
|
+
throw new NotInitializedError();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// FORMATTING UTILITIES
|
|
120
|
+
// ============================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format bytes to human-readable string
|
|
124
|
+
*/
|
|
125
|
+
function formatBytes(bytes) {
|
|
126
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
127
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
128
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Print a section header
|
|
133
|
+
*/
|
|
134
|
+
function printHeader(title) {
|
|
135
|
+
console.log(chalk.white.bold(`\n${title}`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Print a success line
|
|
140
|
+
*/
|
|
141
|
+
function printOk(message) {
|
|
142
|
+
console.log(chalk.green(` ✅ ${message}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Print a warning line
|
|
147
|
+
*/
|
|
148
|
+
function printWarn(message) {
|
|
149
|
+
console.log(chalk.yellow(` ⚠️ ${message}`));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Print an error line
|
|
154
|
+
*/
|
|
155
|
+
function printError(message) {
|
|
156
|
+
console.log(chalk.red(` ❌ ${message}`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Print a dimmed info line
|
|
161
|
+
*/
|
|
162
|
+
function printDim(message) {
|
|
163
|
+
console.log(chalk.dim(` ${message}`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
// Error classes
|
|
168
|
+
CliError,
|
|
169
|
+
NotInitializedError,
|
|
170
|
+
FeatureNotFoundError,
|
|
171
|
+
ValidationError,
|
|
172
|
+
// Handler
|
|
173
|
+
withErrorHandler,
|
|
174
|
+
// Guards
|
|
175
|
+
requireInit,
|
|
176
|
+
// Formatting
|
|
177
|
+
formatBytes,
|
|
178
|
+
printHeader,
|
|
179
|
+
printOk,
|
|
180
|
+
printWarn,
|
|
181
|
+
printError,
|
|
182
|
+
printDim,
|
|
183
|
+
};
|