@stackbilt/cli 0.1.5 → 0.1.8
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/dist/commands/audit.js +9 -3
- package/dist/commands/setup.js +14 -9
- package/dist/commands/validate.js +19 -9
- package/dist/commands/why.d.ts +3 -0
- package/dist/commands/why.js +168 -0
- package/dist/index.js +15 -7
- package/package.json +1 -1
package/dist/commands/audit.js
CHANGED
|
@@ -154,7 +154,7 @@ function printReport(report) {
|
|
|
154
154
|
}
|
|
155
155
|
function getRecentCommits(count) {
|
|
156
156
|
try {
|
|
157
|
-
const log = (
|
|
157
|
+
const log = runGit(['log', `-${count}`, '--format=%H|%an|%aI|%B---END---', '--name-only']);
|
|
158
158
|
const commits = [];
|
|
159
159
|
const entries = log.split('---END---');
|
|
160
160
|
for (const entry of entries) {
|
|
@@ -165,7 +165,7 @@ function getRecentCommits(count) {
|
|
|
165
165
|
if (!firstLine.includes('|'))
|
|
166
166
|
continue;
|
|
167
167
|
const pipeIdx = firstLine.indexOf('|');
|
|
168
|
-
const sha = firstLine.slice(0, pipeIdx)
|
|
168
|
+
const sha = firstLine.slice(0, pipeIdx);
|
|
169
169
|
const rest = firstLine.slice(pipeIdx + 1);
|
|
170
170
|
const [author, timestamp, ...msgParts] = rest.split('|');
|
|
171
171
|
const files = [];
|
|
@@ -189,4 +189,10 @@ function getRecentCommits(count) {
|
|
|
189
189
|
return [];
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
function runGit(args) {
|
|
193
|
+
return (0, node_child_process_1.execFileSync)('git', args, {
|
|
194
|
+
encoding: 'utf-8',
|
|
195
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=audit.js.map
|
package/dist/commands/setup.js
CHANGED
|
@@ -107,17 +107,22 @@ async function setupCommand(options, args) {
|
|
|
107
107
|
console.log(JSON.stringify(result, null, 2));
|
|
108
108
|
return index_2.EXIT_CODE.SUCCESS;
|
|
109
109
|
}
|
|
110
|
-
console.log('
|
|
111
|
-
console.log(`
|
|
112
|
-
console.log(`
|
|
110
|
+
console.log(' Governance guardrails are now active for this repo.');
|
|
111
|
+
console.log(` Baseline path: ${result.configPath}`);
|
|
112
|
+
console.log(` Baseline created: ${result.initialized ? 'yes' : 'already present'}`);
|
|
113
113
|
if (result.workflow.mode === 'github') {
|
|
114
|
-
console.log(`
|
|
114
|
+
console.log(` CI policy gate: ${result.workflow.created ? 'enabled' : 'already present'} (${result.workflow.path})`);
|
|
115
115
|
}
|
|
116
116
|
console.log('');
|
|
117
|
-
console.log('
|
|
118
|
-
console.log('
|
|
119
|
-
console.log('
|
|
120
|
-
console.log('
|
|
117
|
+
console.log(' What this gives you immediately:');
|
|
118
|
+
console.log(' - Merge-time checks for risky changes without governance links');
|
|
119
|
+
console.log(' - Drift detection against your blessed stack');
|
|
120
|
+
console.log(' - Audit-ready governance evidence from repo history');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(' Run now:');
|
|
123
|
+
console.log(' 1. charter validate --format text');
|
|
124
|
+
console.log(' 2. charter drift --format text');
|
|
125
|
+
console.log(' 3. charter audit --format text');
|
|
121
126
|
return index_2.EXIT_CODE.SUCCESS;
|
|
122
127
|
}
|
|
123
128
|
function writeFileIfMissing(targetPath, content, force) {
|
|
@@ -137,4 +142,4 @@ function getFlag(args, flag) {
|
|
|
137
142
|
}
|
|
138
143
|
return undefined;
|
|
139
144
|
}
|
|
140
|
-
//# sourceMappingURL=setup.js.map
|
|
145
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -124,14 +124,18 @@ function getCommitRange(args) {
|
|
|
124
124
|
return args[rangeIdx + 1];
|
|
125
125
|
}
|
|
126
126
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
const currentBranch = runGit(['rev-parse', 'HEAD']).trim();
|
|
128
|
+
let baseBranch = '';
|
|
129
|
+
try {
|
|
130
|
+
baseBranch = runGit(['rev-parse', '--verify', 'main']).trim();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
baseBranch = runGit(['rev-parse', '--verify', 'master']).trim();
|
|
134
|
+
}
|
|
135
|
+
if (!baseBranch || baseBranch === currentBranch) {
|
|
132
136
|
return 'HEAD~5..HEAD';
|
|
133
137
|
}
|
|
134
|
-
return `${
|
|
138
|
+
return `${baseBranch}..HEAD`;
|
|
135
139
|
}
|
|
136
140
|
catch {
|
|
137
141
|
return 'HEAD~5..HEAD';
|
|
@@ -139,7 +143,7 @@ function getCommitRange(args) {
|
|
|
139
143
|
}
|
|
140
144
|
function getGitCommits(range) {
|
|
141
145
|
try {
|
|
142
|
-
const log = (
|
|
146
|
+
const log = runGit(['log', range, '--format=%H|%an|%aI|%s', '--name-only']);
|
|
143
147
|
const commits = [];
|
|
144
148
|
let current = null;
|
|
145
149
|
for (const line of log.split('\n')) {
|
|
@@ -148,7 +152,7 @@ function getGitCommits(range) {
|
|
|
148
152
|
commits.push(current);
|
|
149
153
|
const [sha, author, timestamp, ...msgParts] = line.split('|');
|
|
150
154
|
current = {
|
|
151
|
-
sha
|
|
155
|
+
sha,
|
|
152
156
|
author,
|
|
153
157
|
timestamp,
|
|
154
158
|
message: msgParts.join('|'),
|
|
@@ -167,4 +171,10 @@ function getGitCommits(range) {
|
|
|
167
171
|
return [];
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
|
-
|
|
174
|
+
function runGit(args) {
|
|
175
|
+
return (0, node_child_process_1.execFileSync)('git', args, {
|
|
176
|
+
encoding: 'utf-8',
|
|
177
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.quickstartCommand = quickstartCommand;
|
|
4
|
+
exports.whyCommand = whyCommand;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const index_1 = require("../index");
|
|
9
|
+
const git_1 = require("@stackbilt/git");
|
|
10
|
+
async function quickstartCommand(options) {
|
|
11
|
+
const snapshot = getSnapshot(options.configPath);
|
|
12
|
+
if (options.format === 'json') {
|
|
13
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
14
|
+
return index_1.EXIT_CODE.SUCCESS;
|
|
15
|
+
}
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log(' Charter Quickstart');
|
|
18
|
+
console.log(' Turns governance from abstract policy into merge-time guardrails.');
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log(' Repo snapshot:');
|
|
21
|
+
console.log(` - Git repo: ${snapshot.inGitRepo ? 'yes' : 'no'}`);
|
|
22
|
+
console.log(` - Governance baseline (.charter): ${snapshot.hasBaseline ? 'installed' : 'missing'}`);
|
|
23
|
+
console.log(` - Recent commits scanned: ${snapshot.commitsScanned}`);
|
|
24
|
+
console.log(` - Governance-linked commit coverage: ${snapshot.coveragePercent}%`);
|
|
25
|
+
console.log(` - High-risk commits without governance links: ${snapshot.highRiskUnlinked}`);
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(' Why teams use Charter:');
|
|
28
|
+
console.log(' - Catch risky, unreviewed changes before merge');
|
|
29
|
+
console.log(' - Create an auditable trail from code changes to governance decisions');
|
|
30
|
+
console.log(' - Keep policy checks consistent across local dev and CI');
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log(` Next action: ${snapshot.nextAction}`);
|
|
33
|
+
console.log('');
|
|
34
|
+
return index_1.EXIT_CODE.SUCCESS;
|
|
35
|
+
}
|
|
36
|
+
async function whyCommand(options) {
|
|
37
|
+
if (options.format === 'json') {
|
|
38
|
+
console.log(JSON.stringify({
|
|
39
|
+
problem: 'Teams lose context on risky changes and approvals become inconsistent.',
|
|
40
|
+
charterSolves: [
|
|
41
|
+
'Enforces governance trailers for significant changes',
|
|
42
|
+
'Scans for stack drift against blessed patterns',
|
|
43
|
+
'Produces repeatable audit evidence for PRs and reviews',
|
|
44
|
+
],
|
|
45
|
+
value: [
|
|
46
|
+
'Lower probability of breaking changes landing without architectural context',
|
|
47
|
+
'Faster reviews because reviewers see linked decisions in commit metadata',
|
|
48
|
+
'Clear governance posture for leadership and compliance reporting',
|
|
49
|
+
],
|
|
50
|
+
start: 'charter setup --ci github',
|
|
51
|
+
}, null, 2));
|
|
52
|
+
return index_1.EXIT_CODE.SUCCESS;
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(' Why Charter');
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' Problem it solves:');
|
|
58
|
+
console.log(' Governance intent lives in docs, but risky changes merge without clear decision links.');
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(' What Charter does:');
|
|
61
|
+
console.log(' - Enforces commit-level governance links for significant changes');
|
|
62
|
+
console.log(' - Detects drift from your blessed architecture patterns');
|
|
63
|
+
console.log(' - Generates audit output leadership and reviewers can actually use');
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(' Expected payoff:');
|
|
66
|
+
console.log(' - Fewer high-impact surprises in production');
|
|
67
|
+
console.log(' - Faster review cycles with clearer architectural accountability');
|
|
68
|
+
console.log(' - Consistent governance behavior across repos');
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(' Start here: charter setup --ci github');
|
|
71
|
+
console.log('');
|
|
72
|
+
return index_1.EXIT_CODE.SUCCESS;
|
|
73
|
+
}
|
|
74
|
+
function getSnapshot(configPath) {
|
|
75
|
+
const inGitRepo = isGitRepo();
|
|
76
|
+
const hasBaseline = fs.existsSync(path.join(configPath, 'config.json'));
|
|
77
|
+
if (!inGitRepo) {
|
|
78
|
+
return {
|
|
79
|
+
inGitRepo,
|
|
80
|
+
hasBaseline,
|
|
81
|
+
commitsScanned: 0,
|
|
82
|
+
coveragePercent: 0,
|
|
83
|
+
highRiskUnlinked: 0,
|
|
84
|
+
nextAction: 'Run this inside a git repository, then run: charter setup --ci github',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const commits = hasCommits() ? getRecentCommits(20) : [];
|
|
88
|
+
const parsed = (0, git_1.parseAllTrailers)(commits);
|
|
89
|
+
const linked = new Set();
|
|
90
|
+
for (const t of parsed.governedBy)
|
|
91
|
+
linked.add(t.commitSha);
|
|
92
|
+
for (const t of parsed.resolvesRequest)
|
|
93
|
+
linked.add(t.commitSha);
|
|
94
|
+
let highRiskUnlinked = 0;
|
|
95
|
+
for (const commit of commits) {
|
|
96
|
+
if (!linked.has(commit.sha) && (0, git_1.assessCommitRisk)(commit.files_changed, commit.message) === 'HIGH') {
|
|
97
|
+
highRiskUnlinked++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const coveragePercent = commits.length > 0 ? Math.round((linked.size / commits.length) * 100) : 0;
|
|
101
|
+
const nextAction = !hasBaseline
|
|
102
|
+
? 'Run: charter setup --ci github'
|
|
103
|
+
: highRiskUnlinked > 0
|
|
104
|
+
? 'Run: charter validate --format text and add Governed-By trailers to high-risk commits'
|
|
105
|
+
: 'Run: charter audit --format text for a shareable governance posture report';
|
|
106
|
+
return {
|
|
107
|
+
inGitRepo,
|
|
108
|
+
hasBaseline,
|
|
109
|
+
commitsScanned: commits.length,
|
|
110
|
+
coveragePercent,
|
|
111
|
+
highRiskUnlinked,
|
|
112
|
+
nextAction,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function isGitRepo() {
|
|
116
|
+
try {
|
|
117
|
+
(0, node_child_process_1.execFileSync)('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function getRecentCommits(count) {
|
|
125
|
+
try {
|
|
126
|
+
const log = (0, node_child_process_1.execFileSync)('git', ['log', `-${count}`, '--format=%H|%s', '--name-only'], {
|
|
127
|
+
encoding: 'utf-8',
|
|
128
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
129
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
130
|
+
});
|
|
131
|
+
const commits = [];
|
|
132
|
+
let current = null;
|
|
133
|
+
for (const line of log.split('\n')) {
|
|
134
|
+
if (line.includes('|') && line.length > 40) {
|
|
135
|
+
if (current)
|
|
136
|
+
commits.push(current);
|
|
137
|
+
const [sha, ...message] = line.split('|');
|
|
138
|
+
current = {
|
|
139
|
+
sha,
|
|
140
|
+
author: '',
|
|
141
|
+
timestamp: '',
|
|
142
|
+
message: message.join('|'),
|
|
143
|
+
files_changed: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else if (line.trim() && current) {
|
|
147
|
+
current.files_changed.push(line.trim());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (current)
|
|
151
|
+
commits.push(current);
|
|
152
|
+
return commits;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function hasCommits() {
|
|
159
|
+
try {
|
|
160
|
+
(0, node_child_process_1.execFileSync)('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
161
|
+
stdio: 'ignore',
|
|
162
|
+
});
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,18 +15,21 @@ const validate_1 = require("./commands/validate");
|
|
|
15
15
|
const audit_1 = require("./commands/audit");
|
|
16
16
|
const drift_1 = require("./commands/drift");
|
|
17
17
|
const classify_1 = require("./commands/classify");
|
|
18
|
+
const why_1 = require("./commands/why");
|
|
18
19
|
const package_json_1 = require("../package.json");
|
|
19
20
|
const CLI_VERSION = package_json_1.version;
|
|
20
21
|
const HELP = `
|
|
21
22
|
charter - repo-level governance toolkit
|
|
22
23
|
|
|
23
24
|
Usage:
|
|
25
|
+
charter Show immediate governance value + risk snapshot
|
|
24
26
|
charter setup [--ci github] Bootstrap .charter/ and optional CI workflow
|
|
25
27
|
charter init Scaffold .charter/ config directory
|
|
26
28
|
charter validate Validate git commits for governance trailers
|
|
27
29
|
charter audit Generate governance audit report
|
|
28
30
|
charter drift [--path <dir>] Scan files for pattern drift
|
|
29
31
|
charter classify <subject> Classify a change (SURFACE/LOCAL/CROSS_CUTTING)
|
|
32
|
+
charter why Explain why teams adopt Charter and expected ROI
|
|
30
33
|
charter doctor Check CLI + config health
|
|
31
34
|
charter --help Show this help
|
|
32
35
|
charter --version Show version
|
|
@@ -52,7 +55,7 @@ class CLIError extends Error {
|
|
|
52
55
|
}
|
|
53
56
|
exports.CLIError = CLIError;
|
|
54
57
|
async function run(args) {
|
|
55
|
-
if (args.
|
|
58
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
56
59
|
console.log(HELP);
|
|
57
60
|
return exports.EXIT_CODE.SUCCESS;
|
|
58
61
|
}
|
|
@@ -60,18 +63,21 @@ async function run(args) {
|
|
|
60
63
|
console.log(`charter v${CLI_VERSION}`);
|
|
61
64
|
return exports.EXIT_CODE.SUCCESS;
|
|
62
65
|
}
|
|
63
|
-
const
|
|
64
|
-
const restArgs = args.slice(1);
|
|
65
|
-
const format = getFlag(restArgs, '--format') || 'text';
|
|
66
|
+
const format = getFlag(args, '--format') || 'text';
|
|
66
67
|
if (format !== 'text' && format !== 'json') {
|
|
67
68
|
throw new CLIError(`Invalid --format value: ${format}. Use text or json.`);
|
|
68
69
|
}
|
|
69
70
|
const options = {
|
|
70
|
-
configPath: getFlag(
|
|
71
|
+
configPath: getFlag(args, '--config') || '.charter',
|
|
71
72
|
format,
|
|
72
|
-
ciMode:
|
|
73
|
-
yes:
|
|
73
|
+
ciMode: args.includes('--ci'),
|
|
74
|
+
yes: args.includes('--yes'),
|
|
74
75
|
};
|
|
76
|
+
if (args.length === 0 || args[0].startsWith('-')) {
|
|
77
|
+
return (0, why_1.quickstartCommand)(options);
|
|
78
|
+
}
|
|
79
|
+
const command = args[0];
|
|
80
|
+
const restArgs = args.slice(1);
|
|
75
81
|
switch (command) {
|
|
76
82
|
case 'setup':
|
|
77
83
|
return (0, setup_1.setupCommand)(options, restArgs);
|
|
@@ -85,6 +91,8 @@ async function run(args) {
|
|
|
85
91
|
return (0, drift_1.driftCommand)(options, restArgs);
|
|
86
92
|
case 'classify':
|
|
87
93
|
return (0, classify_1.classifyCommand)(options, restArgs);
|
|
94
|
+
case 'why':
|
|
95
|
+
return (0, why_1.whyCommand)(options);
|
|
88
96
|
case 'doctor':
|
|
89
97
|
return (0, doctor_1.doctorCommand)(options);
|
|
90
98
|
default:
|