deploybuddy 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +40 -2
- package/dist/adapters/NodeWorkspaceAdapter.js +5 -0
- package/dist/baselineStore.js +38 -0
- package/dist/cli.js +576 -0
- package/dist/historyStore.js +37 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeployBuddy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DeployBuddy CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeployBuddy checks local project, Git, diagnostics, policy, change, and release evidence. The CLI uses the same deterministic `@deploybuddy/core` assessment logic as the VS Code extension and GitHub Action. It does not make AI requests or send repository data externally.
|
|
4
|
+
|
|
5
|
+
## Install for local development
|
|
6
|
+
|
|
7
|
+
From the monorepo root:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install
|
|
11
|
+
npm run cli:build
|
|
12
|
+
node apps/cli/dist/cli.js check
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
deploybuddy check
|
|
19
|
+
deploybuddy status
|
|
20
|
+
deploybuddy git
|
|
21
|
+
deploybuddy framework
|
|
22
|
+
deploybuddy checks
|
|
23
|
+
deploybuddy doctor
|
|
24
|
+
deploybuddy readiness
|
|
25
|
+
deploybuddy release
|
|
26
|
+
deploybuddy policy
|
|
27
|
+
deploybuddy changes
|
|
28
|
+
deploybuddy history
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `deploybuddy check --json` for automation. JSON property names and enums are public contracts for a given CLI major version. `--quiet` emits `VERDICT SCORE`; color is automatic for TTY output and can be disabled with `--no-color` or `NO_COLOR`.
|
|
32
|
+
|
|
33
|
+
## Exit codes
|
|
34
|
+
|
|
35
|
+
- `check`: 0 for safe/caution by default; 1 for danger when `exitOnDanger` is enabled or when configured policy fails; 2 for invalid options/configuration or inspection failure.
|
|
36
|
+
- `release`: 0 for PASS or NOT CONFIGURED; 1 for FAIL; 2 for invalid configuration or inspection failure.
|
|
37
|
+
- `policy validate`: 0 for valid/unconfigured policy; 2 for malformed JSON or invalid policy.
|
|
38
|
+
- `compare`: 0 when a comparison is produced or no usable baseline exists; 2 for invalid input or inspection failure.
|
|
39
|
+
- `doctor`: 0 when integration is healthy; 2 when configuration or inspection is invalid.
|
|
40
|
+
|
|
41
|
+
These codes are stable within a CLI major version. See the root [CLI contract](../../docs/cli.md) for the full JSON and exit behavior.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeWorkspaceAdapter = void 0;
|
|
4
|
+
var node_adapter_1 = require("@deploybuddy/node-adapter");
|
|
5
|
+
Object.defineProperty(exports, "NodeWorkspaceAdapter", { enumerable: true, get: function () { return node_adapter_1.NodeWorkspaceAdapter; } });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getBaselinePath = getBaselinePath;
|
|
7
|
+
exports.readLocalBaseline = readLocalBaseline;
|
|
8
|
+
exports.saveLocalBaseline = saveLocalBaseline;
|
|
9
|
+
exports.clearLocalBaseline = clearLocalBaseline;
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const node_os_1 = require("node:os");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const promises_1 = require("node:fs/promises");
|
|
14
|
+
const core_1 = require("@deploybuddy/core");
|
|
15
|
+
/** Baselines are scoped to a normalized project path but the path itself is never persisted. */
|
|
16
|
+
function getBaselinePath(cwd, env = process.env) {
|
|
17
|
+
const base = process.platform === 'win32' ? env.LOCALAPPDATA || node_path_1.default.join((0, node_os_1.homedir)(), 'AppData', 'Local') : process.platform === 'darwin' ? node_path_1.default.join((0, node_os_1.homedir)(), 'Library', 'Caches') : env.XDG_CACHE_HOME || node_path_1.default.join((0, node_os_1.homedir)(), '.cache');
|
|
18
|
+
const scope = (0, node_crypto_1.createHash)('sha256').update(node_path_1.default.resolve(cwd)).digest('hex');
|
|
19
|
+
return node_path_1.default.join(base, 'DeployBuddy', 'baselines', `${scope}.json`);
|
|
20
|
+
}
|
|
21
|
+
async function readLocalBaseline(cwd) {
|
|
22
|
+
try {
|
|
23
|
+
return (0, core_1.parseAssessmentBaseline)(JSON.parse(await (0, promises_1.readFile)(getBaselinePath(cwd), 'utf8')));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT')
|
|
27
|
+
return undefined;
|
|
28
|
+
return { status: 'invalid', message: 'The local DeployBuddy baseline could not be read.' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function saveLocalBaseline(cwd, baseline) {
|
|
32
|
+
const target = getBaselinePath(cwd);
|
|
33
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(target), { recursive: true, mode: 0o700 });
|
|
34
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
35
|
+
await (0, promises_1.writeFile)(temporary, `${JSON.stringify(baseline, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
36
|
+
await (0, promises_1.rename)(temporary, target);
|
|
37
|
+
}
|
|
38
|
+
async function clearLocalBaseline(cwd) { await (0, promises_1.rm)(getBaselinePath(cwd), { force: true }); }
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.parseArgs = parseArgs;
|
|
8
|
+
exports.getAssessment = getAssessment;
|
|
9
|
+
exports.formatDelta = formatDelta;
|
|
10
|
+
exports.runCLI = runCLI;
|
|
11
|
+
const promises_1 = require("node:readline/promises");
|
|
12
|
+
const node_process_1 = require("node:process");
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_util_1 = require("node:util");
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const core_1 = require("@deploybuddy/core");
|
|
17
|
+
const promises_2 = require("node:fs/promises");
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const NodeWorkspaceAdapter_1 = require("./adapters/NodeWorkspaceAdapter");
|
|
20
|
+
const baselineStore_1 = require("./baselineStore");
|
|
21
|
+
const historyStore_1 = require("./historyStore");
|
|
22
|
+
const execFile = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const commands = new Set(['check', 'status', 'git', 'framework', 'checks', 'doctor', 'policy', 'baseline', 'compare', 'history', 'changes', 'readiness', 'release', 'help']);
|
|
25
|
+
const commandName = argv.find(value => !value.startsWith('-')) ?? 'help';
|
|
26
|
+
if (!commands.has(commandName))
|
|
27
|
+
throw new Error(`Unknown command: ${commandName}. Run deploybuddy --help.`);
|
|
28
|
+
const allowed = new Set(['--json', '--quiet', '--no-color', '--run', '--debug', '--help', '-h', '--limit', '--yes', '--head', '--base', '--from', '--to']);
|
|
29
|
+
for (const arg of argv)
|
|
30
|
+
if (arg.startsWith('-') && !allowed.has(arg))
|
|
31
|
+
throw new Error(`Unknown option: ${arg}.`);
|
|
32
|
+
const command = argv.includes('--help') || argv.includes('-h') ? 'help' : commandName;
|
|
33
|
+
const policyAction = argv.includes('validate') ? 'validate' : argv.includes('init') ? 'init' : 'show';
|
|
34
|
+
const baselineAction = argv.includes('save') ? 'save' : argv.includes('clear') ? 'clear' : 'show';
|
|
35
|
+
const historyAction = command === 'history' ? ['clear', 'stats', 'show', 'commit', 'changes'].find(item => argv.includes(item)) ?? 'list' : 'list';
|
|
36
|
+
const limitIndex = argv.indexOf('--limit');
|
|
37
|
+
const optionValue = (flag) => { const index = argv.indexOf(flag); return index >= 0 ? argv[index + 1] : undefined; };
|
|
38
|
+
const base = optionValue('--base');
|
|
39
|
+
const from = optionValue('--from');
|
|
40
|
+
const to = optionValue('--to');
|
|
41
|
+
const validRevision = (value) => Boolean(value && /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(value) && !value.includes('..') && !value.includes('//') && !value.endsWith('/') && !value.includes('@{'));
|
|
42
|
+
for (const [flag, value] of [['--base', base], ['--from', from], ['--to', to]])
|
|
43
|
+
if (argv.includes(flag) && !validRevision(value))
|
|
44
|
+
throw new Error(`${flag} requires a valid Git revision.`);
|
|
45
|
+
const rawLimit = limitIndex >= 0 ? argv[limitIndex + 1] : undefined;
|
|
46
|
+
const limit = rawLimit === undefined ? 10 : Number(rawLimit);
|
|
47
|
+
if (limitIndex >= 0 && (!/^\d+$/.test(rawLimit ?? '') || limit < 1 || limit > 100))
|
|
48
|
+
throw new Error('--limit must be an integer between 1 and 100.');
|
|
49
|
+
if (argv.includes('validate') && argv.includes('init'))
|
|
50
|
+
throw new Error('Choose one `deploybuddy policy` subcommand: validate or init.');
|
|
51
|
+
const consumedValues = new Set([limitIndex + 1, argv.indexOf('--base') + 1, argv.indexOf('--from') + 1, argv.indexOf('--to') + 1]);
|
|
52
|
+
const positionals = argv.filter((value, index) => !value.startsWith('-') && !consumedValues.has(index));
|
|
53
|
+
if (command === 'policy' && positionals.some(value => !['policy', 'validate', 'init'].includes(value)))
|
|
54
|
+
throw new Error(`Unknown policy subcommand: ${positionals.find(value => !['policy', 'validate', 'init'].includes(value))}.`);
|
|
55
|
+
if (command === 'baseline' && positionals.some(value => !['baseline', 'save', 'show', 'clear'].includes(value)))
|
|
56
|
+
throw new Error(`Unknown baseline subcommand: ${positionals.find(value => !['baseline', 'save', 'show', 'clear'].includes(value))}.`);
|
|
57
|
+
if (command !== 'policy' && (argv.includes('validate') || argv.includes('init')))
|
|
58
|
+
throw new Error('`validate` and `init` are subcommands of `deploybuddy policy`.');
|
|
59
|
+
if ((command !== 'baseline' && argv.includes('save')) || (command !== 'baseline' && command !== 'history' && argv.includes('clear')))
|
|
60
|
+
throw new Error('`save` and `clear` are subcommands of `deploybuddy baseline` (or `history clear`).');
|
|
61
|
+
if (command !== 'history' && (argv.some(value => ['stats', 'commit'].includes(value) || (value === 'changes' && command !== 'changes')) || limitIndex >= 0 || argv.includes('--yes')))
|
|
62
|
+
throw new Error('History options are supported only by `deploybuddy history`.');
|
|
63
|
+
if (command === 'history' && historyAction !== 'list' && limitIndex >= 0)
|
|
64
|
+
throw new Error('--limit is supported only by the history list command.');
|
|
65
|
+
if (command === 'history' && ['show', 'commit'].includes(historyAction) && positionals.filter(value => !['history', 'show', 'commit'].includes(value)).length !== 1)
|
|
66
|
+
throw new Error(`Usage: deploybuddy history ${historyAction} <id-or-sha>.`);
|
|
67
|
+
if (command === 'history' && !['show', 'commit'].includes(historyAction)) {
|
|
68
|
+
const extra = positionals.filter(value => !['history', historyAction].includes(value));
|
|
69
|
+
if (extra.length)
|
|
70
|
+
throw new Error(`Unknown history subcommand or argument: ${extra[0]}.`);
|
|
71
|
+
}
|
|
72
|
+
if (argv.includes('--head') && command !== 'compare')
|
|
73
|
+
throw new Error('--head is supported only by `deploybuddy compare`.');
|
|
74
|
+
if ((base || from || to) && command !== 'changes')
|
|
75
|
+
throw new Error('--base, --from and --to are supported only by `deploybuddy changes`.');
|
|
76
|
+
if (command === 'changes' && (base !== undefined && (from !== undefined || to !== undefined) || (from === undefined) !== (to === undefined)))
|
|
77
|
+
throw new Error('Use either `changes --base <revision>` or `changes --from <revision> --to <revision>`.');
|
|
78
|
+
if (command === 'changes' && positionals.length !== 0)
|
|
79
|
+
throw new Error('Usage: deploybuddy changes [--base <revision> | --from <revision> --to <revision>].');
|
|
80
|
+
if (argv.includes('--yes') && (command !== 'history' || historyAction !== 'clear'))
|
|
81
|
+
throw new Error('--yes is supported only by `deploybuddy history clear`.');
|
|
82
|
+
if (command === 'history' && historyAction === 'clear' && argv.includes('--json'))
|
|
83
|
+
throw new Error('--json is not supported by `deploybuddy history clear`.');
|
|
84
|
+
const historyQuery = command === 'history' && ['show', 'commit'].includes(historyAction) ? positionals.filter(value => !['history', 'show', 'commit'].includes(value))[0] : undefined;
|
|
85
|
+
return { command, policyAction, baselineAction, historyAction, ...(historyQuery ? { historyQuery } : {}), limit, yes: argv.includes('--yes'), head: argv.includes('--head'), json: argv.includes('--json'), quiet: argv.includes('--quiet'), noColor: argv.includes('--no-color'), run: argv.includes('--run'), debug: argv.includes('--debug') || Boolean(process.env.DEBUG?.split(',').some(value => value === '*' || value === 'deploybuddy')), ...(base ? { base } : {}), ...(from ? { from } : {}), ...(to ? { to } : {}) };
|
|
86
|
+
}
|
|
87
|
+
function color(text, ansi, enabled) { return enabled ? `\u001b[${ansi}m${text}\u001b[0m` : text; }
|
|
88
|
+
function green(text, enabled) { return color(text, '32', enabled); }
|
|
89
|
+
function yellow(text, enabled) { return color(text, '33', enabled); }
|
|
90
|
+
function red(text, enabled) { return color(text, '31', enabled); }
|
|
91
|
+
function mark(text, enabled, level = 'good') { return level === 'bad' ? red(text, enabled) : level === 'warn' ? yellow(text, enabled) : green(text, enabled); }
|
|
92
|
+
// eslint-disable-next-line no-control-regex
|
|
93
|
+
function terminalText(value) { return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, '').slice(0, 240); }
|
|
94
|
+
const verdictDisplay = (risk) => risk.verdict.replaceAll('_', ' ');
|
|
95
|
+
async function getAssessment(adapter = new NodeWorkspaceAdapter_1.NodeWorkspaceAdapter()) {
|
|
96
|
+
const signals = await (0, core_1.collectWorkspaceSignals)(adapter);
|
|
97
|
+
const risk = (0, core_1.calculateDeploymentRisk)(signals);
|
|
98
|
+
const confidence = (0, core_1.calculateEvidenceConfidence)(signals);
|
|
99
|
+
const policyParse = signals.policy ?? await adapter.getPolicy?.() ?? (0, core_1.parseDeployBuddyPolicy)(undefined);
|
|
100
|
+
const policy = (0, core_1.evaluatePolicy)(policyParse, { risk, confidence, git: signals.git, diagnostics: signals.diagnostics, tests: signals.tests, checks: signals.checks ?? [], project: signals.project });
|
|
101
|
+
const files = await adapter.getChangedFiles?.();
|
|
102
|
+
const changeAssessment = files ? (0, core_1.analyzeChanges)(files) : undefined;
|
|
103
|
+
const recommendations = (0, core_1.getDeploymentRecommendations)(signals);
|
|
104
|
+
const readiness = (0, core_1.evaluateReleaseReadiness)({ risk, confidence, policy, changes: changeAssessment, signals, recommendations });
|
|
105
|
+
const releaseGates = (0, core_1.evaluateReleaseGates)({ config: policyParse.policy?.release, risk, confidence, policy, changes: changeAssessment, readiness });
|
|
106
|
+
return { signals, risk, confidence, recommendations, policy, readiness, releaseGates, policyParse, ...(changeAssessment ? { changeAssessment } : {}) };
|
|
107
|
+
}
|
|
108
|
+
function jsonReport(assessment) {
|
|
109
|
+
const { signals: s, risk, confidence, recommendations } = assessment;
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
risk: { score: risk.score, verdict: risk.verdict, level: risk.level },
|
|
112
|
+
confidence: { score: confidence.score, level: confidence.level },
|
|
113
|
+
project: { frameworks: s.project?.frameworks ?? [] },
|
|
114
|
+
...(s.git.revision ? { revision: s.git.revision } : {}),
|
|
115
|
+
...(assessment.changeAssessment ? { changes: changeSummaryJSON(assessment.changeAssessment) } : {}),
|
|
116
|
+
git: { available: s.git.available, branch: s.git.branch, upstream: s.git.upstream, ahead: s.git.ahead, behind: s.git.behind, dirty: s.git.hasUncommittedChanges, staged: s.git.available ? s.git.stagedFiles : null, unstaged: s.git.unstagedFiles, untracked: s.git.available ? s.git.untrackedFiles : null, conflicts: s.git.conflictFiles, detachedHead: s.git.detachedHead },
|
|
117
|
+
checks: s.checks ?? [],
|
|
118
|
+
recommendations,
|
|
119
|
+
policy: assessment.policy,
|
|
120
|
+
readiness: assessment.readiness,
|
|
121
|
+
releaseGates: assessment.releaseGates,
|
|
122
|
+
}, null, 2);
|
|
123
|
+
}
|
|
124
|
+
function changeSummaryJSON(assessment) {
|
|
125
|
+
return { totalFiles: assessment.summary.totalFiles, attention: assessment.attention.level, categories: Object.fromEntries(assessment.summary.categories.map(item => [item.category, item.count])), findings: assessment.summary.findings.map(item => item.code) };
|
|
126
|
+
}
|
|
127
|
+
function changeReport(assessment, json = false) {
|
|
128
|
+
if (json)
|
|
129
|
+
return JSON.stringify({ totalFiles: assessment.summary.totalFiles, attention: assessment.attention.level, categories: Object.fromEntries(assessment.summary.categories.map(item => [item.category, item.count])), findings: assessment.summary.findings.map(item => item.code), reasons: assessment.attention.reasons }, null, 2);
|
|
130
|
+
const label = (value) => value[0].toUpperCase() + value.slice(1);
|
|
131
|
+
return ['DeployBuddy Changes', '', `Changed files: ${assessment.summary.totalFiles}`, '', 'Categories:', ...(assessment.summary.categories.length ? assessment.summary.categories.map(item => `${label(item.category).padEnd(16)} ${item.count}`) : ['None detected.']), '', `Change attention: ${assessment.attention.level.toUpperCase()}`, '', 'Change findings:', ...(assessment.summary.findings.length ? assessment.summary.findings.map(item => `${item.severity === 'warning' ? '⚠' : '✓'} ${item.title}`) : ['No change findings.']), '', ...assessment.attention.reasons.map(reason => `- ${reason}`), '', 'No source code was inspected.'].join('\n');
|
|
132
|
+
}
|
|
133
|
+
function checkReport(assessment, useColor) {
|
|
134
|
+
const { signals: s, risk, confidence, recommendations } = assessment;
|
|
135
|
+
const uncertainSafe = risk.level === 'safe' && confidence.level === 'low';
|
|
136
|
+
const lines = ['DeployBuddy', '', 'Project:', s.project?.frameworks.length ? s.project.frameworks.map(labelFramework).join(' + ') : 'Unknown', '', 'Branch:', s.git.available ? terminalText(s.git.branch ?? 'Detached HEAD / unborn') : 'Unavailable', ...(s.git.revision ? [`Commit: ${terminalText(s.git.revision.shortSha ?? s.git.revision.sha ?? '-')}`] : []), '', `${uncertainSafe ? 'Observed risk' : 'Risk'}: ${risk.score}% — ${verdictDisplay(risk)}`, `Confidence: ${confidence.score}% — ${confidence.level.toUpperCase()}`, ''];
|
|
137
|
+
const blockers = risk.reasons.filter(item => ['errors', 'many-errors', 'failed-tests', 'primary-branch'].includes(item.code));
|
|
138
|
+
const cautions = risk.reasons.filter(item => !blockers.includes(item));
|
|
139
|
+
const findings = (0, core_1.analyzeGitGuardrails)(s.git);
|
|
140
|
+
const dangerous = findings.filter(item => item.severity === 'danger');
|
|
141
|
+
const blockerLines = dangerous.length ? dangerous.map(item => mark(`✗ ${terminalText(item.detail)}`, useColor, 'bad')) : blockers.map(item => mark(`✗ ${item.message}`, useColor, 'bad'));
|
|
142
|
+
lines.push('BLOCKERS', ...(blockerLines.length ? blockerLines : ['None detected.']));
|
|
143
|
+
const warningLines = findings.filter(item => item.severity === 'warning').map(item => mark(`! ${terminalText(item.detail)}`, useColor, 'warn'));
|
|
144
|
+
const cautionLines = [...cautions.map(item => mark(`! ${item.message}`, useColor, 'warn')), ...warningLines];
|
|
145
|
+
lines.push('', 'CAUTIONS', ...(cautionLines.length ? cautionLines : ['None detected.']));
|
|
146
|
+
const positives = [];
|
|
147
|
+
if (s.git.available && !s.git.hasUncommittedChanges)
|
|
148
|
+
positives.push('Git working tree clean');
|
|
149
|
+
if (s.checks?.some(item => item.kind === 'build' && item.status === 'available'))
|
|
150
|
+
positives.push('Build available');
|
|
151
|
+
lines.push('', 'LOOKS GOOD', ...(positives.length ? positives.map(item => mark(`✓ ${item}`, useColor)) : ['No positive checks confirmed.']));
|
|
152
|
+
const unknown = [];
|
|
153
|
+
if (s.tests.testStatus !== 'known')
|
|
154
|
+
unknown.push('Tests have not been verified');
|
|
155
|
+
if (!s.diagnostics.available)
|
|
156
|
+
unknown.push('Workspace diagnostics unavailable in the CLI');
|
|
157
|
+
lines.push('', 'UNKNOWN', ...(unknown.length ? unknown.map(item => `? ${item}`) : ['No unknown signals.']));
|
|
158
|
+
if (s.project?.parseErrors.length)
|
|
159
|
+
lines.push('', 'Project manifest warnings', ...s.project.parseErrors.map(item => `! ${item}`));
|
|
160
|
+
const personality = risk.level === 'safe' ? 'Suspiciously responsible. Carry on.' : risk.level === 'caution' ? 'Technically deployable. Emotionally complicated.' : 'Production has reviewed your application and respectfully declined.';
|
|
161
|
+
lines.push('', 'Policy:', !assessment.policy.configured ? 'Not configured' : assessment.policy.passed ? 'PASSED' : 'FAILED');
|
|
162
|
+
lines.push('', 'Release Readiness:', assessment.readiness.status.replaceAll('_', ' ').toUpperCase(), '', 'Release Gates:', assessment.releaseGates.status.replaceAll('_', ' ').toUpperCase());
|
|
163
|
+
if (assessment.releaseGates.violations.length)
|
|
164
|
+
lines.push('Gate violations:', ...assessment.releaseGates.violations.map(item => `- ${terminalText(item.detail)}`));
|
|
165
|
+
if (assessment.readiness.reasons.length)
|
|
166
|
+
lines.push('Why:', ...assessment.readiness.reasons.map(item => `- ${terminalText(item.detail)}`));
|
|
167
|
+
if (assessment.policy.violations.length)
|
|
168
|
+
lines.push('Policy violations:', ...assessment.policy.violations.map(item => mark(`✗ ${terminalText(item.detail)}`, useColor, 'bad')));
|
|
169
|
+
if (assessment.policy.configured && !assessment.policy.passed)
|
|
170
|
+
lines.push('', risk.level === 'safe'
|
|
171
|
+
? 'Observed deployment risk is low, but this repository does not currently satisfy its deployment policy.'
|
|
172
|
+
: `Observed deployment risk is ${risk.score}%, and this repository does not currently satisfy its deployment policy.`);
|
|
173
|
+
if (assessment.changeAssessment)
|
|
174
|
+
lines.push('', 'Change attention:', assessment.changeAssessment.attention.level.toUpperCase(), ...assessment.changeAssessment.summary.categories.map(item => `- ${item.category}: ${item.count}`), ...assessment.changeAssessment.attention.reasons.map(reason => `- ${reason}`));
|
|
175
|
+
lines.push('', 'Next:', ...assessment.readiness.recommendedActions.slice(0, 3).map((action, index) => `${index + 1}. ${action}`), '', 'Recommendation:', recommendations[0]?.detail ?? risk.advice, '', (0, core_1.getConfidenceAwareMessage)(risk.level, confidence.level, personality));
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
function labelFramework(value) { return { 'laravel': 'Laravel', 'vite': 'Vite', 'react': 'React', 'vue': 'Vue', 'next.js': 'Next.js', 'nuxt': 'Nuxt', 'svelte': 'Svelte', 'angular': 'Angular', 'express': 'Express', 'nestjs': 'NestJS', 'astro': 'Astro' }[value] ?? value; }
|
|
179
|
+
function statusReport(assessment) {
|
|
180
|
+
const s = assessment.signals;
|
|
181
|
+
const check = (kind) => s.checks?.find(item => item.kind === kind)?.status === 'available' ? 'Available' : s.checks?.find(item => item.kind === kind)?.status === 'unavailable' ? 'Unavailable' : 'Unknown';
|
|
182
|
+
const uncertainSafe = assessment.risk.level === 'safe' && assessment.confidence.level === 'low';
|
|
183
|
+
return [`${uncertainSafe ? 'Observed risk' : 'Risk'}: ${assessment.risk.score}%`, `Confidence: ${assessment.confidence.score}%`, `Verdict: ${verdictDisplay(assessment.risk)}`, `Release Readiness: ${assessment.readiness.status.toUpperCase()}`, `Release Gates: ${assessment.releaseGates.status.toUpperCase()}`, `Policy: ${!assessment.policy.configured ? 'Not configured' : assessment.policy.passed ? 'PASS' : `${assessment.policy.violations.length} violations`}`, ...(uncertainSafe ? [(0, core_1.getConfidenceAwareMessage)(assessment.risk.level, assessment.confidence.level, assessment.risk.message)] : []), `Frameworks: ${s.project?.frameworks.map(labelFramework).join(', ') || 'None detected'}`, `Git: ${!s.git.available ? 'Unavailable' : s.git.hasUncommittedChanges ? 'Dirty' : 'Clean'}`, `Tests: ${s.tests.testStatus === 'known' ? 'Known results' : 'Unknown'}`, `Build: ${check('build')}`].join('\n');
|
|
184
|
+
}
|
|
185
|
+
function readinessReport(assessment, json) {
|
|
186
|
+
if (json)
|
|
187
|
+
return JSON.stringify(assessment.readiness, null, 2);
|
|
188
|
+
return ['Release Readiness', '', 'Status:', assessment.readiness.status.replaceAll('_', ' ').toUpperCase(), '', 'Reasons:', ...(assessment.readiness.reasons.length ? assessment.readiness.reasons.map(reason => `- ${reason.detail}`) : ['No additional concerns.']), '', 'Recommended actions:', ...assessment.readiness.recommendedActions.map((action, index) => `${index + 1}. ${action}`)].join('\n');
|
|
189
|
+
}
|
|
190
|
+
function releaseReport(assessment, json) {
|
|
191
|
+
if (json)
|
|
192
|
+
return JSON.stringify(assessment.releaseGates, null, 2);
|
|
193
|
+
const gates = assessment.releaseGates;
|
|
194
|
+
if (!gates.configured)
|
|
195
|
+
return 'DeployBuddy Release Gate\n\nStatus:\nNOT CONFIGURED\n\nNo release gates are configured in .deploybuddy.json.';
|
|
196
|
+
return ['DeployBuddy Release Gate', '', 'Status:', gates.status.toUpperCase(), '', `Readiness: ${assessment.readiness.status.replaceAll('_', ' ').toUpperCase()}`, '', 'Gate violations:', ...(gates.violations.length ? gates.violations.map((item, index) => `${index + 1}. ${terminalText(item.detail)}`) : ['None.'])].join('\n');
|
|
197
|
+
}
|
|
198
|
+
function policyReport(assessment, json) {
|
|
199
|
+
if (json)
|
|
200
|
+
return JSON.stringify(assessment.policy, null, 2);
|
|
201
|
+
const source = assessment.policy.configured ? '.deploybuddy.json' : 'Not configured';
|
|
202
|
+
return ['DeployBuddy Policy', '', `Source: ${source}`, `Status: ${!assessment.policy.configured ? 'NOT CONFIGURED' : assessment.policy.passed ? 'PASSED' : 'FAILED'}`, `Rules evaluated: ${assessment.policy.rulesEvaluated}`, `Violations: ${assessment.policy.violations.length}`, ...(assessment.policy.violations.length ? assessment.policy.violations.map(item => `- ${terminalText(item.title)}: ${terminalText(item.detail)}`) : ['No policy violations.'])].join('\n');
|
|
203
|
+
}
|
|
204
|
+
function formatDelta(previous, current, delta) {
|
|
205
|
+
const signed = (value) => `${value > 0 ? '+' : ''}${value}`;
|
|
206
|
+
const value = (item) => item === null ? 'unknown' : String(item);
|
|
207
|
+
const policy = (value) => value === undefined ? 'Not configured' : value ? 'PASS' : 'FAIL';
|
|
208
|
+
return ['DeployBuddy Delta', '', `Risk: ${previous.risk.score}% → ${current.risk.score}% (${signed(delta.riskDelta)})`, `Confidence: ${previous.confidence.score}% → ${current.confidence.score}% (${signed(delta.confidenceDelta)})`, `Policy: ${policy(delta.previousPolicyPassed)} → ${policy(delta.currentPolicyPassed)}`, ...(delta.releaseGatesChanged ? [`Release Gates: ${delta.previousReleaseGatesStatus?.toUpperCase()} → ${delta.currentReleaseGatesStatus?.toUpperCase()}`] : []), '', 'Evidence changed:', ...(delta.changedSignals.length ? delta.changedSignals.map(item => `- ${item.code}: ${value(item.previous)} → ${value(item.current)}`) : ['None.']), '', 'New concerns:', ...(delta.addedFindings.length ? delta.addedFindings.map(item => `- ${item.title}`) : ['None.']), '', 'Resolved:', ...(delta.resolvedFindings.length ? delta.resolvedFindings.map(item => `- ${item.title}`) : ['None.']), '', `Overall: Deployment posture ${delta.posture.toLowerCase()}.`].join('\n');
|
|
209
|
+
}
|
|
210
|
+
function jsonDelta(previous, current, delta) {
|
|
211
|
+
return JSON.stringify({ schemaVersion: 1, baseline: { risk: previous.risk, confidence: previous.confidence, policy: previous.policy }, current: { risk: current.risk, confidence: current.confidence, policy: current.policy }, delta }, null, 2);
|
|
212
|
+
}
|
|
213
|
+
function directionLabel(direction) { return direction === 'insufficient-data' ? 'Insufficient data' : direction[0].toUpperCase() + direction.slice(1); }
|
|
214
|
+
function formatHistoryStats(summary) {
|
|
215
|
+
return ['Summary', `Assessments: ${summary.count}`, `Average risk: ${summary.averageRisk === null ? 'Unknown' : `${summary.averageRisk}%`}`, `Highest risk: ${summary.highestRisk === null ? 'Unknown' : `${summary.highestRisk}%`}`, `Lowest risk: ${summary.lowestRisk === null ? 'Unknown' : `${summary.lowestRisk}%`}`, `Latest risk: ${summary.latestRisk === null ? 'Unknown' : `${summary.latestRisk}%`}`, '', `Average confidence: ${summary.averageConfidence === null ? 'Unknown' : `${summary.averageConfidence}%`}`, `Policy pass rate: ${summary.policyPassRate === null ? 'Not configured' : `${summary.policyPassRate}%`}`, `Release readiness pass rate: ${summary.readinessPassRate === null ? 'No readiness history' : `${summary.readinessPassRate}%`}`, `Release gate pass rate: ${summary.releaseGatePassRate === null ? 'No release gate history' : `${summary.releaseGatePassRate}%`}`, `Readiness: READY ${summary.readinessCounts.ready}, NEEDS ATTENTION ${summary.readinessCounts.needs_attention}, BLOCKED ${summary.readinessCounts.blocked}, INSUFFICIENT EVIDENCE ${summary.readinessCounts.insufficient_evidence}`, `Risk trend: ${directionLabel(summary.riskTrend)}`, `Confidence trend: ${directionLabel(summary.confidenceTrend)}`, '', 'Most common concerns:', ...(summary.frequentFindings.length ? summary.frequentFindings.map((item, index) => `${index + 1}. ${item.code} — ${item.count}`) : ['None recorded.'])].join('\n');
|
|
216
|
+
}
|
|
217
|
+
function formatHistory(entries, summary, limit, warning) {
|
|
218
|
+
const recent = entries.slice(-limit).reverse();
|
|
219
|
+
const date = (value) => value.slice(0, 16).replace('T', ' ');
|
|
220
|
+
const policy = (entry) => !entry.policy?.configured ? 'N/A' : entry.policy.passed ? 'PASS' : 'FAIL';
|
|
221
|
+
const rows = recent.map(entry => `${date(entry.recordedAt).padEnd(19)} ${(entry.revision?.shortSha ?? '-').padEnd(9)} ${`${entry.risk.score}%`.padEnd(6)} ${`${entry.confidence.score}%`.padEnd(12)} ${policy(entry)}`);
|
|
222
|
+
return ['DeployBuddy History', '', `Last ${recent.length} assessment${recent.length === 1 ? '' : 's'}`, '', 'Date Commit Risk Confidence Policy', ...rows, '', `Risk trend: ${directionLabel(summary.riskTrend)}`, `Confidence trend: ${directionLabel(summary.confidenceTrend)}`, '', formatHistoryStats(summary), ...(warning ? ['', `Warning: ${warning}`] : [])].join('\n');
|
|
223
|
+
}
|
|
224
|
+
function formatHistoryEntry(entry) {
|
|
225
|
+
return ['DeployBuddy Recorded Assessment', '', `ID: ${terminalText(entry.id)}`, `Recorded: ${terminalText(entry.recordedAt)}`, `Commit: ${terminalText(entry.revision?.shortSha ?? entry.revision?.sha ?? '-')}`, `Branch: ${terminalText(entry.revision?.branch ?? '-')}`, `Detached HEAD: ${entry.revision?.detached === undefined ? 'Unknown' : entry.revision.detached ? 'Yes' : 'No'}`, `Risk: ${entry.risk.score}% — ${entry.risk.verdict}`, `Confidence: ${entry.confidence.score}% — ${entry.confidence.level.toUpperCase()}`, `Policy: ${!entry.policy?.configured ? 'Not configured' : entry.policy.passed ? 'PASS' : 'FAIL'}`, `Frameworks: ${entry.profile?.frameworks.map(labelFramework).join(', ') || 'Unknown'}`, `Signals: Git dirty ${entry.signals.gitDirty === null ? 'unknown' : entry.signals.gitDirty ? 'yes' : 'no'}; errors ${entry.signals.errors ?? 'unknown'}; warnings ${entry.signals.warnings ?? 'unknown'}; tests ${entry.signals.testsKnown ? 'known' : 'unknown'}; build ${entry.signals.buildKnown ? 'known' : 'unknown'}; lint ${entry.signals.lintKnown ? 'known' : 'unknown'}`, `Findings: ${entry.findings.map(terminalText).join(', ') || 'None'}`].join('\n');
|
|
226
|
+
}
|
|
227
|
+
function formatRiskChanges(entries) {
|
|
228
|
+
const jumps = (0, core_1.findMaterialRiskChanges)(entries);
|
|
229
|
+
return ['Deployment posture changes', '', ...(jumps.length ? jumps.slice().reverse().map((item, index) => {
|
|
230
|
+
const from = entries.find(entry => entry.id === item.fromEntryId);
|
|
231
|
+
const to = entries.find(entry => entry.id === item.toEntryId);
|
|
232
|
+
return [`${index + 1}. Risk ${item.riskDelta > 0 ? `increased +${item.riskDelta}` : `decreased ${item.riskDelta}`}`, ` ${item.fromRevision ?? '-'} → ${item.toRevision ?? '-'}`, ` ${from.risk.score}% → ${to.risk.score}%`, ...(item.addedFindings.length ? [' New:', ...item.addedFindings.map(code => ` - ${code}`)] : []), ...(item.resolvedFindings.length ? [' Resolved:', ...item.resolvedFindings.map(code => ` - ${code}`)] : []), ...(item.policyChanged ? [` Policy: ${from.policy?.passed ? 'PASS' : 'FAIL'} → ${to.policy?.passed ? 'PASS' : 'FAIL'}`] : [])].join('\n');
|
|
233
|
+
}) : [`No material risk changes (${core_1.MATERIAL_RISK_CHANGE_THRESHOLD} points or more) are recorded.`])].join('\n');
|
|
234
|
+
}
|
|
235
|
+
async function recordHistory(assessment, adapter, io) {
|
|
236
|
+
try {
|
|
237
|
+
const entry = (0, core_1.createAssessmentHistoryEntry)(assessment, (0, node_crypto_1.randomUUID)());
|
|
238
|
+
const result = await (0, historyStore_1.appendLocalHistory)(adapter.cwd, entry);
|
|
239
|
+
if (result.warning)
|
|
240
|
+
io.writeError(`DeployBuddy: ${result.warning}\n`);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
io.writeError('DeployBuddy could not save this assessment to local history; the check result is unaffected.\n');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function gitReport(signals, useColor) {
|
|
247
|
+
const git = signals.git;
|
|
248
|
+
const guardrails = (0, core_1.analyzeGitGuardrails)(git);
|
|
249
|
+
const count = (value) => value === null ? 'Unknown' : String(value);
|
|
250
|
+
const guardrailLines = guardrails.map(item => `${item.severity === 'danger' ? '✗' : item.severity === 'warning' ? '!' : '✓'} ${item.title}: ${item.detail}`);
|
|
251
|
+
if (!guardrailLines.length)
|
|
252
|
+
guardrailLines.push('✓ Branch synchronized enough for review.', '✓ No conflicts.');
|
|
253
|
+
return ['Git Health', '', `Branch: ${git.available ? terminalText(git.branch ?? 'Detached HEAD / unborn') : 'Unavailable'}`, `Upstream: ${git.available ? terminalText(git.upstream ?? 'None configured') : 'Unknown'}`, '', `Ahead: ${count(git.ahead)}`, `Behind: ${count(git.behind)}`, '', 'Working tree:', !git.available ? 'Unknown' : git.hasUncommittedChanges ? 'Dirty' : 'Clean', `Staged: ${git.available ? git.stagedFiles : 'Unknown'}`, `Unstaged: ${count(git.unstagedFiles)}`, `Untracked: ${git.available ? git.untrackedFiles : 'Unknown'}`, `Conflicts: ${count(git.conflictFiles)}`, `Detached HEAD: ${git.detachedHead === null ? 'Unknown' : git.detachedHead ? 'Yes' : 'No'}`, '', 'Guardrails:', ...guardrailLines.map(line => { const safe = terminalText(line); return safe.startsWith('✗') ? red(safe, useColor) : safe.startsWith('!') ? yellow(safe, useColor) : green(safe, useColor); })].join('\n');
|
|
254
|
+
}
|
|
255
|
+
function frameworkReport(project) {
|
|
256
|
+
const frameworkLines = project.frameworks.length ? project.frameworks.map(item => `✓ ${labelFramework(item)}`) : ['? No framework markers detected.'];
|
|
257
|
+
const managerLines = project.packageManagers.length ? project.packageManagers.map(item => `✓ ${item}`) : ['? None detected.'];
|
|
258
|
+
return ['Detected project technologies:', '', ...frameworkLines, '', 'Package managers:', ...managerLines, '', 'Project roots:', ...(project.roots.length ? project.roots : ['(none detected)']), ...project.parseErrors.map(item => `Warning: ${item}`)].join('\n');
|
|
259
|
+
}
|
|
260
|
+
function checksReport(checks) {
|
|
261
|
+
return checks.map(item => `${capitalize(item.kind)}:\n${item.status.toUpperCase()}${item.command ? ` — ${terminalText(item.command)}` : ''}`).join('\n\n');
|
|
262
|
+
}
|
|
263
|
+
function capitalize(value) { return value[0]?.toUpperCase() + value.slice(1); }
|
|
264
|
+
function doctorReport(assessment, configValid, useColor) {
|
|
265
|
+
const s = assessment.signals;
|
|
266
|
+
const hasPackage = (s.project?.roots.length ?? 0) > 0;
|
|
267
|
+
const parseFailed = (s.project?.parseErrors.length ?? 0) > 0;
|
|
268
|
+
const hasLaravel = s.project?.frameworks.includes('laravel') ?? false;
|
|
269
|
+
const hasVite = s.project?.frameworks.includes('vite') ?? false;
|
|
270
|
+
const testsKnown = s.tests.testStatus === 'known';
|
|
271
|
+
const manifestState = parseFailed ? '! Project manifest parsing failed' : hasPackage ? '✓ Project manifests parsed' : '? No recognized project manifest';
|
|
272
|
+
const lines = ['DeployBuddy Doctor', '', `${mark(s.git.available ? '✓ Git available' : '! Git unavailable', useColor, s.git.available ? 'good' : 'warn')}`, `${mark(s.git.available ? '✓ Repository detected' : '! No Git repository detected', useColor, s.git.available ? 'good' : 'warn')}`, '✓ Node.js available', `${mark(manifestState, useColor, hasPackage && !parseFailed ? 'good' : 'warn')}`];
|
|
273
|
+
if (hasLaravel)
|
|
274
|
+
lines.push(mark('✓ Laravel detected', useColor));
|
|
275
|
+
if (hasVite)
|
|
276
|
+
lines.push(mark('✓ Vite detected', useColor));
|
|
277
|
+
const policyValid = assessment.policyParse.valid;
|
|
278
|
+
lines.push(`${mark(configValid ? '✓ Configuration valid' : '! Configuration invalid; defaults are active', useColor, configValid ? 'good' : 'warn')}`, `${mark(policyValid ? '✓ Repository policy valid' : '! Repository policy invalid', useColor, policyValid ? 'good' : 'warn')}`, `${mark(testsKnown ? '✓ Test results available' : '! Test results unavailable', useColor, testsKnown ? 'good' : 'warn')}`, mark(configValid && policyValid ? '✓ No dangerous DeployBuddy configuration' : '! Review .deploybuddy.json before relying on its options', useColor, configValid && policyValid ? 'good' : 'warn'));
|
|
279
|
+
for (const error of assessment.policyParse.errors)
|
|
280
|
+
lines.push(`! Policy ${error.path}: ${error.message}`);
|
|
281
|
+
for (const item of s.project?.parseErrors ?? [])
|
|
282
|
+
lines.push(`! ${item}`);
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
async function runDetectedChecks(checks, adapter, io) {
|
|
286
|
+
if (!io.isTTY || !(await io.confirm('Run project scripts now? This executes commands declared in project manifests. [y/N] '))) {
|
|
287
|
+
io.writeError('Checks were not run. Use an interactive terminal and confirm when prompted.\n');
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
let exitCode = 0;
|
|
291
|
+
for (const check of checks) {
|
|
292
|
+
if (check.status !== 'available' || !check.command)
|
|
293
|
+
continue;
|
|
294
|
+
const npmMatch = /^npm run ([a-z0-9][a-z0-9:_-]*)$/i.exec(check.command);
|
|
295
|
+
const composerMatch = /^composer ([a-z0-9][a-z0-9:_-]*)$/i.exec(check.command);
|
|
296
|
+
const artisanTests = check.command === 'php artisan test';
|
|
297
|
+
const program = npmMatch ? 'npm' : composerMatch ? 'composer' : artisanTests ? 'php' : undefined;
|
|
298
|
+
const args = npmMatch ? ['run', npmMatch[1] ?? ''] : composerMatch ? ['run-script', composerMatch[1] ?? ''] : artisanTests ? ['artisan', 'test'] : undefined;
|
|
299
|
+
if (!program || !args) {
|
|
300
|
+
io.writeError(`Skipping an unsupported check command: ${check.command}\n`);
|
|
301
|
+
exitCode = 2;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
io.write(`\nRunning ${check.kind}: ${check.command}\n`);
|
|
305
|
+
try {
|
|
306
|
+
const result = io.execute ? await io.execute(program, args, adapter.cwd) : await execFile(program, args, { cwd: adapter.cwd, maxBuffer: 4 * 1024 * 1024, timeout: 15 * 60 * 1000, windowsHide: true });
|
|
307
|
+
io.write(result.stdout + result.stderr);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
const value = error;
|
|
311
|
+
io.write((value.stdout ?? '') + (value.stderr ?? ''));
|
|
312
|
+
io.writeError(`${check.kind} failed${typeof value.code === 'number' ? ` with exit code ${value.code}` : ''}.\n`);
|
|
313
|
+
exitCode = 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return exitCode;
|
|
317
|
+
}
|
|
318
|
+
const usage = 'DeployBuddy CLI\n\nUsage: deploybuddy <check|status|git|framework|doctor|checks|policy|baseline|compare|history|changes|readiness|release> [--json] [--quiet] [--no-color]\n deploybuddy checks --run\n deploybuddy readiness [--json]\n deploybuddy release [--json]\n deploybuddy policy [--json]\n deploybuddy policy validate\n deploybuddy policy init\n deploybuddy baseline save|show|clear\n deploybuddy compare [--json]\n deploybuddy compare --head [--json]\n deploybuddy history [--limit 20] [--json]\n deploybuddy history stats [--json]\n deploybuddy history show <id-or-sha> [--json]\n deploybuddy history commit <sha> [--json]\n deploybuddy history changes [--json]\n deploybuddy history clear [--yes]\n';
|
|
319
|
+
async function runCLI(argv, adapter = new NodeWorkspaceAdapter_1.NodeWorkspaceAdapter(), io = {
|
|
320
|
+
isTTY: Boolean(node_process_1.stdout.isTTY), write: text => node_process_1.stdout.write(text), writeError: text => node_process_1.stderr.write(text),
|
|
321
|
+
confirm: async (message) => {
|
|
322
|
+
const rl = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
323
|
+
try {
|
|
324
|
+
return /^y(es)?$/i.test((await rl.question(message)).trim());
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
rl.close();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}) {
|
|
331
|
+
let args;
|
|
332
|
+
try {
|
|
333
|
+
args = parseArgs(argv);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
io.writeError(`${terminalText(error instanceof Error ? error.message : String(error))}\n`);
|
|
337
|
+
return 2;
|
|
338
|
+
}
|
|
339
|
+
adapter.beginCollection?.();
|
|
340
|
+
const useColor = io.isTTY && !args.noColor && !process.env.NO_COLOR;
|
|
341
|
+
if (args.command === 'help') {
|
|
342
|
+
io.write(usage);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
if ((args.json && !['check', 'policy', 'compare', 'baseline', 'history', 'changes', 'readiness', 'release'].includes(args.command)) || (args.quiet && args.command !== 'check')) {
|
|
346
|
+
io.writeError('--json is supported by check, policy, baseline, compare, history, changes, readiness and release; --quiet is supported by check.\n');
|
|
347
|
+
return 2;
|
|
348
|
+
}
|
|
349
|
+
if (args.run && args.command !== 'checks') {
|
|
350
|
+
io.writeError('--run is supported only by `deploybuddy checks`.\n');
|
|
351
|
+
return 2;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
if (args.command === 'baseline') {
|
|
355
|
+
if (args.baselineAction === 'clear') {
|
|
356
|
+
await (0, baselineStore_1.clearLocalBaseline)(adapter.cwd);
|
|
357
|
+
io.write('Local baseline cleared.\n');
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
if (args.baselineAction === 'save') {
|
|
361
|
+
const assessment = await getAssessment(adapter);
|
|
362
|
+
const baseline = (0, core_1.createAssessmentBaseline)(assessment);
|
|
363
|
+
await (0, baselineStore_1.saveLocalBaseline)(adapter.cwd, baseline);
|
|
364
|
+
io.write(`Baseline saved locally for this project (${baseline.risk.score}% risk, ${baseline.confidence.score}% confidence).\n`);
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
const stored = await (0, baselineStore_1.readLocalBaseline)(adapter.cwd);
|
|
368
|
+
if (!stored) {
|
|
369
|
+
io.write(args.json ? `${JSON.stringify({ configured: false, baseline: null })}\n` : 'No local baseline saved for this project. Run `deploybuddy baseline save`.\n');
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
const output = stored.status === 'valid' ? `DeployBuddy Baseline\n\nSaved: ${stored.baseline.recordedAt}\nRisk: ${stored.baseline.risk.score}% — ${stored.baseline.risk.verdict}\nConfidence: ${stored.baseline.confidence.score}% — ${stored.baseline.confidence.level.toUpperCase()}\nPolicy: ${stored.baseline.policy.configured ? stored.baseline.policy.passed ? 'PASS' : 'FAIL' : 'Not configured'}\n` : `${stored.message} Use deploybuddy baseline clear to reset it.\n`;
|
|
373
|
+
io.write(args.json ? `${JSON.stringify({ configured: true, ...stored })}\n` : output);
|
|
374
|
+
return stored.status === 'invalid' || stored.status === 'unsupported' ? 2 : 0;
|
|
375
|
+
}
|
|
376
|
+
if (args.command === 'compare') {
|
|
377
|
+
if (args.head) {
|
|
378
|
+
const assessment = await getAssessment(adapter);
|
|
379
|
+
const current = (0, core_1.createAssessmentHistoryEntry)(assessment, (0, node_crypto_1.randomUUID)());
|
|
380
|
+
const stored = await (0, historyStore_1.readLocalHistory)(adapter.cwd);
|
|
381
|
+
const currentRevision = current.revision?.sha ?? current.revision?.shortSha;
|
|
382
|
+
const sameCommit = currentRevision ? (0, core_1.findRevisionAssessment)(stored.entries, currentRevision) : { status: 'missing' };
|
|
383
|
+
const sameCommitIds = new Set(sameCommit.status === 'found' ? sameCommit.entries.map(entry => entry.id) : []);
|
|
384
|
+
const previous = currentRevision ? [...stored.entries].reverse().find(entry => {
|
|
385
|
+
const recorded = entry.revision?.sha ?? entry.revision?.shortSha;
|
|
386
|
+
return recorded !== undefined && !sameCommitIds.has(entry.id);
|
|
387
|
+
}) : undefined;
|
|
388
|
+
if (!previous) {
|
|
389
|
+
const payload = { schemaVersion: 1, available: false, message: currentRevision ? 'No prior assessment from a different recorded commit was found.' : 'Current Git revision is unavailable.' };
|
|
390
|
+
io.write(args.json ? `${JSON.stringify(payload, null, 2)}\n` : `${payload.message}\n`);
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
const comparison = (0, core_1.compareRevisionEntries)(previous, current);
|
|
394
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, available: true, previous, current, comparison }, null, 2)}\n` : [`Compared recorded commit ${previous.revision?.shortSha ?? previous.revision?.sha ?? '-'} with current HEAD ${current.revision?.shortSha ?? current.revision?.sha ?? '-'}.`, `Risk: ${previous.risk.score}% → ${current.risk.score}% (${comparison.riskDelta > 0 ? '+' : ''}${comparison.riskDelta})`, `Confidence: ${previous.confidence.score}% → ${current.confidence.score}% (${comparison.confidenceDelta > 0 ? '+' : ''}${comparison.confidenceDelta})`, `Policy: ${previous.policy?.passed ? 'PASS' : 'FAIL'} → ${current.policy?.passed ? 'PASS' : 'FAIL'}`, `New findings: ${comparison.addedFindings.join(', ') || 'None'}`, `Resolved findings: ${comparison.resolvedFindings.join(', ') || 'None'}`].join('\n') + '\n');
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
const stored = await (0, baselineStore_1.readLocalBaseline)(adapter.cwd);
|
|
398
|
+
if (!stored) {
|
|
399
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, available: false, message: 'No local baseline is saved.' })}\n` : 'No local baseline saved for this project. Run `deploybuddy baseline save` first.\n');
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
if (stored.status !== 'valid') {
|
|
403
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, available: false, baselineStatus: stored.status, message: stored.message })}\n` : `${stored.message} Use deploybuddy baseline clear to reset it.\n`);
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
406
|
+
const assessment = await getAssessment(adapter);
|
|
407
|
+
const current = (0, core_1.createAssessmentBaseline)(assessment);
|
|
408
|
+
const delta = (0, core_1.compareAssessments)(stored.baseline, current);
|
|
409
|
+
io.write(`${args.json ? jsonDelta(stored.baseline, current, delta) : formatDelta(stored.baseline, current, delta)}\n`);
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
if (args.command === 'history') {
|
|
413
|
+
if (args.historyAction === 'clear') {
|
|
414
|
+
if (!args.yes && (!io.isTTY || !(await io.confirm('Clear all local DeployBuddy history for this project? [y/N] ')))) {
|
|
415
|
+
io.writeError('History was not cleared. Use an interactive confirmation or pass --yes.\n');
|
|
416
|
+
return 2;
|
|
417
|
+
}
|
|
418
|
+
await (0, historyStore_1.clearLocalHistory)(adapter.cwd);
|
|
419
|
+
io.write('Local DeployBuddy history cleared.\n');
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
const stored = await (0, historyStore_1.readLocalHistory)(adapter.cwd);
|
|
423
|
+
if (args.historyAction === 'show') {
|
|
424
|
+
const query = args.historyQuery ?? '';
|
|
425
|
+
const byId = stored.entries.find(entry => entry.id === query);
|
|
426
|
+
if (byId) {
|
|
427
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, entry: byId }, null, 2)}\n` : `${formatHistoryEntry(byId)}\n`);
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
const lookup = (0, core_1.findRevisionAssessment)(stored.entries, query);
|
|
431
|
+
if (lookup.status === 'ambiguous') {
|
|
432
|
+
io.writeError(`Revision prefix ${query} is ambiguous; use a longer SHA.\n`);
|
|
433
|
+
return 2;
|
|
434
|
+
}
|
|
435
|
+
if (lookup.status === 'missing') {
|
|
436
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, found: false })}\n` : `No assessment matches ID or revision ${query}.\n`);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
const entry = lookup.entries.at(-1);
|
|
440
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, entry }, null, 2)}\n` : `${formatHistoryEntry(entry)}\n`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
if (args.historyAction === 'commit') {
|
|
444
|
+
const lookup = (0, core_1.findRevisionAssessment)(stored.entries, args.historyQuery ?? '');
|
|
445
|
+
if (lookup.status === 'ambiguous') {
|
|
446
|
+
io.writeError(`Revision prefix ${args.historyQuery} is ambiguous; use a longer SHA.\n`);
|
|
447
|
+
return 2;
|
|
448
|
+
}
|
|
449
|
+
const entries = lookup.status === 'found' ? lookup.entries : [];
|
|
450
|
+
io.write(args.json ? `${JSON.stringify({ schemaVersion: 1, revision: args.historyQuery, entries }, null, 2)}\n` : entries.length ? formatHistory(entries, (0, core_1.summarizeHistory)(entries), entries.length, stored.warning) + '\n' : `No local assessment is associated with revision ${args.historyQuery}.\n`);
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
if (args.historyAction === 'changes') {
|
|
454
|
+
const jumps = (0, core_1.findMaterialRiskChanges)(stored.entries);
|
|
455
|
+
const transitions = (0, core_1.findPolicyTransitions)(stored.entries);
|
|
456
|
+
const readinessTransitions = (0, core_1.findReadinessTransitions)(stored.entries);
|
|
457
|
+
const releaseGateTransitions = (0, core_1.findReleaseGateTransitions)(stored.entries);
|
|
458
|
+
const payload = { schemaVersion: 1, threshold: core_1.MATERIAL_RISK_CHANGE_THRESHOLD, riskChanges: jumps, policyTransitions: transitions, readinessTransitions, releaseGateTransitions };
|
|
459
|
+
if (args.json)
|
|
460
|
+
io.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
461
|
+
else {
|
|
462
|
+
io.write(`${formatRiskChanges(stored.entries)}\n`);
|
|
463
|
+
const startedFailing = transitions.find(item => item.transition === 'PASS_TO_FAIL');
|
|
464
|
+
if (startedFailing)
|
|
465
|
+
io.write(`\nPolicy started failing at: ${startedFailing.toRevision ?? '-'}\nViolations introduced: ${startedFailing.introducedViolations.join(', ') || 'Unknown'}\n`);
|
|
466
|
+
for (const transition of readinessTransitions)
|
|
467
|
+
io.write(`\nRelease readiness changed: ${transition.previous.toUpperCase()} → ${transition.current.toUpperCase()}\n`);
|
|
468
|
+
for (const transition of releaseGateTransitions)
|
|
469
|
+
io.write(`\nRelease gates changed: ${transition.previous.toUpperCase()} → ${transition.current.toUpperCase()}\n`);
|
|
470
|
+
}
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
const summary = (0, core_1.summarizeHistory)(stored.entries);
|
|
474
|
+
const entries = args.historyAction === 'stats' ? [] : stored.entries.slice(-args.limit).reverse();
|
|
475
|
+
const payload = { schemaVersion: 1, entries, summary, ...(stored.warning ? { warnings: [stored.warning] } : {}) };
|
|
476
|
+
io.write(args.json ? `${JSON.stringify(payload, null, 2)}\n` : args.historyAction === 'stats' ? `${formatHistoryStats(summary)}${stored.warning ? `\n\nWarning: ${stored.warning}` : ''}\n` : `${formatHistory(stored.entries, summary, args.limit, stored.warning)}\n`);
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
if (args.command === 'changes') {
|
|
480
|
+
const range = args.from && args.to ? { from: args.from, to: args.to } : args.base ? { base: args.base } : undefined;
|
|
481
|
+
const files = await adapter.getChangedFiles?.(range);
|
|
482
|
+
io.write(`${changeReport((0, core_1.analyzeChanges)(files ?? []), args.json)}\n`);
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
if (args.command === 'policy') {
|
|
486
|
+
if (args.policyAction === 'init') {
|
|
487
|
+
const target = node_path_1.default.join(adapter.cwd, '.deploybuddy.json');
|
|
488
|
+
let exists = true;
|
|
489
|
+
try {
|
|
490
|
+
await (0, promises_2.access)(target);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
exists = false;
|
|
494
|
+
}
|
|
495
|
+
if (exists && (!io.isTTY || !(await io.confirm(`Overwrite ${target}? This replaces the existing policy/configuration. [y/N] `)))) {
|
|
496
|
+
io.writeError(`Refusing to overwrite ${target} without confirmation.\n`);
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
const template = `${JSON.stringify({ version: 1, risk: { maxScore: 65 }, confidence: { minimumScore: 60 }, git: { requireClean: true } }, null, 2)}\n`;
|
|
500
|
+
await (0, promises_2.writeFile)(target, template, exists ? 'utf8' : { encoding: 'utf8', flag: 'wx' });
|
|
501
|
+
io.write(`Created policy template: ${target}\n`);
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
if (args.policyAction === 'validate') {
|
|
505
|
+
const parsed = await adapter.getPolicy?.() ?? (0, core_1.parseDeployBuddyPolicy)(undefined);
|
|
506
|
+
if (!parsed.configured) {
|
|
507
|
+
io.write('No repository deployment policy is configured.\n');
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
510
|
+
if (parsed.valid) {
|
|
511
|
+
io.write('✓ .deploybuddy.json is valid.\n');
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
io.write(`Invalid DeployBuddy policy:\n\n${parsed.errors.map(error => `${terminalText(error.path)}: ${terminalText(error.message)}`).join('\n')}\n`);
|
|
515
|
+
return 2;
|
|
516
|
+
}
|
|
517
|
+
const assessment = await getAssessment(adapter);
|
|
518
|
+
io.write(`${policyReport(assessment, args.json)}\n`);
|
|
519
|
+
return assessment.policy.configured && !assessment.policy.passed ? 1 : 0;
|
|
520
|
+
}
|
|
521
|
+
if (args.command === 'readiness') {
|
|
522
|
+
const assessment = await getAssessment(adapter);
|
|
523
|
+
io.write(`${readinessReport(assessment, args.json)}\n`);
|
|
524
|
+
return ['blocked', 'insufficient_evidence'].includes(assessment.readiness.status) ? 1 : 0;
|
|
525
|
+
}
|
|
526
|
+
if (args.command === 'release') {
|
|
527
|
+
const assessment = await getAssessment(adapter);
|
|
528
|
+
if (!assessment.policyParse.valid) {
|
|
529
|
+
io.write(args.json ? `${JSON.stringify({ error: { message: 'Repository configuration is invalid.', errors: assessment.policyParse.errors } })}\n` : `Invalid DeployBuddy policy/configuration:\n\n${assessment.policyParse.errors.map(error => `${terminalText(error.path)}: ${terminalText(error.message)}`).join('\n')}\n`);
|
|
530
|
+
return 2;
|
|
531
|
+
}
|
|
532
|
+
io.write(`${releaseReport(assessment, args.json)}\n`);
|
|
533
|
+
return assessment.releaseGates.status === 'fail' ? 1 : 0;
|
|
534
|
+
}
|
|
535
|
+
if (args.command === 'check' || args.command === 'status' || args.command === 'git') {
|
|
536
|
+
const assessment = await getAssessment(adapter);
|
|
537
|
+
if (args.command === 'check') {
|
|
538
|
+
await recordHistory(assessment, adapter, io);
|
|
539
|
+
if (args.json)
|
|
540
|
+
io.write(`${jsonReport(assessment)}\n`);
|
|
541
|
+
else if (args.quiet)
|
|
542
|
+
io.write(`${verdictDisplay(assessment.risk)} ${assessment.risk.score}\n`);
|
|
543
|
+
else
|
|
544
|
+
io.write(`${checkReport(assessment, useColor)}\n`);
|
|
545
|
+
return (assessment.risk.level === 'danger' && (assessment.signals.config?.exitOnDanger ?? true)) || (assessment.policy.configured && !assessment.policy.passed) ? 1 : 0;
|
|
546
|
+
}
|
|
547
|
+
io.write(args.command === 'status' ? `${statusReport(assessment)}\n` : `${gitReport(assessment.signals, useColor)}\n`);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
if (args.command === 'framework') {
|
|
551
|
+
io.write(`${frameworkReport(await adapter.getProjectProfile())}\n`);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
if (args.command === 'checks') {
|
|
555
|
+
const checks = await adapter.getCheckResults();
|
|
556
|
+
return args.run ? await runDetectedChecks(checks, adapter, io) : (io.write(`${checksReport(checks)}\n`), 0);
|
|
557
|
+
}
|
|
558
|
+
const assessment = await getAssessment(adapter);
|
|
559
|
+
const config = await adapter.getConfig?.();
|
|
560
|
+
const configValid = (config?.valid ?? true) && assessment.policyParse.valid;
|
|
561
|
+
io.write(`${doctorReport(assessment, configValid, useColor)}\n`);
|
|
562
|
+
return configValid ? 0 : 2;
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (args.json)
|
|
566
|
+
io.write(`${JSON.stringify({ error: { message: 'DeployBuddy could not inspect this project. Check file permissions and try again.', code: 'INSPECTION_FAILED' } })}\n`);
|
|
567
|
+
else {
|
|
568
|
+
io.writeError(`DeployBuddy could not inspect this project: ${terminalText(error instanceof Error ? error.message : String(error))}\n`);
|
|
569
|
+
if (args.debug)
|
|
570
|
+
io.writeError(error instanceof Error ? `${error.stack ?? ''}\n` : '');
|
|
571
|
+
}
|
|
572
|
+
return 2;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (require.main === module)
|
|
576
|
+
void runCLI(process.argv.slice(2)).then(code => { process.exitCode = code; });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_HISTORY_ENTRIES = void 0;
|
|
4
|
+
exports.getHistoryPath = getHistoryPath;
|
|
5
|
+
exports.readLocalHistory = readLocalHistory;
|
|
6
|
+
exports.appendLocalHistory = appendLocalHistory;
|
|
7
|
+
exports.clearLocalHistory = clearLocalHistory;
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const core_1 = require("@deploybuddy/core");
|
|
11
|
+
const baselineStore_1 = require("./baselineStore");
|
|
12
|
+
exports.MAX_HISTORY_ENTRIES = 100;
|
|
13
|
+
function getHistoryPath(cwd) { return (0, baselineStore_1.getBaselinePath)(cwd).replace(/\.json$/, '.history.json'); }
|
|
14
|
+
async function readLocalHistory(cwd) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = (0, core_1.parseAssessmentHistory)(JSON.parse(await (0, promises_1.readFile)(getHistoryPath(cwd), 'utf8')));
|
|
17
|
+
return { ...parsed, ...(parsed.invalidCount || parsed.unsupportedCount ? { warning: 'Some malformed or unsupported history entries were skipped.' } : {}) };
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT')
|
|
21
|
+
return { entries: [], invalidCount: 0, unsupportedCount: 0 };
|
|
22
|
+
return { entries: [], invalidCount: 1, unsupportedCount: 0, warning: 'DeployBuddy history storage is malformed and was ignored.' };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function appendLocalHistory(cwd, entry) {
|
|
26
|
+
const read = await readLocalHistory(cwd);
|
|
27
|
+
if (read.entries.some(item => item.id === entry.id))
|
|
28
|
+
return { ...read, duplicate: true };
|
|
29
|
+
const entries = [...read.entries, entry].sort((a, b) => a.recordedAt.localeCompare(b.recordedAt) || a.id.localeCompare(b.id)).slice(-exports.MAX_HISTORY_ENTRIES);
|
|
30
|
+
const target = getHistoryPath(cwd);
|
|
31
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(target), { recursive: true, mode: 0o700 });
|
|
32
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
33
|
+
await (0, promises_1.writeFile)(temporary, `${JSON.stringify({ schemaVersion: 1, entries }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
34
|
+
await (0, promises_1.rename)(temporary, target);
|
|
35
|
+
return { entries, invalidCount: read.invalidCount, unsupportedCount: read.unsupportedCount, ...(read.warning ? { warning: read.warning } : {}), duplicate: false };
|
|
36
|
+
}
|
|
37
|
+
async function clearLocalHistory(cwd) { await (0, promises_1.rm)(getHistoryPath(cwd), { force: true }); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploybuddy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local deployment intelligence for your terminal.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"deploybuddy": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/prestone2/deploybuddy.git",
|
|
24
|
+
"directory": "apps/cli"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/prestone2/deploybuddy#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/prestone2/deploybuddy/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"deployment",
|
|
32
|
+
"deploy",
|
|
33
|
+
"devops",
|
|
34
|
+
"git",
|
|
35
|
+
"testing",
|
|
36
|
+
"release-readiness"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "npm --prefix ../../packages/core run build && npm --prefix ../../packages/node-adapter run build && tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/cli.js',0o755)\"",
|
|
40
|
+
"start": "node dist/cli.js",
|
|
41
|
+
"test": "npm run build && node --test test/*.test.cjs"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@deploybuddy/core": "file:../../packages/core",
|
|
45
|
+
"@deploybuddy/node-adapter": "file:../../packages/node-adapter"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"typescript": "~6.0.2"
|
|
50
|
+
}
|
|
51
|
+
}
|