@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { createLoginCommand } from './commands/login.js';
|
|
6
|
+
import { createLinkCommand } from './commands/link.js';
|
|
7
|
+
import { createScanCommand } from './commands/scan.js';
|
|
8
|
+
import { createFixCommand } from './commands/fix.js';
|
|
9
|
+
import { createSyncCommand } from './commands/sync.js';
|
|
10
|
+
import { createInitCommand } from './commands/init.js';
|
|
11
|
+
import { createCheckCommand } from './commands/check.js';
|
|
12
|
+
import { createHooksCommand } from './commands/hooks.js';
|
|
13
|
+
import { createDaemonCommand } from './commands/daemon.js';
|
|
14
|
+
import { createWorkCommand } from './commands/work.js';
|
|
15
|
+
import { createWatchCommand } from './commands/watch.js';
|
|
16
|
+
import { createFocusCommand } from './commands/focus.js';
|
|
17
|
+
import { createEnvPullCommand } from './commands/env.js';
|
|
18
|
+
import { createConfigCommand } from './commands/config.js';
|
|
19
|
+
import { createMcpCommand } from './commands/mcp.js';
|
|
20
|
+
import { createNexusCommand } from './commands/nexus.js';
|
|
21
|
+
import { createSyncRulesCommand } from './commands/sync-rules.js';
|
|
22
|
+
import { createOverrideCommand } from './commands/override.js';
|
|
23
|
+
import { checkVersion } from './utils/version.js';
|
|
24
|
+
import dotenv from 'dotenv';
|
|
25
|
+
|
|
26
|
+
// Load environment variables
|
|
27
|
+
dotenv.config();
|
|
28
|
+
|
|
29
|
+
const program = new Command();
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.name('rigstate')
|
|
33
|
+
.description('CLI for Rigstate - The AI-Native Dev Studio')
|
|
34
|
+
.version('0.2.0');
|
|
35
|
+
|
|
36
|
+
// Register commands
|
|
37
|
+
program.addCommand(createLoginCommand());
|
|
38
|
+
program.addCommand(createLinkCommand());
|
|
39
|
+
program.addCommand(createScanCommand());
|
|
40
|
+
program.addCommand(createFixCommand());
|
|
41
|
+
program.addCommand(createSyncCommand());
|
|
42
|
+
program.addCommand(createInitCommand());
|
|
43
|
+
program.addCommand(createCheckCommand());
|
|
44
|
+
program.addCommand(createHooksCommand());
|
|
45
|
+
program.addCommand(createDaemonCommand());
|
|
46
|
+
program.addCommand(createWorkCommand());
|
|
47
|
+
program.addCommand(createWatchCommand());
|
|
48
|
+
program.addCommand(createFocusCommand());
|
|
49
|
+
program.addCommand(createEnvPullCommand());
|
|
50
|
+
program.addCommand(createConfigCommand());
|
|
51
|
+
program.addCommand(createMcpCommand());
|
|
52
|
+
program.addCommand(createNexusCommand());
|
|
53
|
+
program.addCommand(createSyncRulesCommand());
|
|
54
|
+
program.addCommand(createOverrideCommand());
|
|
55
|
+
|
|
56
|
+
program.hook('preAction', async () => {
|
|
57
|
+
await checkVersion();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Add helpful examples
|
|
61
|
+
program.on('--help', () => {
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.bold('Examples:'));
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(chalk.cyan(' $ rigstate login sk_rigstate_your_api_key'));
|
|
66
|
+
console.log(chalk.dim(' Authenticate with your Rigstate API key'));
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(chalk.cyan(' $ rigstate scan'));
|
|
69
|
+
console.log(chalk.dim(' Scan the current directory'));
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(chalk.cyan(' $ rigstate scan ./src --project abc123'));
|
|
72
|
+
console.log(chalk.dim(' Scan a specific directory with project ID'));
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log(chalk.cyan(' $ rigstate scan --json'));
|
|
75
|
+
console.log(chalk.dim(' Output results in JSON format (useful for IDE extensions)'));
|
|
76
|
+
console.log('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Parse arguments
|
|
80
|
+
program.parse(process.argv);
|
|
81
|
+
|
|
82
|
+
// Show help if no command provided
|
|
83
|
+
if (!process.argv.slice(2).length) {
|
|
84
|
+
program.outputHelp();
|
|
85
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import { SwarmAgent, ServiceOrder } from '@rigstate/shared';
|
|
3
|
+
|
|
4
|
+
export type AgentWeight = number;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE HIERARCHY OF RIGSTATE
|
|
8
|
+
* Defines the decision power of each agent in The Council.
|
|
9
|
+
*/
|
|
10
|
+
export const AGENT_WEIGHTS: Record<SwarmAgent, AgentWeight> = {
|
|
11
|
+
'FRANK': 100, // SUPREME COURT (Security/Architecture) - Veto Power
|
|
12
|
+
'SVEN': 95, // SENTINEL (Compliance/Audit) - Highly Authoritative
|
|
13
|
+
'SINDRE': 80, // VAULT KEEPER (Data Integrity) - High Authority on Data
|
|
14
|
+
'MAJA': 70, // SCRIBE (Documentation) - Advisory
|
|
15
|
+
'EITRI': 60 // BUILDER (Implementation) - Mutable, subservient to Architects
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface Conflict {
|
|
19
|
+
id: string;
|
|
20
|
+
proposal: ServiceOrder;
|
|
21
|
+
objector: SwarmAgent;
|
|
22
|
+
reason: string;
|
|
23
|
+
severity: 'CRITICAL' | 'WARNING' | 'SUGGESTION';
|
|
24
|
+
alternativeProposal?: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CouncilResolution {
|
|
28
|
+
outcome: 'UPHELD' | 'OVERRULED' | 'COMPROMISE';
|
|
29
|
+
winner: SwarmAgent;
|
|
30
|
+
action: 'BLOCK' | 'PROCEED' | 'MODIFY';
|
|
31
|
+
rationale: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* THE COUNCIL v2
|
|
36
|
+
* Resolves disputes between agents using Weighted Democracy.
|
|
37
|
+
*/
|
|
38
|
+
export class Council {
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Judges a conflict based on Agent weights and Hierarchy.
|
|
42
|
+
*/
|
|
43
|
+
public resolve(conflict: Conflict): CouncilResolution {
|
|
44
|
+
const proposer = conflict.proposal.targetAgent; // The agent doing the work (e.g. Eitri)
|
|
45
|
+
const objector = conflict.objector; // The agent complaining (e.g. Sven)
|
|
46
|
+
|
|
47
|
+
const proposerWeight = AGENT_WEIGHTS[proposer] || 50;
|
|
48
|
+
const objectorWeight = AGENT_WEIGHTS[objector] || 50;
|
|
49
|
+
|
|
50
|
+
console.log(`⚖️ COUNCIL IN SESSION: ${objector} (${objectorWeight}) objects to ${proposer} (${proposerWeight})`);
|
|
51
|
+
|
|
52
|
+
// 1. ABSOLUTE VETO (Frank/Sven on Critical Issues)
|
|
53
|
+
if ((objector === 'FRANK' || objector === 'SVEN') && conflict.severity === 'CRITICAL') {
|
|
54
|
+
return {
|
|
55
|
+
outcome: 'UPHELD',
|
|
56
|
+
winner: objector,
|
|
57
|
+
action: 'BLOCK',
|
|
58
|
+
rationale: `Absolute Veto by ${objector} on CRITICAL violation: ${conflict.reason}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. SUGGESTION OVERRIDE (Pragmatism)
|
|
63
|
+
// If it's just a suggestion, the Builder (Proposer) usually proceeds unless it's the Supreme Court (Frank).
|
|
64
|
+
if (conflict.severity === 'SUGGESTION' && objector !== 'FRANK') {
|
|
65
|
+
return {
|
|
66
|
+
outcome: 'OVERRULED',
|
|
67
|
+
winner: proposer,
|
|
68
|
+
action: 'PROCEED',
|
|
69
|
+
rationale: `Minor suggestion by ${objector} noted but overruled for velocity.`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. WEIGHT COMPARISON
|
|
74
|
+
if (objectorWeight > proposerWeight) {
|
|
75
|
+
return {
|
|
76
|
+
outcome: 'UPHELD',
|
|
77
|
+
winner: objector,
|
|
78
|
+
action: 'BLOCK',
|
|
79
|
+
rationale: `${objector} outranks ${proposer}. Objection validated.`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. TIE-BREAKER / LOWER AUTHORITY
|
|
84
|
+
// If weights are equal or objector is lower, we generally favor the builder (Proposer)
|
|
85
|
+
// unless it's a security suggestion.
|
|
86
|
+
if (conflict.severity === 'SUGGESTION' && proposer === 'EITRI') {
|
|
87
|
+
return {
|
|
88
|
+
outcome: 'OVERRULED',
|
|
89
|
+
winner: proposer,
|
|
90
|
+
action: 'PROCEED',
|
|
91
|
+
rationale: `${proposer} has implementation authority on non-critical suggestions.`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Default: Proposer wins if they outrank objector
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'OVERRULED',
|
|
98
|
+
winner: proposer,
|
|
99
|
+
action: 'PROCEED',
|
|
100
|
+
rationale: `Objection overruled. ${proposer} retains authority.`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
import { ServiceOrder, SwarmAgent, NexusContext } from '@rigstate/shared';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
import { HiveGateway } from '../hive/gateway';
|
|
7
|
+
import { Logger } from '../utils/logger';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE NEXUS DISPATCHER
|
|
11
|
+
* "The Brain Stem" of Rigstate.
|
|
12
|
+
* Routes ServiceOrders between agents and enforces the Human Kill-Switch.
|
|
13
|
+
*/
|
|
14
|
+
export class NexusDispatcher extends EventEmitter {
|
|
15
|
+
private context: NexusContext;
|
|
16
|
+
private orderQueue: ServiceOrder[] = [];
|
|
17
|
+
private orderHistory: ServiceOrder[] = [];
|
|
18
|
+
private gateway: HiveGateway;
|
|
19
|
+
|
|
20
|
+
constructor(context: NexusContext) {
|
|
21
|
+
super();
|
|
22
|
+
this.context = context;
|
|
23
|
+
this.gateway = new HiveGateway(
|
|
24
|
+
process.env.RIGSTATE_HIVE_URL || 'https://rigstate.com/api/hive',
|
|
25
|
+
process.env.RIGSTATE_HIVE_TOKEN
|
|
26
|
+
);
|
|
27
|
+
Logger.info(`🧠 NEXUS DISPATCHER ONLINE. Context: ${context.projectId} (DryRun: ${context.dryRun})`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new Service Order and routes it.
|
|
32
|
+
*/
|
|
33
|
+
public async dispatch(
|
|
34
|
+
source: SwarmAgent,
|
|
35
|
+
target: SwarmAgent,
|
|
36
|
+
intent: string,
|
|
37
|
+
action: string,
|
|
38
|
+
params: Record<string, any>,
|
|
39
|
+
constraints: string[] = []
|
|
40
|
+
): Promise<ServiceOrder> {
|
|
41
|
+
|
|
42
|
+
const order: ServiceOrder = {
|
|
43
|
+
id: uuidv4(),
|
|
44
|
+
traceId: uuidv4(), // TODO: Inherit traceId if chained
|
|
45
|
+
sourceAgent: source,
|
|
46
|
+
targetAgent: target,
|
|
47
|
+
priority: 'NORMAL',
|
|
48
|
+
intent,
|
|
49
|
+
action,
|
|
50
|
+
parameters: params,
|
|
51
|
+
constraints,
|
|
52
|
+
status: 'PENDING',
|
|
53
|
+
createdAt: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.orderQueue.push(order);
|
|
57
|
+
this.emit('order:created', order);
|
|
58
|
+
|
|
59
|
+
// Security / Kill-Switch Check
|
|
60
|
+
// EITRI (The Smith) is the only one who can hammer the metal (write files)
|
|
61
|
+
if (target === 'EITRI' && order.action.startsWith('fs.write')) {
|
|
62
|
+
if (this.context.dryRun) {
|
|
63
|
+
Logger.info(`🛑 NEXUS KILL-SWITCH: Order ${order.id} blocked by Dry-Run protocol.`);
|
|
64
|
+
order.status = 'PENDING'; // Kept as PENDING but blocked
|
|
65
|
+
// Ideally status should represent AWAITING_APPROVAL explicitly
|
|
66
|
+
// But for now strict dry-run just prevents execution
|
|
67
|
+
this.emit('order:blocked', order);
|
|
68
|
+
return order;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If automatic or dry-run disabled
|
|
73
|
+
return this.executeOrder(order);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Executes the order (simulated for now, essentially "Sending" it)
|
|
78
|
+
*/
|
|
79
|
+
private async executeOrder(order: ServiceOrder): Promise<ServiceOrder> {
|
|
80
|
+
order.status = 'EXECUTING';
|
|
81
|
+
order.startedAt = new Date().toISOString();
|
|
82
|
+
this.emit('order:started', order);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
Logger.info(`🚀 NEXUS: Routing Order ${order.id} [${order.sourceAgent} -> ${order.targetAgent}]: ${order.intent}`);
|
|
86
|
+
|
|
87
|
+
// SPECIAL ROUTING: HIVE UPLINK
|
|
88
|
+
if (order.targetAgent === 'MAJA' && order.action === 'HIVE_TRANSMIT') {
|
|
89
|
+
const signal = order.parameters.signal;
|
|
90
|
+
if (signal) {
|
|
91
|
+
await this.gateway.transmit(signal);
|
|
92
|
+
order.status = 'COMPLETED';
|
|
93
|
+
return order;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Here we would actually call the Agent's handler function
|
|
98
|
+
// For now, we just emit the specific event for listeners
|
|
99
|
+
this.emit(`agent:${order.targetAgent}`, order);
|
|
100
|
+
|
|
101
|
+
// Simulation of async completion would happen via callback/promise resolution elsewhere
|
|
102
|
+
return order;
|
|
103
|
+
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
106
|
+
Logger.error(`Dispatch failed for order ${order.id}`, error);
|
|
107
|
+
|
|
108
|
+
order.status = 'FAILED';
|
|
109
|
+
order.error = {
|
|
110
|
+
code: 'DISPATCH_ERROR',
|
|
111
|
+
message: errorMessage
|
|
112
|
+
};
|
|
113
|
+
this.emit('order:failed', order);
|
|
114
|
+
return order;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Human Approval (The "Red Button")
|
|
120
|
+
*/
|
|
121
|
+
public async approveOrder(orderId: string): Promise<void> {
|
|
122
|
+
const order = this.orderQueue.find(o => o.id === orderId);
|
|
123
|
+
if (!order) throw new Error(`Order ${orderId} not found`);
|
|
124
|
+
|
|
125
|
+
if (order.status !== 'AWAITING_APPROVAL') {
|
|
126
|
+
Logger.warn(`Order ${orderId} is not awaiting approval (Status: ${order.status})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Logger.info(`✅ HUMAN APPROVED Order ${orderId}`);
|
|
131
|
+
await this.executeOrder(order);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
interface RigstateConfig {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
projectId?: string;
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const config = new Conf<RigstateConfig>({
|
|
10
|
+
projectName: 'rigstate-cli',
|
|
11
|
+
defaults: {
|
|
12
|
+
apiUrl: 'http://localhost:3000',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the stored API key
|
|
18
|
+
* @throws {Error} If no API key is found (user not logged in)
|
|
19
|
+
*/
|
|
20
|
+
export function getApiKey(): string {
|
|
21
|
+
const apiKey = config.get('apiKey');
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'❌ Not logged in. Please run "rigstate login <your-api-key>" first.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return apiKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set the API key
|
|
32
|
+
*/
|
|
33
|
+
export function setApiKey(key: string): void {
|
|
34
|
+
config.set('apiKey', key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the default project ID (if set)
|
|
39
|
+
*/
|
|
40
|
+
export function getProjectId(): string | undefined {
|
|
41
|
+
return config.get('projectId');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set the default project ID
|
|
46
|
+
*/
|
|
47
|
+
export function setProjectId(projectId: string): void {
|
|
48
|
+
config.set('projectId', projectId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the API URL
|
|
53
|
+
* Priority: Environment variable > Stored config > Production default
|
|
54
|
+
*/
|
|
55
|
+
export function getApiUrl(): string {
|
|
56
|
+
// 1. Check environment variable first
|
|
57
|
+
if (process.env.RIGSTATE_API_URL) {
|
|
58
|
+
return process.env.RIGSTATE_API_URL;
|
|
59
|
+
}
|
|
60
|
+
// 2. Check stored config
|
|
61
|
+
const storedUrl = config.get('apiUrl');
|
|
62
|
+
if (storedUrl) {
|
|
63
|
+
return storedUrl;
|
|
64
|
+
}
|
|
65
|
+
// 3. Default to production
|
|
66
|
+
return 'https://app.rigstate.com';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the API URL
|
|
71
|
+
*/
|
|
72
|
+
export function setApiUrl(url: string): void {
|
|
73
|
+
config.set('apiUrl', url);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Clear all config
|
|
78
|
+
*/
|
|
79
|
+
export function clearConfig(): void {
|
|
80
|
+
config.clear();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { config };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read and parse .gitignore file
|
|
6
|
+
*/
|
|
7
|
+
export async function readGitignore(dir: string): Promise<string[]> {
|
|
8
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
9
|
+
try {
|
|
10
|
+
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
11
|
+
return content
|
|
12
|
+
.split('\n')
|
|
13
|
+
.map((line) => line.trim())
|
|
14
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// No .gitignore file found, return empty array
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a path should be ignored based on .gitignore patterns
|
|
23
|
+
*/
|
|
24
|
+
export function shouldIgnore(filePath: string, patterns: string[]): boolean {
|
|
25
|
+
const relativePath = filePath.replace(/^\.\//, '');
|
|
26
|
+
|
|
27
|
+
// Default ignore patterns
|
|
28
|
+
const defaultIgnores = [
|
|
29
|
+
'node_modules',
|
|
30
|
+
'.git',
|
|
31
|
+
'dist',
|
|
32
|
+
'build',
|
|
33
|
+
'.next',
|
|
34
|
+
'.turbo',
|
|
35
|
+
'coverage',
|
|
36
|
+
'.env',
|
|
37
|
+
'.env.local',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const allPatterns = [...defaultIgnores, ...patterns];
|
|
41
|
+
|
|
42
|
+
for (const pattern of allPatterns) {
|
|
43
|
+
if (pattern.endsWith('/')) {
|
|
44
|
+
// Directory pattern
|
|
45
|
+
const dir = pattern.slice(0, -1);
|
|
46
|
+
if (relativePath.includes(`${dir}/`) || relativePath === dir) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
} else if (pattern.includes('*')) {
|
|
50
|
+
// Glob pattern - simple implementation
|
|
51
|
+
const regex = new RegExp(
|
|
52
|
+
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
|
53
|
+
);
|
|
54
|
+
if (regex.test(relativePath)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Exact match or contains
|
|
59
|
+
if (relativePath.includes(pattern)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if file is a code file based on extension
|
|
70
|
+
*/
|
|
71
|
+
export function isCodeFile(filePath: string): boolean {
|
|
72
|
+
const codeExtensions = [
|
|
73
|
+
'.js',
|
|
74
|
+
'.jsx',
|
|
75
|
+
'.ts',
|
|
76
|
+
'.tsx',
|
|
77
|
+
'.py',
|
|
78
|
+
'.java',
|
|
79
|
+
'.go',
|
|
80
|
+
'.rb',
|
|
81
|
+
'.php',
|
|
82
|
+
'.c',
|
|
83
|
+
'.cpp',
|
|
84
|
+
'.h',
|
|
85
|
+
'.cs',
|
|
86
|
+
'.swift',
|
|
87
|
+
'.kt',
|
|
88
|
+
'.rs',
|
|
89
|
+
'.vue',
|
|
90
|
+
'.svelte',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
94
|
+
return codeExtensions.includes(ext);
|
|
95
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
// --- Types ---
|
|
6
|
+
|
|
7
|
+
export enum InterventionLevel {
|
|
8
|
+
GHOST = 0, // Log only, silent
|
|
9
|
+
NUDGE = 1, // Warn on commit/complete
|
|
10
|
+
SENTINEL = 2 // Immediate SOFT_LOCK
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type LockStatus = 'OPEN' | 'SOFT_LOCK';
|
|
14
|
+
|
|
15
|
+
export interface GovernanceConfig {
|
|
16
|
+
governance: {
|
|
17
|
+
intervention_level: InterventionLevel;
|
|
18
|
+
allow_overrides: boolean;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SessionState {
|
|
23
|
+
status: LockStatus;
|
|
24
|
+
active_violation?: string | null;
|
|
25
|
+
lock_reason?: string | null;
|
|
26
|
+
last_updated: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONFIG: GovernanceConfig = {
|
|
30
|
+
governance: {
|
|
31
|
+
intervention_level: InterventionLevel.GHOST,
|
|
32
|
+
allow_overrides: true
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_SESSION: SessionState = {
|
|
37
|
+
status: 'OPEN',
|
|
38
|
+
active_violation: null,
|
|
39
|
+
lock_reason: null,
|
|
40
|
+
last_updated: new Date().toISOString()
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// --- Config Management ---
|
|
44
|
+
|
|
45
|
+
export async function getGovernanceConfig(rootDir: string = process.cwd()): Promise<GovernanceConfig> {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = path.join(rootDir, 'rigstate.config.json');
|
|
48
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
49
|
+
const userConfig = JSON.parse(content);
|
|
50
|
+
return {
|
|
51
|
+
governance: {
|
|
52
|
+
...DEFAULT_CONFIG.governance,
|
|
53
|
+
...userConfig.governance
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return DEFAULT_CONFIG;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Session State Management ---
|
|
62
|
+
|
|
63
|
+
export async function getSessionState(rootDir: string = process.cwd()): Promise<SessionState> {
|
|
64
|
+
try {
|
|
65
|
+
const sessionPath = path.join(rootDir, '.rigstate', 'session.json');
|
|
66
|
+
const content = await fs.readFile(sessionPath, 'utf-8');
|
|
67
|
+
return JSON.parse(content);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return DEFAULT_SESSION;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function setSoftLock(
|
|
74
|
+
reason: string,
|
|
75
|
+
violationId: string,
|
|
76
|
+
rootDir: string = process.cwd()
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const sessionPath = path.join(rootDir, '.rigstate', 'session.json');
|
|
79
|
+
const state: SessionState = {
|
|
80
|
+
status: 'SOFT_LOCK',
|
|
81
|
+
active_violation: violationId,
|
|
82
|
+
lock_reason: reason,
|
|
83
|
+
last_updated: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await fs.mkdir(path.dirname(sessionPath), { recursive: true });
|
|
87
|
+
await fs.writeFile(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function clearSoftLock(
|
|
91
|
+
rootDir: string = process.cwd()
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const sessionPath = path.join(rootDir, '.rigstate', 'session.json');
|
|
94
|
+
const state: SessionState = {
|
|
95
|
+
...DEFAULT_SESSION,
|
|
96
|
+
last_updated: new Date().toISOString()
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await fs.mkdir(path.dirname(sessionPath), { recursive: true });
|
|
100
|
+
await fs.writeFile(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Checks if the user is authorized to perform an override
|
|
105
|
+
* (For now, assumes CLI user is authorized, but logs it)
|
|
106
|
+
*/
|
|
107
|
+
export async function performOverride(
|
|
108
|
+
violationId: string,
|
|
109
|
+
reason: string,
|
|
110
|
+
rootDir: string = process.cwd()
|
|
111
|
+
): Promise<boolean> {
|
|
112
|
+
const config = await getGovernanceConfig(rootDir);
|
|
113
|
+
|
|
114
|
+
if (!config.governance.allow_overrides) {
|
|
115
|
+
console.log(chalk.red('❌ Overrides are disabled for this project.'));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Security violations (SEC-*) cannot be overridden via CLI usually,
|
|
120
|
+
// but the implementation here depends on how we define "Security".
|
|
121
|
+
// For now, we allow overriding via this function, but the *Caller* should check violation type.
|
|
122
|
+
|
|
123
|
+
await clearSoftLock(rootDir);
|
|
124
|
+
// TODO: Add to Mission Report (Audit Log)
|
|
125
|
+
// We will handle logging in the command handler itself
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export enum LogLevel {
|
|
4
|
+
INFO = 'INFO',
|
|
5
|
+
WARN = 'WARN',
|
|
6
|
+
ERROR = 'ERROR',
|
|
7
|
+
DEBUG = 'DEBUG'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Logger {
|
|
11
|
+
private static formatMessage(level: LogLevel, message: string, context?: unknown): string {
|
|
12
|
+
const timestamp = new Date().toISOString();
|
|
13
|
+
let prefix = '';
|
|
14
|
+
|
|
15
|
+
switch (level) {
|
|
16
|
+
case LogLevel.INFO:
|
|
17
|
+
prefix = chalk.blue(`[${LogLevel.INFO}]`);
|
|
18
|
+
break;
|
|
19
|
+
case LogLevel.WARN:
|
|
20
|
+
prefix = chalk.yellow(`[${LogLevel.WARN}]`);
|
|
21
|
+
break;
|
|
22
|
+
case LogLevel.ERROR:
|
|
23
|
+
prefix = chalk.red(`[${LogLevel.ERROR}]`);
|
|
24
|
+
break;
|
|
25
|
+
case LogLevel.DEBUG:
|
|
26
|
+
prefix = chalk.gray(`[${LogLevel.DEBUG}]`);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let output = `${chalk.gray(timestamp)} ${prefix} ${message}`;
|
|
31
|
+
|
|
32
|
+
if (context) {
|
|
33
|
+
if (context instanceof Error) {
|
|
34
|
+
output += `\n${chalk.red(context.stack || context.message)}`;
|
|
35
|
+
} else if (typeof context === 'object') {
|
|
36
|
+
try {
|
|
37
|
+
output += `\n${chalk.gray(JSON.stringify(context, null, 2))}`;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
output += `\n${chalk.gray('[Circular or invalid object]')}`;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
output += ` ${String(context)}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static info(message: string, context?: unknown) {
|
|
50
|
+
console.log(this.formatMessage(LogLevel.INFO, message, context));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static warn(message: string, context?: unknown) {
|
|
54
|
+
console.warn(this.formatMessage(LogLevel.WARN, message, context));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static error(message: string, error?: unknown) {
|
|
58
|
+
console.error(this.formatMessage(LogLevel.ERROR, message, error));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static debug(message: string, context?: unknown) {
|
|
62
|
+
if (process.env.DEBUG || process.env.RIGSTATE_DEBUG) {
|
|
63
|
+
console.debug(this.formatMessage(LogLevel.DEBUG, message, context));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|