Skip to content

Commit 13c4d01

Browse files
security-pack
1 parent 25d8ce1 commit 13c4d01

4 files changed

Lines changed: 92 additions & 66 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ The installer is **enterprise-grade safe**:
145145
* **Dry-run**: `npx @fabioforest/fgos-kit init --dry-run` (simulate without changes)
146146
* **Audit Logs**: detailed execution logs saved to `.agent/_audit/`
147147
* **Auto-backup**: overwrites automatically create backups (e.g., `.agent.bak-2024...`)
148-
* **Safe-by-default**: never overwrites without `--overwrite` flag
148+
* **Safe-by-default**: incremental updates (adds missing files, preserves existing ones) unless `--overwrite` is used
149149

150150
See [Safety Policy](docs/INSTALL_SAFETY.md) for details.
151151

cli/index.js

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env node
22
/**
3-
* fgos-kit — safe installer
3+
* fgos-kit — safe installer with incremental update (smart merge)
44
* Principles:
5-
* - Do not modify anything without explicit user consent
6-
* - Never delete without showing a detailed plan
7-
* - Always generate an audit log (success and failures)
5+
* - Do not modify existing files without explicit user consent (overwrite)
6+
* - Add missing files/directories (incremental update)
7+
* - Always generate an audit log
88
*/
99

1010
import { Command } from "commander";
@@ -59,14 +59,43 @@ function download(url, outPath) {
5959
}
6060

6161
function ensureDir(p) {
62-
fs.mkdirSync(p, { recursive: true });
62+
if (!fs.existsSync(p)) {
63+
fs.mkdirSync(p, { recursive: true });
64+
return true; // created
65+
}
66+
return false; // existed
6367
}
6468

6569
function safeCopyDir(src, dst) {
66-
// Node >=16 has cpSync
6770
fs.cpSync(src, dst, { recursive: true });
6871
}
6972

73+
// Smart Merge: walks source tree and copies ONLY if dest doesn't exist
74+
// Returns stats: { added: [], skipped: [] }
75+
function smartMerge(src, dst, stats = { added: [], skipped: [] }, rootDst = dst) {
76+
const entries = fs.readdirSync(src, { withFileTypes: true });
77+
78+
ensureDir(dst);
79+
80+
for (const entry of entries) {
81+
const srcPath = path.join(src, entry.name);
82+
const dstPath = path.join(dst, entry.name);
83+
const relPath = path.relative(rootDst, dstPath);
84+
85+
if (entry.isDirectory()) {
86+
smartMerge(srcPath, dstPath, stats, rootDst);
87+
} else {
88+
if (fs.existsSync(dstPath)) {
89+
stats.skipped.push(relPath);
90+
} else {
91+
fs.copyFileSync(srcPath, dstPath);
92+
stats.added.push(relPath);
93+
}
94+
}
95+
}
96+
return stats;
97+
}
98+
7099
function writeAuditLog(targetDir, logObj) {
71100
const logDir = path.join(targetDir, ".agent", "_audit");
72101
ensureDir(logDir);
@@ -101,16 +130,16 @@ function writeAuditLog(targetDir, logObj) {
101130
program
102131
.name("fgos-kit")
103132
.description("Safe installer for Finance Data Governance OS (.agent kit)")
104-
.version("0.2.0");
133+
.version("0.3.0");
105134

106135
program
107136
.command("init")
108-
.description("Initialize .agent/ with governance agents, skills, workflows and rules (safe-by-default)")
137+
.description("Initialize or update .agent/ (safe incremental merge)")
109138
.option("--path <dir>", "target directory", ".")
110139
.option("--branch <name>", "branch to download from", DEFAULT_BRANCH)
111-
.option("--yes", "skip interactive prompts (still creates backup before overwrite)", false)
140+
.option("--yes", "skip interactive prompts", false)
112141
.option("--dry-run", "show plan and exit without changes", false)
113-
.option("--overwrite", "allow overwriting existing .agent content (creates backup first)", false)
142+
.option("--overwrite", "force overwrite existing files (creates backup)", false)
114143
.action(async (opts) => {
115144
const target = path.resolve(opts.path);
116145
const agentDir = path.join(target, ".agent");
@@ -129,104 +158,100 @@ program
129158
};
130159

131160
try {
132-
// 1) Context check (non-destructive)
133-
if (!fs.existsSync(target)) {
134-
throw new Error(`Target path does not exist: ${target}`);
135-
}
161+
if (!fs.existsSync(target)) throw new Error(`Target path does not exist: ${target}`);
136162

137163
const agentExists = fs.existsSync(agentDir);
138164
logObj.plan.push(`Detect target: ${target}`);
139165
logObj.plan.push(`.agent exists: ${agentExists}`);
140166

141-
// 2) Decide what would happen
142-
if (!agentExists) {
143-
logObj.plan.push(`Create .agent/ directory`);
144-
} else {
145-
logObj.plan.push(`Existing .agent/ found`);
146-
logObj.plan.push(`No deletion will happen unless --overwrite is provided and user confirms`);
167+
let mode = "CREATE";
168+
if (agentExists) {
169+
if (opts.overwrite) mode = "OVERWRITE";
170+
else mode = "MERGE";
147171
}
148172

149-
logObj.plan.push(`Download kit from GitHub tarball`);
150-
logObj.plan.push(`Copy: agents/, skills/, workflows/ (if present), rules/ (if present) into .agent/`);
173+
logObj.plan.push(`Operation Mode: ${mode}`);
174+
if (mode === "OVERWRITE") logObj.plan.push(`Backup existing .agent -> Wipe -> Reinstall`);
175+
if (mode === "MERGE") logObj.plan.push(`Incremental update: Add missing files, SKIP existing files`);
151176

152-
// 3) Show plan + require consent
153-
console.log("\n=== PLAN (no changes yet) ===");
177+
// Show plan
178+
console.log("\n=== PLAN ===");
154179
for (const p of logObj.plan) console.log(" -", p);
155180

156181
if (opts.dryRun) {
157-
console.log("\n✅ Dry-run complete. No files were changed.");
158-
return;
159-
}
160-
161-
if (agentExists && !opts.overwrite) {
162-
console.log("\n⚠️ .agent/ already exists.");
163-
console.log("To proceed safely, re-run with: --overwrite");
164-
console.log("Nothing was changed.");
182+
console.log("\n✅ Dry-run complete.");
165183
return;
166184
}
167185

168-
// If overwrite, ask confirmation (unless --yes)
186+
// Confirmation
169187
if (!opts.yes) {
170-
const ok = await askYesNo("\nProceed with installation? (y/N): ");
188+
const ok = await askYesNo(`\nProceed with ${mode}? (y/N): `);
171189
if (!ok) {
172-
console.log("Cancelled. No changes made.");
190+
console.log("Cancelled.");
173191
return;
174192
}
175193
}
176194

177-
// 4) Download & extract (still not touching target until ready)
195+
// Download
178196
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fgos-"));
179197
const tarPath = path.join(tmp, "repo.tar.gz");
180198
const url = `https://codeload.github.com/${REPO}/tar.gz/refs/heads/${opts.branch}`;
181199

182200
console.log("\n📦 Downloading kit...");
183201
await download(url, tarPath);
184-
185-
console.log("📂 Extracting...");
186202
execSync(`tar -xzf "${tarPath}" -C "${tmp}"`);
187-
188203
const extracted = fs.readdirSync(tmp).find((d) => d.startsWith("finance-data-governance-os-"));
189-
if (!extracted) throw new Error("Could not find extracted repository directory");
190204
const repoRoot = path.join(tmp, extracted);
191205

192-
// 5) Backup if overwriting
193-
if (agentExists) {
206+
// Execution
207+
ensureDir(agentDir);
208+
209+
if (mode === "OVERWRITE") {
194210
const backupDir = path.join(target, `.agent.bak-${runId}`);
195211
console.log(`🧷 Creating backup: ${backupDir}`);
196212
safeCopyDir(agentDir, backupDir);
197213
logObj.executed.push(`Backup created: ${backupDir}`);
198-
}
199214

200-
// 6) Apply changes (scoped to .agent only)
201-
ensureDir(agentDir);
215+
// Wipe subdirs to ensure clean slate, but keep .agent root (for audit logs)
216+
const dirsToClear = ["agents", "skills", "workflows", "rules"];
217+
for (const d of dirsToClear) fs.rmSync(path.join(agentDir, d), { recursive: true, force: true });
218+
}
202219

203220
const dirsToCopy = ["agents", "skills", "workflows", "rules"];
221+
let totalAdded = 0;
222+
let totalSkipped = 0;
223+
204224
for (const name of dirsToCopy) {
205225
const src = path.join(repoRoot, name);
206226
const dst = path.join(agentDir, name);
207227

208228
if (fs.existsSync(src)) {
209-
// if overwriting, remove target subdir (not whole .agent)
210-
fs.rmSync(dst, { recursive: true, force: true });
211-
safeCopyDir(src, dst);
212-
console.log(` ✓ ${name}/`);
213-
logObj.executed.push(`Copied ${name}/ to .agent/${name}/`);
229+
if (mode === "OVERWRITE") {
230+
safeCopyDir(src, dst);
231+
console.log(` ✓ ${name}/ (overwritten)`);
232+
logObj.executed.push(`Overwritten ${name}/`);
233+
} else {
234+
// MERGE logic
235+
const stats = smartMerge(src, dst, undefined, agentDir);
236+
console.log(` ✓ ${name}/ (added: ${stats.added.length}, skipped: ${stats.skipped.length})`);
237+
logObj.executed.push(`Merged ${name}/: ${stats.added.length} new, ${stats.skipped.length} skipped`);
238+
totalAdded += stats.added.length;
239+
totalSkipped += stats.skipped.length;
240+
}
214241
}
215242
}
216243

217-
// 7) Write audit logs
218-
writeAuditLog(target, logObj);
219-
220-
console.log("\n✅ Installed safely.");
221-
console.log(`Audit log saved at: ${path.join(target, ".agent", "_audit")}`);
244+
if (mode === "MERGE") {
245+
console.log(`\nStats: ${totalAdded} files added, ${totalSkipped} files preserved.`);
246+
}
222247

223-
// cleanup
248+
writeAuditLog(target, logObj);
249+
console.log("\n✅ Done.");
224250
fs.rmSync(tmp, { recursive: true, force: true });
251+
225252
} catch (err) {
226-
logObj.errors.push(String(err?.stack || err?.message || err));
227-
try {
228-
writeAuditLog(path.resolve(opts.path), logObj);
229-
} catch { }
253+
logObj.errors.push(String(err?.stack || err?.message));
254+
try { writeAuditLog(path.resolve(opts.path), logObj); } catch { }
230255
console.error("\n❌ Error:", err.message);
231256
process.exit(1);
232257
}

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fabioforest/fgos-kit",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Installer CLI for Finance Data Governance OS (.agent kit)",
55
"bin": {
66
"fgos-kit": "index.js"

docs/INSTALL_SAFETY.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ The Finance Data Governance OS installer (`fgos-kit`) is designed with a **Safet
55
## Core Safety Principles
66

77
1. **Non-Destructive by Default**: The installer will never overwrite existing files in `.agent/` without your explicit permission (`--overwrite` flag).
8-
2. **Consent-First**: Unless running in CI mode (`--yes`), the installer will always ask for confirmation before making changes.
8+
2. **Incremental Updates**: If `.agent/` exists, the installer enters **Merge Mode**. It adds missing files (new agents/skills) but **skips** any file that already exists to preserve your customizations.
9+
3. **Consent-First**: Unless running in CI mode (`--yes`), the installer will always ask for confirmation before making changes.
910
3. **Audit Trails**: Every execution generates a detailed log file (Markdown and JSON) documenting exactly what was done.
1011
4. **Automatic Backups**: Before overwriting any content, a timestamped backup of the previous version is created automatically.
1112

@@ -18,13 +19,13 @@ Audit logs are stored in `.agent/_audit/`.
1819

1920
## Usage Modes
2021

21-
### Safe Mode (Default)
22+
### Safe Mode (Default / Incremental)
2223
```bash
2324
npx @fabioforest/fgos-kit init
2425
```
2526
- Checks if `.agent/` exists.
26-
- If it exists, aborts with a warning.
27-
- If not, asks for confirmation to create it.
27+
- **If not exists**: Creates full structure.
28+
- **If exists**: Scans for missing files and adds them (Smart Merge). **Existing files are preserved.**
2829

2930
### Simulation Mode (Dry Run)
3031
```bash

0 commit comments

Comments
 (0)