@rigstate/cli 0.6.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/.env.example +5 -0
- package/IMPLEMENTATION.md +239 -0
- package/QUICK_START.md +220 -0
- package/README.md +150 -0
- package/dist/index.cjs +3987 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3964 -0
- package/dist/index.js.map +1 -0
- package/install.sh +15 -0
- package/package.json +53 -0
- package/src/commands/check.ts +329 -0
- package/src/commands/config.ts +81 -0
- package/src/commands/daemon.ts +197 -0
- package/src/commands/env.ts +158 -0
- package/src/commands/fix.ts +140 -0
- package/src/commands/focus.ts +134 -0
- package/src/commands/hooks.ts +163 -0
- package/src/commands/init.ts +282 -0
- package/src/commands/link.ts +45 -0
- package/src/commands/login.ts +35 -0
- package/src/commands/mcp.ts +73 -0
- package/src/commands/nexus.ts +81 -0
- package/src/commands/override.ts +65 -0
- package/src/commands/scan.ts +242 -0
- package/src/commands/sync-rules.ts +191 -0
- package/src/commands/sync.ts +339 -0
- package/src/commands/watch.ts +283 -0
- package/src/commands/work.ts +172 -0
- package/src/daemon/bridge-listener.ts +127 -0
- package/src/daemon/core.ts +184 -0
- package/src/daemon/factory.ts +45 -0
- package/src/daemon/file-watcher.ts +97 -0
- package/src/daemon/guardian-monitor.ts +133 -0
- package/src/daemon/heuristic-engine.ts +203 -0
- package/src/daemon/intervention-protocol.ts +128 -0
- package/src/daemon/telemetry.ts +23 -0
- package/src/daemon/types.ts +18 -0
- package/src/hive/gateway.ts +74 -0
- package/src/hive/protocol.ts +29 -0
- package/src/hive/scrubber.ts +72 -0
- package/src/index.ts +85 -0
- package/src/nexus/council.ts +103 -0
- package/src/nexus/dispatcher.ts +133 -0
- package/src/utils/config.ts +83 -0
- package/src/utils/files.ts +95 -0
- package/src/utils/governance.ts +128 -0
- package/src/utils/logger.ts +66 -0
- package/src/utils/manifest.ts +18 -0
- package/src/utils/rule-engine.ts +292 -0
- package/src/utils/skills-provisioner.ts +153 -0
- package/src/utils/version.ts +1 -0
- package/src/utils/watchdog.ts +215 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
|
|
9
|
+
import { readGitignore, shouldIgnore, isCodeFile } from '../utils/files.js';
|
|
10
|
+
|
|
11
|
+
interface ScanResult {
|
|
12
|
+
id: string;
|
|
13
|
+
file_path: string;
|
|
14
|
+
issues: Array<{
|
|
15
|
+
type: string;
|
|
16
|
+
severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
17
|
+
message: string;
|
|
18
|
+
line?: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ApiResponse {
|
|
23
|
+
results: ScanResult[];
|
|
24
|
+
summary: {
|
|
25
|
+
total_files: number;
|
|
26
|
+
total_issues: number;
|
|
27
|
+
by_severity: Record<string, number>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createScanCommand(): Command {
|
|
32
|
+
return new Command('scan')
|
|
33
|
+
.description('Scan code files for security and quality issues')
|
|
34
|
+
.argument('[path]', 'Directory or file to scan', '.')
|
|
35
|
+
.option('--json', 'Output results as JSON')
|
|
36
|
+
.option('--project <id>', 'Project ID to associate with this scan')
|
|
37
|
+
.action(async (targetPath: string, options: { json?: boolean; project?: string }) => {
|
|
38
|
+
const spinner = ora();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Get API credentials
|
|
42
|
+
const apiKey = getApiKey();
|
|
43
|
+
const apiUrl = getApiUrl();
|
|
44
|
+
const projectId = options.project || getProjectId();
|
|
45
|
+
|
|
46
|
+
if (!projectId) {
|
|
47
|
+
console.warn(
|
|
48
|
+
chalk.yellow(
|
|
49
|
+
'â ī¸ No project ID specified. Use --project <id> or set a default.'
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Resolve target path
|
|
55
|
+
const scanPath = path.resolve(process.cwd(), targetPath);
|
|
56
|
+
|
|
57
|
+
spinner.start(`Scanning ${chalk.cyan(scanPath)}...`);
|
|
58
|
+
|
|
59
|
+
// Read .gitignore patterns
|
|
60
|
+
const gitignorePatterns = await readGitignore(scanPath);
|
|
61
|
+
|
|
62
|
+
// Find all code files
|
|
63
|
+
const pattern = path.join(scanPath, '**/*');
|
|
64
|
+
const allFiles = await glob(pattern, {
|
|
65
|
+
nodir: true,
|
|
66
|
+
dot: false,
|
|
67
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Filter files
|
|
71
|
+
const codeFiles = allFiles.filter((file) => {
|
|
72
|
+
const relativePath = path.relative(scanPath, file);
|
|
73
|
+
return isCodeFile(file) && !shouldIgnore(relativePath, gitignorePatterns);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (codeFiles.length === 0) {
|
|
77
|
+
spinner.warn(chalk.yellow('No code files found to scan.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
spinner.text = `Found ${codeFiles.length} files. Scanning...`;
|
|
82
|
+
|
|
83
|
+
// Scan each file individually
|
|
84
|
+
const results: ScanResult[] = [];
|
|
85
|
+
let totalIssues = 0;
|
|
86
|
+
const severityCounts: Record<string, number> = {};
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < codeFiles.length; i++) {
|
|
89
|
+
const filePath = codeFiles[i];
|
|
90
|
+
const relativePath = path.relative(scanPath, filePath);
|
|
91
|
+
|
|
92
|
+
spinner.text = `Scanning ${i + 1}/${codeFiles.length}: ${relativePath}`;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
96
|
+
|
|
97
|
+
// Call the API for this file
|
|
98
|
+
const response = await axios.post(
|
|
99
|
+
`${apiUrl}/api/v1/audit`,
|
|
100
|
+
{
|
|
101
|
+
content,
|
|
102
|
+
file_path: relativePath,
|
|
103
|
+
project_id: projectId,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
headers: {
|
|
107
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
},
|
|
110
|
+
timeout: 60000, // 1 minute per file
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Aggregate results
|
|
115
|
+
const vulnerabilities = response.data.vulnerabilities || [];
|
|
116
|
+
if (vulnerabilities.length > 0) {
|
|
117
|
+
results.push({
|
|
118
|
+
id: response.data.id || relativePath,
|
|
119
|
+
file_path: relativePath,
|
|
120
|
+
issues: vulnerabilities.map((v: any) => ({
|
|
121
|
+
type: v.type,
|
|
122
|
+
severity: v.severity,
|
|
123
|
+
message: v.description || v.title,
|
|
124
|
+
line: v.line_number,
|
|
125
|
+
})),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
totalIssues += vulnerabilities.length;
|
|
129
|
+
|
|
130
|
+
vulnerabilities.forEach((v: any) => {
|
|
131
|
+
severityCounts[v.severity] = (severityCounts[v.severity] || 0) + 1;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
} catch (fileError) {
|
|
135
|
+
if (axios.isAxiosError(fileError)) {
|
|
136
|
+
console.warn(chalk.yellow(`\nâ ī¸ Skipping ${relativePath}: ${fileError.message}`));
|
|
137
|
+
} else {
|
|
138
|
+
console.warn(chalk.yellow(`\nâ ī¸ Error reading ${relativePath}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
spinner.succeed(chalk.green('â
Scan completed!'));
|
|
144
|
+
|
|
145
|
+
// Build aggregated response
|
|
146
|
+
const aggregatedResponse: ApiResponse = {
|
|
147
|
+
results,
|
|
148
|
+
summary: {
|
|
149
|
+
total_files: codeFiles.length,
|
|
150
|
+
total_issues: totalIssues,
|
|
151
|
+
by_severity: severityCounts,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Output results
|
|
156
|
+
if (options.json) {
|
|
157
|
+
console.log(JSON.stringify(aggregatedResponse, null, 2));
|
|
158
|
+
} else {
|
|
159
|
+
printPrettyResults(aggregatedResponse);
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
spinner.fail(chalk.red('â Scan failed'));
|
|
163
|
+
|
|
164
|
+
if (axios.isAxiosError(error)) {
|
|
165
|
+
if (error.response) {
|
|
166
|
+
console.error(chalk.red('API Error:'), error.response.data);
|
|
167
|
+
} else if (error.request) {
|
|
168
|
+
console.error(
|
|
169
|
+
chalk.red('Network Error:'),
|
|
170
|
+
'Could not reach the API. Is the server running?'
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
console.error(chalk.red('Error:'), error.message);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
console.error(
|
|
177
|
+
chalk.red('Error:'),
|
|
178
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function printPrettyResults(data: ApiResponse): void {
|
|
188
|
+
const { results, summary } = data;
|
|
189
|
+
|
|
190
|
+
console.log('\n' + chalk.bold('đ Scan Summary'));
|
|
191
|
+
console.log(chalk.dim('â'.repeat(60)));
|
|
192
|
+
console.log(`Total Files Scanned: ${chalk.cyan(summary.total_files)}`);
|
|
193
|
+
console.log(`Total Issues Found: ${chalk.yellow(summary.total_issues)}`);
|
|
194
|
+
|
|
195
|
+
if (summary.by_severity) {
|
|
196
|
+
console.log('\nIssues by Severity:');
|
|
197
|
+
Object.entries(summary.by_severity).forEach(([severity, count]) => {
|
|
198
|
+
const color = getSeverityColor(severity as any);
|
|
199
|
+
console.log(` ${color(`${severity}:`)} ${count}`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (results && results.length > 0) {
|
|
204
|
+
console.log('\n' + chalk.bold('đ Detailed Results'));
|
|
205
|
+
console.log(chalk.dim('â'.repeat(60)));
|
|
206
|
+
|
|
207
|
+
results.forEach((result) => {
|
|
208
|
+
if (result.issues && result.issues.length > 0) {
|
|
209
|
+
console.log(`\n${chalk.bold(result.file_path)}`);
|
|
210
|
+
|
|
211
|
+
result.issues.forEach((issue) => {
|
|
212
|
+
const severityColor = getSeverityColor(issue.severity);
|
|
213
|
+
const lineInfo = issue.line ? chalk.dim(`:${issue.line}`) : '';
|
|
214
|
+
|
|
215
|
+
console.log(
|
|
216
|
+
` ${severityColor(`[${issue.severity.toUpperCase()}]`)} ${issue.type}${lineInfo}`
|
|
217
|
+
);
|
|
218
|
+
console.log(` ${chalk.dim(issue.message)}`);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('\n' + chalk.dim('â'.repeat(60)));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getSeverityColor(severity: string): (str: string) => string {
|
|
228
|
+
switch (severity.toLowerCase()) {
|
|
229
|
+
case 'critical':
|
|
230
|
+
return chalk.red.bold;
|
|
231
|
+
case 'high':
|
|
232
|
+
return chalk.red;
|
|
233
|
+
case 'medium':
|
|
234
|
+
return chalk.yellow;
|
|
235
|
+
case 'low':
|
|
236
|
+
return chalk.blue;
|
|
237
|
+
case 'info':
|
|
238
|
+
return chalk.gray;
|
|
239
|
+
default:
|
|
240
|
+
return chalk.white;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getApiKey, getApiUrl } from '../utils/config.js';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
|
|
7
|
+
interface SyncResult {
|
|
8
|
+
projectId: string;
|
|
9
|
+
projectName: string;
|
|
10
|
+
status: 'success' | 'failed';
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createSyncRulesCommand() {
|
|
15
|
+
const syncRules = new Command('sync-rules');
|
|
16
|
+
|
|
17
|
+
syncRules
|
|
18
|
+
.description('đĄī¸ Push Frank Protocol v1.0 to all existing projects')
|
|
19
|
+
.option('--dry-run', 'Preview changes without pushing to GitHub')
|
|
20
|
+
.option('--project <id>', 'Sync a specific project only')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
const spinner = ora('đĄī¸ Frank Protocol: Initializing retroactive sync...').start();
|
|
23
|
+
const results: SyncResult[] = [];
|
|
24
|
+
|
|
25
|
+
// Get config
|
|
26
|
+
let apiKey: string;
|
|
27
|
+
try {
|
|
28
|
+
apiKey = getApiKey();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
spinner.fail(chalk.red('Not authenticated. Run "rigstate login" first.'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const apiUrl = getApiUrl();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Fetch projects via API
|
|
38
|
+
spinner.text = 'Fetching projects...';
|
|
39
|
+
|
|
40
|
+
const projectsResponse = await axios.get(`${apiUrl}/api/v1/projects`, {
|
|
41
|
+
params: options.project ? { project_id: options.project } : {},
|
|
42
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!projectsResponse.data.success) {
|
|
46
|
+
throw new Error(projectsResponse.data.error || 'Failed to fetch projects');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let projects = projectsResponse.data.data.projects || [];
|
|
50
|
+
|
|
51
|
+
if (projects.length === 0) {
|
|
52
|
+
spinner.fail(chalk.red('No projects found.'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// If multiple projects found and no specific project flag, ask user
|
|
57
|
+
if (projects.length > 1 && !options.project) {
|
|
58
|
+
spinner.stop(); // Stop spinner to allow interaction
|
|
59
|
+
|
|
60
|
+
const inquirer = (await import('inquirer')).default;
|
|
61
|
+
const { selectedProjectId } = await inquirer.prompt([{
|
|
62
|
+
type: 'list',
|
|
63
|
+
name: 'selectedProjectId',
|
|
64
|
+
message: 'Multiple projects found. Which one do you want to sync?',
|
|
65
|
+
choices: projects.map((p: any) => ({
|
|
66
|
+
name: `${p.name} [${p.id}]`,
|
|
67
|
+
value: p.id
|
|
68
|
+
}))
|
|
69
|
+
}]);
|
|
70
|
+
|
|
71
|
+
projects = projects.filter((p: any) => p.id === selectedProjectId);
|
|
72
|
+
options.project = selectedProjectId; // Set this so we know we are in a targeted context for file writing
|
|
73
|
+
|
|
74
|
+
// Try to save this preference to .env for future
|
|
75
|
+
try {
|
|
76
|
+
const fs = await import('fs/promises');
|
|
77
|
+
const path = await import('path');
|
|
78
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
79
|
+
const envLocalPath = path.join(process.cwd(), '.env.local');
|
|
80
|
+
|
|
81
|
+
// Check if .env.local exists, otherwise check .env
|
|
82
|
+
let targetEnv = envLocalPath;
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(envLocalPath);
|
|
85
|
+
} catch {
|
|
86
|
+
try {
|
|
87
|
+
await fs.access(envPath);
|
|
88
|
+
targetEnv = envPath;
|
|
89
|
+
} catch {
|
|
90
|
+
// Neither exist, create .env
|
|
91
|
+
targetEnv = envPath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let content = '';
|
|
96
|
+
try {
|
|
97
|
+
content = await fs.readFile(targetEnv, 'utf-8');
|
|
98
|
+
} catch { }
|
|
99
|
+
|
|
100
|
+
if (!content.includes('RIGSTATE_PROJECT_ID')) {
|
|
101
|
+
const newContent = content.endsWith('\n') || content === ''
|
|
102
|
+
? `${content}RIGSTATE_PROJECT_ID=${selectedProjectId}\n`
|
|
103
|
+
: `${content}\nRIGSTATE_PROJECT_ID=${selectedProjectId}\n`;
|
|
104
|
+
|
|
105
|
+
await fs.writeFile(targetEnv, newContent, 'utf-8');
|
|
106
|
+
console.log(chalk.dim(` đž Saved default project to ${path.basename(targetEnv)}`));
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Ignore error saving env
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
spinner.succeed(`Syncing project: ${projects[0].name}`);
|
|
114
|
+
|
|
115
|
+
// Process each project
|
|
116
|
+
for (const project of projects) {
|
|
117
|
+
const projectSpinner = ora(` Syncing: ${project.name}...`).start();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (options.dryRun) {
|
|
121
|
+
projectSpinner.succeed(chalk.yellow(` [DRY-RUN] Would sync: ${project.name}`));
|
|
122
|
+
results.push({ projectId: project.id, projectName: project.name, status: 'success' });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Call API to regenerate and sync rules
|
|
127
|
+
const syncResponse = await axios.post(`${apiUrl}/api/v1/rules/sync`, {
|
|
128
|
+
project_id: project.id
|
|
129
|
+
}, {
|
|
130
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (syncResponse.data.success) {
|
|
134
|
+
if (syncResponse.data.data.github_synced) {
|
|
135
|
+
projectSpinner.succeed(chalk.green(` â
${project.name} [${project.id}] â GitHub synced`));
|
|
136
|
+
} else {
|
|
137
|
+
projectSpinner.info(chalk.blue(` âšī¸ ${project.name} [${project.id}] â Rules generated (no GitHub)`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Write files locally if we are syncing a single project or if inferred context matches
|
|
141
|
+
// For safety, if user didn't specify project and we found multiple, we only write if we can be sure.
|
|
142
|
+
// But usually, if you run this in a repo, you want the files.
|
|
143
|
+
// Let's write files if they are returned and we are arguably in the right place.
|
|
144
|
+
// To be safe: Only write if projects.length === 1 OR options.project is set.
|
|
145
|
+
|
|
146
|
+
const files = syncResponse.data.data.files;
|
|
147
|
+
if (files && Array.isArray(files) && (projects.length === 1 || options.project)) {
|
|
148
|
+
const fs = await import('fs/promises');
|
|
149
|
+
const path = await import('path');
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const filePath = path.join(process.cwd(), file.path);
|
|
153
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
154
|
+
await fs.writeFile(filePath, file.content, 'utf-8');
|
|
155
|
+
// projectSpinner.text = `Wrote ${file.path}`; // Don't spam spinner
|
|
156
|
+
}
|
|
157
|
+
console.log(chalk.dim(` đž Wrote ${files.length} rule files to local .cursor/rules/`));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
results.push({ projectId: project.id, projectName: project.name, status: 'success' });
|
|
161
|
+
} else {
|
|
162
|
+
projectSpinner.warn(chalk.yellow(` â ī¸ ${project.name} â ${syncResponse.data.error || 'Unknown error'}`));
|
|
163
|
+
results.push({ projectId: project.id, projectName: project.name, status: 'failed', error: syncResponse.data.error });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
} catch (e: any) {
|
|
167
|
+
projectSpinner.fail(chalk.red(` â ${project.name}: ${e.message}`));
|
|
168
|
+
results.push({ projectId: project.id, projectName: project.name, status: 'failed', error: e.message });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Summary
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log(chalk.bold('đ Sync Summary:'));
|
|
175
|
+
const successful = results.filter(r => r.status === 'success').length;
|
|
176
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
177
|
+
console.log(chalk.green(` â
Successful: ${successful}`));
|
|
178
|
+
if (failed > 0) {
|
|
179
|
+
console.log(chalk.red(` â Failed: ${failed}`));
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.cyan('đĄī¸ Frank Protocol v1.0 has been injected into the rules engine.'));
|
|
183
|
+
console.log(chalk.dim(' All new chats will now boot with mandatory governance checks.'));
|
|
184
|
+
|
|
185
|
+
} catch (e: any) {
|
|
186
|
+
spinner.fail(chalk.red('Sync failed: ' + e.message));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return syncRules;
|
|
191
|
+
}
|