@greenarmor/ges
Advanced tools
| import { Command } from "commander"; | ||
| export declare const assignCommand: Command; |
| import { Command } from "commander"; | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import { ensureGESInitialized } from "../utils/project.js"; | ||
| import { input, select } from "../utils/prompts.js"; | ||
| import { banner, divider, blank, success, error as errorOut, warn, info, kv, BOLD, CYAN, GREEN, YELLOW, } from "../utils/ui.js"; | ||
| import { loadFixAssignments, createFixAssignment, addFixAssignment, resolveFixAssignment, findingKey, loadGovernanceRecords, recordActivity, } from "@greenarmor/ges-core"; | ||
| export const assignCommand = new Command("assign") | ||
| .description("Assign pending fixes to governance provenance records") | ||
| .option("--finding <key>", "Finding key (ruleId:file:line) to assign") | ||
| .option("--record <id>", "Governance record ID or system name") | ||
| .option("--assignee <name>", "Person assigned to fix this") | ||
| .option("--assignee-role <role>", "Role of the assignee") | ||
| .option("--notes <notes>", "Notes for this assignment") | ||
| .option("--actor <name>", "Your name (for audit trail)") | ||
| .option("--actor-role <role>", "Your role (for audit trail)") | ||
| .option("--list", "List all fix assignments") | ||
| .option("--resolve <key>", "Resolve a fix assignment by finding key") | ||
| .option("--by <name>", "Who resolved the fix (for --resolve)") | ||
| .option("--by-role <role>", "Role of resolver (for --resolve)") | ||
| .option("--method <method>", "Resolution method: auto-fix, manual, not-applicable") | ||
| .option("--resolution-notes <notes>", "Notes about the resolution") | ||
| .action(async (options) => { | ||
| const root = ensureGESInitialized(); | ||
| if (!root) | ||
| return; | ||
| if (options.list) { | ||
| listAssignments(root); | ||
| return; | ||
| } | ||
| if (options.resolve) { | ||
| resolveAssignment(root, options.resolve, options); | ||
| return; | ||
| } | ||
| await assignFinding(root, options); | ||
| }); | ||
| function loadFindingsForAssign(root) { | ||
| const auditPath = path.join(root, ".ges", "last-audit.json"); | ||
| try { | ||
| const raw = fs.readFileSync(auditPath, "utf-8"); | ||
| const data = JSON.parse(raw); | ||
| if (data.findings && Array.isArray(data.findings)) { | ||
| return data.findings; | ||
| } | ||
| } | ||
| catch { | ||
| // no audit yet | ||
| } | ||
| return []; | ||
| } | ||
| function listAssignments(root) { | ||
| banner("Fix Assignments", "Pending fixes linked to governance provenance records"); | ||
| const assignments = loadFixAssignments(root); | ||
| if (assignments.length === 0) { | ||
| warn("No fix assignments found.", "Run `ges assign` to assign a pending fix to a governance record."); | ||
| blank(); | ||
| return; | ||
| } | ||
| blank(); | ||
| for (const a of assignments) { | ||
| const statusIcon = a.status === "fixed" || a.status === "verified" ? "●" : a.status === "in-progress" ? "◐" : "○"; | ||
| const statusColor = a.status === "fixed" || a.status === "verified" ? GREEN : a.status === "in-progress" ? CYAN : YELLOW; | ||
| console.log(` ${statusColor(statusIcon)} ${BOLD(a.finding_rule_id)} — ${a.finding_title}`); | ||
| kv("Finding Key", a.finding_key); | ||
| kv("Location", `${a.finding_file}${a.finding_line ? ":" + a.finding_line : ""}`); | ||
| kv("Governance Record", a.governance_system_name); | ||
| kv("Assignee", `${a.assignee}${a.assignee_role ? " (" + a.assignee_role + ")" : ""}`); | ||
| kv("Status", a.status); | ||
| kv("Assigned By", a.assigned_by); | ||
| kv("Assigned At", new Date(a.assigned_at).toLocaleString()); | ||
| if (a.resolution) { | ||
| kv("Resolved By", `${a.resolution.resolved_by}${a.resolution.resolved_by_role ? " (" + a.resolution.resolved_by_role + ")" : ""}`); | ||
| kv("Method", a.resolution.method); | ||
| kv("Resolved At", new Date(a.resolution.resolved_at).toLocaleString()); | ||
| if (a.resolution.resolution_notes) | ||
| kv("Resolution Notes", a.resolution.resolution_notes); | ||
| } | ||
| if (a.notes) | ||
| kv("Notes", a.notes); | ||
| blank(); | ||
| } | ||
| divider(); | ||
| info("Total assignments", String(assignments.length)); | ||
| blank(); | ||
| } | ||
| async function assignFinding(root, options) { | ||
| banner("Assign Fix to Governance Record", "Link a pending fix to a provenance chain"); | ||
| const findings = loadFindingsForAssign(root); | ||
| if (findings.length === 0) { | ||
| errorOut("No findings found.", "Run `ges audit` first to generate findings."); | ||
| blank(); | ||
| return; | ||
| } | ||
| const records = loadGovernanceRecords(root); | ||
| if (records.length === 0) { | ||
| errorOut("No governance records found.", "Create one first with `ges governance add`."); | ||
| blank(); | ||
| return; | ||
| } | ||
| const assignments = loadFixAssignments(root); | ||
| const assignedKeys = new Set(assignments.map(a => a.finding_key)); | ||
| let selectedFinding; | ||
| if (options.finding) { | ||
| selectedFinding = findings.find(f => findingKey({ ruleId: f.ruleId, file: f.file, line: f.line }) === options.finding); | ||
| if (!selectedFinding) { | ||
| errorOut("Finding not found", `No finding with key: ${options.finding}`); | ||
| blank(); | ||
| return; | ||
| } | ||
| } | ||
| else { | ||
| const choices = findings.map(f => { | ||
| const fkey = findingKey({ ruleId: f.ruleId, file: f.file, line: f.line }); | ||
| const isAssigned = assignedKeys.has(fkey); | ||
| return { | ||
| name: `${f.severity.toUpperCase()} ${f.ruleId} — ${f.title} (${f.file}${f.line ? ":" + f.line : ""})${isAssigned ? " [ASSIGNED]" : ""}`, | ||
| value: fkey, | ||
| disabled: isAssigned, | ||
| }; | ||
| }); | ||
| const fkey = await select({ message: "Select a finding to assign:", choices }); | ||
| selectedFinding = findings.find(f => findingKey({ ruleId: f.ruleId, file: f.file, line: f.line }) === fkey); | ||
| } | ||
| if (!selectedFinding) { | ||
| errorOut("No finding selected."); | ||
| return; | ||
| } | ||
| const fkey = findingKey({ ruleId: selectedFinding.ruleId, file: selectedFinding.file, line: selectedFinding.line }); | ||
| if (assignedKeys.has(fkey)) { | ||
| warn("This finding is already assigned.", "Use `ges assign --list` to see current assignments."); | ||
| blank(); | ||
| return; | ||
| } | ||
| let recordId = options.record; | ||
| let selectedRecord = recordId | ||
| ? records.find(r => r.id === recordId || r.system_name.toLowerCase() === recordId.toLowerCase()) | ||
| : undefined; | ||
| if (!selectedRecord) { | ||
| if (recordId) { | ||
| errorOut("Governance record not found", recordId); | ||
| return; | ||
| } | ||
| const recordChoices = records.map(r => ({ | ||
| name: `${r.system_name} (${r.status}, ${r.risk_level} risk)`, | ||
| value: r.id, | ||
| })); | ||
| recordId = await select({ message: "Select governance record:", choices: recordChoices }); | ||
| selectedRecord = records.find(r => r.id === recordId); | ||
| } | ||
| if (!selectedRecord) { | ||
| errorOut("Governance record not found."); | ||
| return; | ||
| } | ||
| const assignee = options.assignee || await input({ message: "Assignee name:" }); | ||
| if (!assignee.trim()) { | ||
| errorOut("Assignee name is required."); | ||
| return; | ||
| } | ||
| const assigneeRole = options.assigneeRole || await input({ message: "Assignee role (optional):" }); | ||
| const notes = options.notes || await input({ message: "Notes (optional):" }); | ||
| const actorName = options.actor || await input({ message: "Your name (for audit trail):" }); | ||
| const actorRole = options.actorRole || await input({ message: "Your role (optional):" }); | ||
| const assignment = createFixAssignment({ | ||
| finding_key: fkey, | ||
| finding_rule_id: selectedFinding.ruleId, | ||
| finding_title: selectedFinding.title, | ||
| finding_file: selectedFinding.file, | ||
| finding_line: selectedFinding.line, | ||
| finding_severity: selectedFinding.severity, | ||
| finding_control_ids: selectedFinding.controlIds, | ||
| governance_record_id: selectedRecord.id, | ||
| governance_system_name: selectedRecord.system_name, | ||
| assignee: assignee.trim(), | ||
| assignee_role: assigneeRole.trim(), | ||
| assigned_by: actorName.trim() || "cli", | ||
| notes: notes.trim(), | ||
| }); | ||
| addFixAssignment(root, assignment); | ||
| recordActivity(root, { | ||
| source: "cli", | ||
| action: "fix_assign", | ||
| title: `Fix assigned: ${selectedFinding.ruleId} → ${selectedRecord.system_name}`, | ||
| description: `Assigned ${selectedFinding.ruleId} (${selectedFinding.title}) to ${assignee} (${assigneeRole || "unspecified role"}), linked to governance record ${selectedRecord.system_name}.`, | ||
| details: { | ||
| finding_key: fkey, | ||
| governance_record_id: selectedRecord.id, | ||
| assignee, | ||
| governance_system_name: selectedRecord.system_name, | ||
| }, | ||
| actor_name: actorName.trim() || undefined, | ||
| actor_role: actorRole.trim() || undefined, | ||
| }); | ||
| blank(); | ||
| success("Fix assigned to governance record"); | ||
| kv("Finding", `${selectedFinding.ruleId} — ${selectedFinding.title}`); | ||
| kv("Location", `${selectedFinding.file}${selectedFinding.line ? ":" + selectedFinding.line : ""}`); | ||
| kv("Governance Record", selectedRecord.system_name); | ||
| kv("Assignee", `${assignee}${assigneeRole ? " (" + assigneeRole + ")" : ""}`); | ||
| kv("Finding Key", fkey); | ||
| blank(); | ||
| info("Provenance chain", `Fix → ${selectedRecord.system_name} → ${selectedRecord.approval ? selectedRecord.approval.approver_name + " (approved)" : "no approval yet"}`); | ||
| blank(); | ||
| } | ||
| function resolveAssignment(root, fkey, options) { | ||
| const existing = loadFixAssignments(root).find(a => a.finding_key === fkey); | ||
| if (!existing) { | ||
| errorOut("Fix assignment not found", `No assignment for finding key: ${fkey}`); | ||
| blank(); | ||
| return; | ||
| } | ||
| const resolver = options.by || "cli"; | ||
| const resolverRole = options.byRole || ""; | ||
| const method = options.method || "manual"; | ||
| const notes = options.resolutionNotes || ""; | ||
| const resolved = resolveFixAssignment(root, fkey, { | ||
| resolved_by: resolver, | ||
| resolved_by_role: resolverRole, | ||
| method: method, | ||
| resolution_notes: notes, | ||
| }); | ||
| if (!resolved) { | ||
| errorOut("Failed to resolve assignment."); | ||
| return; | ||
| } | ||
| recordActivity(root, { | ||
| source: "cli", | ||
| action: "fix_resolve", | ||
| title: `Fix resolved: ${resolved.finding_rule_id}`, | ||
| description: `Resolved ${resolved.finding_rule_id} via ${method} by ${resolver}.`, | ||
| details: { | ||
| finding_key: fkey, | ||
| governance_record_id: resolved.governance_record_id, | ||
| method, | ||
| }, | ||
| actor_name: resolver, | ||
| actor_role: resolverRole || undefined, | ||
| }); | ||
| blank(); | ||
| success("Fix assignment resolved"); | ||
| kv("Finding", `${resolved.finding_rule_id} — ${resolved.finding_title}`); | ||
| kv("Governance Record", resolved.governance_system_name); | ||
| kv("Resolved By", `${resolver}${resolverRole ? " (" + resolverRole + ")" : ""}`); | ||
| kv("Method", method); | ||
| blank(); | ||
| } |
+2
-0
@@ -21,2 +21,3 @@ #!/usr/bin/env node | ||
| import { governanceCommand } from "./commands/governance.js"; | ||
| import { assignCommand } from "./commands/assign.js"; | ||
| import { CLI_VERSION } from "./utils/version.js"; | ||
@@ -46,2 +47,3 @@ const program = new Command(); | ||
| program.addCommand(governanceCommand); | ||
| program.addCommand(assignCommand); | ||
| program.parse(); |
@@ -158,9 +158,9 @@ import { Command } from "commander"; | ||
| if (!existing.includes(".dev-logs/")) { | ||
| fs.appendFileSync(gitignorePath, `\n# GESF developer logs (not for remote)\n${devLogsIgnore}`); | ||
| fs.appendFileSync(gitignorePath, `\n# GESF developer logs (developer-only, not for remote)\n${devLogsIgnore}`); | ||
| } | ||
| } | ||
| else { | ||
| writeFileSync(gitignorePath, `# GESF developer logs (not for remote)\n${devLogsIgnore}\n`); | ||
| writeFileSync(gitignorePath, `# GESF developer logs (developer-only, not for remote)\n${devLogsIgnore}\n`); | ||
| } | ||
| writeFileSync(path.join(process.cwd(), ".dev-logs", "README.md"), `# Developer Logs\n\nThis directory is for GESF development notes, session logs, AI recommendations, and release notes.\n\n**This directory is gitignored and intended for developers only. Do not submit to remote.**\n\n## Structure\n\n- \`session-*.md\` — Session logs\n- \`release-notes-*.md\` — Release notes\n- \`ai-recommendations/\` — Recommendations from AI assistants using the MCP server\n`); | ||
| writeFileSync(path.join(process.cwd(), ".dev-logs", "README.md"), `# Developer Logs\n\nThis directory is part of GESF — the Green Engineering Standard Framework.\n\nIt stores development notes, session logs, AI assistant recommendations, and release notes for your project.\n\n**This directory is gitignored and intended for developers only. Do not submit to remote.**\n\n## Structure\n\n- \`session-*.md\` — Session logs (if using GESF for development tracking)\n- \`release-notes-*.md\` — Release notes for your project\n- \`ai-recommendations/\` — Recommendations from AI assistants using the MCP server (for human review)\n`); | ||
| const configJson = generateConfigJson(config); | ||
@@ -167,0 +167,0 @@ writeFileSync(path.join(process.cwd(), configJson.filePath), configJson.content); |
+14
-14
@@ -6,15 +6,15 @@ { | ||
| "dependencies": { | ||
| "@greenarmor/ges-audit-engine": "1.4.3", | ||
| "@greenarmor/ges-cicd-generator": "1.4.3", | ||
| "@greenarmor/ges-compliance-engine": "1.4.3", | ||
| "@greenarmor/ges-core": "1.4.3", | ||
| "@greenarmor/ges-doc-generator": "1.4.3", | ||
| "@greenarmor/ges-git-hooks": "1.4.3", | ||
| "@greenarmor/ges-mcp-server": "1.4.3", | ||
| "@greenarmor/ges-policy-engine": "1.4.3", | ||
| "@greenarmor/ges-report-generator": "1.4.3", | ||
| "@greenarmor/ges-rules-engine": "1.4.3", | ||
| "@greenarmor/ges-scanner-integration": "1.4.3", | ||
| "@greenarmor/ges-scoring-engine": "1.4.3", | ||
| "@greenarmor/ges-web-dashboard": "1.4.3", | ||
| "@greenarmor/ges-audit-engine": "1.5.0", | ||
| "@greenarmor/ges-cicd-generator": "1.5.0", | ||
| "@greenarmor/ges-compliance-engine": "1.5.0", | ||
| "@greenarmor/ges-core": "1.5.0", | ||
| "@greenarmor/ges-doc-generator": "1.5.0", | ||
| "@greenarmor/ges-git-hooks": "1.5.0", | ||
| "@greenarmor/ges-mcp-server": "1.5.0", | ||
| "@greenarmor/ges-policy-engine": "1.5.0", | ||
| "@greenarmor/ges-report-generator": "1.5.0", | ||
| "@greenarmor/ges-rules-engine": "1.5.0", | ||
| "@greenarmor/ges-scanner-integration": "1.5.0", | ||
| "@greenarmor/ges-scoring-engine": "1.5.0", | ||
| "@greenarmor/ges-web-dashboard": "1.5.0", | ||
| "chalk": "^5.6.2", | ||
@@ -64,3 +64,3 @@ "commander": "^13.0.0" | ||
| "types": "./dist/index.d.ts", | ||
| "version": "1.4.3", | ||
| "version": "1.5.0", | ||
| "scripts": { | ||
@@ -67,0 +67,0 @@ "build": "tsc", |
147041
8.36%57
3.64%3233
8.38%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated