mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Implement teardown logic
This commit is contained in:
parent
49ec685546
commit
dabb758104
6 changed files with 345 additions and 49 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { setup } from "../src/setup.js";
|
import { setup } from "../src/setup.js";
|
||||||
|
import { teardown } from "../src/teardown.js";
|
||||||
|
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
console.error("No command provided. Please provide a command to execute.");
|
console.error("No command provided. Please provide a command to execute.");
|
||||||
|
|
@ -19,6 +20,9 @@ if (command === "help" || command === "--help" || command === "-h") {
|
||||||
if (command === "setup") {
|
if (command === "setup") {
|
||||||
const configFile = process.argv[3];
|
const configFile = process.argv[3];
|
||||||
setup(configFile);
|
setup(configFile);
|
||||||
|
} else if (command === "teardown") {
|
||||||
|
const configFile = process.argv[3];
|
||||||
|
teardown(configFile);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Unknown command: ${command}.`);
|
console.error(`Unknown command: ${command}.`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
@ -29,9 +33,11 @@ if (command === "setup") {
|
||||||
function writeHelp() {
|
function writeHelp() {
|
||||||
console.log("Usage: safe-chain-bun <command>");
|
console.log("Usage: safe-chain-bun <command>");
|
||||||
console.log();
|
console.log();
|
||||||
console.log("Available commands: setup, help");
|
console.log("Available commands: setup, teardown, help");
|
||||||
console.log();
|
console.log();
|
||||||
console.log("- safe-chain-bun setup: Register Safe-Chain-Bun as a security scanner in ~/.bunfig.toml");
|
console.log("- safe-chain-bun setup: Register Safe-Chain-Bun as a security scanner in ~/.bunfig.toml");
|
||||||
console.log("- safe-chain-bun setup <file>: Register Safe-Chain-Bun as a security scanner in specified bunfig.toml file");
|
console.log("- safe-chain-bun setup <file>: Register Safe-Chain-Bun as a security scanner in specified bunfig.toml file");
|
||||||
|
console.log("- safe-chain-bun teardown: Remove Safe-Chain-Bun scanner from ~/.bunfig.toml");
|
||||||
|
console.log("- safe-chain-bun teardown <file>: Remove Safe-Chain-Bun scanner from specified bunfig.toml file");
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import { getGlobalConfigPath, addScannerToToml } from "./toml-utils.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main setup function that registers safe-chain-bun as a security scanner
|
* Main setup function that registers safe-chain-bun as a security scanner
|
||||||
|
|
@ -31,13 +31,6 @@ export function setup(configFile) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the global bunfig.toml path
|
|
||||||
* @returns {string} Path to global bunfig.toml
|
|
||||||
*/
|
|
||||||
function getGlobalConfigPath() {
|
|
||||||
return path.join(os.homedir(), ".bunfig.toml");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates or creates a bunfig.toml file with safe-chain-bun scanner configuration
|
* Updates or creates a bunfig.toml file with safe-chain-bun scanner configuration
|
||||||
|
|
@ -86,42 +79,3 @@ function updateBunfigFile(filePath, isGlobal) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds or updates the scanner configuration in TOML content
|
|
||||||
* @param {string} content - Existing TOML content
|
|
||||||
* @returns {{content: string, changed: boolean}} Updated content and change status
|
|
||||||
*/
|
|
||||||
export function addScannerToToml(content) {
|
|
||||||
const scannerLine = 'scanner = "@aikidosec/safe-chain-bun"';
|
|
||||||
|
|
||||||
if (content.includes(scannerLine)) {
|
|
||||||
return { content, changed: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = content.split(/[\r\n\u2028\u2029]+/);
|
|
||||||
const installSecurityRegex = /^\[install\.security\]$/;
|
|
||||||
const scannerRegex = /^scanner\s*=.*$/;
|
|
||||||
|
|
||||||
const securitySectionIndex = lines.findIndex(line => installSecurityRegex.test(line));
|
|
||||||
|
|
||||||
if (securitySectionIndex >= 0) {
|
|
||||||
const scannerLineIndex = lines.findIndex((line, index) =>
|
|
||||||
index > securitySectionIndex && scannerRegex.test(line)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (scannerLineIndex >= 0) {
|
|
||||||
lines[scannerLineIndex] = scannerLine;
|
|
||||||
} else {
|
|
||||||
lines.splice(securitySectionIndex + 1, 0, scannerLine);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (lines[lines.length - 1] !== '') {
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
lines.push('[install.security]');
|
|
||||||
lines.push(scannerLine);
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content: lines.join(os.EOL), changed: true };
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,8 @@ import assert from "node:assert";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { addScannerToToml, setup } from "./setup.js";
|
import { addScannerToToml } from "./toml-utils.js";
|
||||||
|
import { setup } from "./setup.js";
|
||||||
|
|
||||||
describe("addScannerToToml", () => {
|
describe("addScannerToToml", () => {
|
||||||
it("should add scanner to empty content", () => {
|
it("should add scanner to empty content", () => {
|
||||||
|
|
|
||||||
35
packages/safe-chain-bun/src/teardown.js
Normal file
35
packages/safe-chain-bun/src/teardown.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { getGlobalConfigPath, removeScannerFromToml } from "./toml-utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main teardown function that removes safe-chain-bun as a security scanner
|
||||||
|
* @param {string|undefined} configFile - Optional path to specific bunfig.toml file
|
||||||
|
*/
|
||||||
|
export function teardown(configFile) {
|
||||||
|
try {
|
||||||
|
const targetFile = configFile ? path.resolve(configFile) : getGlobalConfigPath();
|
||||||
|
const isGlobal = !configFile;
|
||||||
|
|
||||||
|
if (!fs.existsSync(targetFile)) {
|
||||||
|
const displayPath = isGlobal ? "~/.bunfig.toml" : configFile;
|
||||||
|
console.log(`ℹ️ Config file not found: ${displayPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(targetFile, "utf8");
|
||||||
|
const result = removeScannerFromToml(content);
|
||||||
|
|
||||||
|
if (result.changed) {
|
||||||
|
fs.writeFileSync(targetFile, result.content, "utf8");
|
||||||
|
const displayPath = isGlobal ? "~/.bunfig.toml" : configFile;
|
||||||
|
console.log(`✅ Safe-Chain-Bun scanner removed from ${displayPath}`);
|
||||||
|
} else {
|
||||||
|
const displayPath = isGlobal ? "~/.bunfig.toml" : configFile;
|
||||||
|
console.log(`ℹ️ Safe-Chain-Bun scanner not found in ${displayPath}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to remove Safe-Chain-Bun scanner: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
packages/safe-chain-bun/src/teardown.spec.js
Normal file
182
packages/safe-chain-bun/src/teardown.spec.js
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { describe, it, before, after, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { removeScannerFromToml } from "./toml-utils.js";
|
||||||
|
import { teardown } from "./teardown.js";
|
||||||
|
|
||||||
|
describe("removeScannerFromToml", () => {
|
||||||
|
it("should return unchanged if scanner not present", () => {
|
||||||
|
const input = `[build]\ntarget = "node"`;
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, false);
|
||||||
|
assert.strictEqual(result.content, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove scanner line only", () => {
|
||||||
|
const input = `[install.security]\nscanner = "@aikidosec/safe-chain-bun"\nother = "config"`;
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
|
||||||
|
assert.ok(result.content.includes('other = "config"'));
|
||||||
|
assert.ok(result.content.includes('[install.security]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove entire [install.security] section if only scanner present", () => {
|
||||||
|
const input = `[build]\ntarget = "node"\n\n[install.security]\nscanner = "@aikidosec/safe-chain-bun"\n\n[test]\npreload = "./setup.ts"`;
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
assert.ok(!result.content.includes('[install.security]'));
|
||||||
|
assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
|
||||||
|
assert.ok(result.content.includes('[build]'));
|
||||||
|
assert.ok(result.content.includes('[test]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle section with comments and whitespace", () => {
|
||||||
|
const input = `[install.security]\n# Security configuration\nscanner = "@aikidosec/safe-chain-bun"\n\n# End of security`;
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
assert.ok(!result.content.includes('[install.security]'));
|
||||||
|
assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve other scanner configurations", () => {
|
||||||
|
const input = `[install.security]\nscanner = "@other/scanner"`;
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, false);
|
||||||
|
assert.strictEqual(result.content, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed line endings", () => {
|
||||||
|
const input = `[install.security]\r\nscanner = "@aikidosec/safe-chain-bun"\r\n\r\n[build]\r\ntarget = "node"`;
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
assert.ok(!result.content.includes('[install.security]'));
|
||||||
|
assert.ok(result.content.includes('[build]'));
|
||||||
|
assert.ok(result.content.includes('target = "node"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex TOML with multiple sections", () => {
|
||||||
|
const input = `[install]
|
||||||
|
registry = "https://registry.npmjs.org/"
|
||||||
|
|
||||||
|
[install.security]
|
||||||
|
scanner = "@aikidosec/safe-chain-bun"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
target = "node"`;
|
||||||
|
|
||||||
|
const result = removeScannerFromToml(input);
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
assert.ok(!result.content.includes('[install.security]'));
|
||||||
|
assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
|
||||||
|
assert.ok(result.content.includes('[install]'));
|
||||||
|
assert.ok(result.content.includes('[build]'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("teardown function", () => {
|
||||||
|
let tempDir;
|
||||||
|
let originalConsoleLog;
|
||||||
|
let originalConsoleError;
|
||||||
|
let consoleOutput;
|
||||||
|
let consoleErrors;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "safe-chain-bun-teardown-test-"));
|
||||||
|
|
||||||
|
consoleOutput = [];
|
||||||
|
consoleErrors = [];
|
||||||
|
originalConsoleLog = console.log;
|
||||||
|
originalConsoleError = console.error;
|
||||||
|
console.log = (...args) => consoleOutput.push(args.join(" "));
|
||||||
|
console.error = (...args) => consoleErrors.push(args.join(" "));
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
console.log = originalConsoleLog;
|
||||||
|
console.error = originalConsoleError;
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to reset console mocks before each test
|
||||||
|
const resetConsole = () => {
|
||||||
|
consoleOutput.length = 0;
|
||||||
|
consoleErrors.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should remove scanner from global config", () => {
|
||||||
|
resetConsole();
|
||||||
|
const mockHomedir = mock.method(os, "homedir", () => tempDir);
|
||||||
|
const globalConfigPath = path.join(tempDir, ".bunfig.toml");
|
||||||
|
|
||||||
|
fs.writeFileSync(globalConfigPath, `[install.security]\nscanner = "@aikidosec/safe-chain-bun"`);
|
||||||
|
|
||||||
|
teardown();
|
||||||
|
|
||||||
|
const content = fs.readFileSync(globalConfigPath, "utf8");
|
||||||
|
assert.ok(!content.includes('scanner = "@aikidosec/safe-chain-bun"'));
|
||||||
|
assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun scanner removed")));
|
||||||
|
|
||||||
|
mockHomedir.mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove scanner from specific file", () => {
|
||||||
|
resetConsole();
|
||||||
|
const specificConfigPath = path.join(tempDir, "project-bunfig.toml");
|
||||||
|
fs.writeFileSync(specificConfigPath, `[build]\ntarget = "node"\n\n[install.security]\nscanner = "@aikidosec/safe-chain-bun"`);
|
||||||
|
|
||||||
|
teardown(specificConfigPath);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(specificConfigPath, "utf8");
|
||||||
|
assert.ok(!content.includes('scanner = "@aikidosec/safe-chain-bun"'));
|
||||||
|
assert.ok(!content.includes('[install.security]'));
|
||||||
|
assert.ok(content.includes('[build]'));
|
||||||
|
assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun scanner removed")));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file not found gracefully", () => {
|
||||||
|
resetConsole();
|
||||||
|
const nonExistentPath = path.join(tempDir, "does-not-exist.toml");
|
||||||
|
|
||||||
|
teardown(nonExistentPath);
|
||||||
|
|
||||||
|
assert.ok(consoleOutput.some(msg => msg.includes("ℹ️ Config file not found")));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should report when scanner not found", () => {
|
||||||
|
resetConsole();
|
||||||
|
const specificConfigPath = path.join(tempDir, "no-scanner.toml");
|
||||||
|
fs.writeFileSync(specificConfigPath, `[build]\ntarget = "node"`);
|
||||||
|
|
||||||
|
teardown(specificConfigPath);
|
||||||
|
|
||||||
|
assert.ok(consoleOutput.some(msg => msg.includes("ℹ️ Safe-Chain-Bun scanner not found")));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle permission errors gracefully", () => {
|
||||||
|
resetConsole();
|
||||||
|
const mockHomedir = mock.method(os, "homedir", () => tempDir);
|
||||||
|
const globalConfigPath = path.join(tempDir, ".bunfig.toml");
|
||||||
|
|
||||||
|
fs.writeFileSync(globalConfigPath, `[install.security]\nscanner = "@aikidosec/safe-chain-bun"`);
|
||||||
|
fs.chmodSync(globalConfigPath, 0o444); // Read-only
|
||||||
|
|
||||||
|
let exitCode;
|
||||||
|
const mockExit = mock.method(process, "exit", (code) => {
|
||||||
|
exitCode = code;
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown();
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 1);
|
||||||
|
assert.ok(consoleErrors.some(msg => msg.includes("❌ Failed to remove Safe-Chain-Bun scanner")));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
fs.chmodSync(globalConfigPath, 0o644);
|
||||||
|
|
||||||
|
mockExit.mock.restore();
|
||||||
|
mockHomedir.mock.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
118
packages/safe-chain-bun/src/toml-utils.js
Normal file
118
packages/safe-chain-bun/src/toml-utils.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the global bunfig.toml path
|
||||||
|
* @returns {string} Path to global bunfig.toml
|
||||||
|
*/
|
||||||
|
export function getGlobalConfigPath() {
|
||||||
|
return path.join(os.homedir(), ".bunfig.toml");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates the scanner configuration in TOML content
|
||||||
|
* @param {string} content - Existing TOML content
|
||||||
|
* @returns {{content: string, changed: boolean}} Updated content and change status
|
||||||
|
*/
|
||||||
|
export function addScannerToToml(content) {
|
||||||
|
const scannerLine = 'scanner = "@aikidosec/safe-chain-bun"';
|
||||||
|
|
||||||
|
if (content.includes(scannerLine)) {
|
||||||
|
return { content, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split(/[\r\n\u2028\u2029]+/);
|
||||||
|
const installSecurityRegex = /^\[install\.security\]$/;
|
||||||
|
const scannerRegex = /^scanner\s*=.*$/;
|
||||||
|
|
||||||
|
const securitySectionIndex = lines.findIndex(line => installSecurityRegex.test(line));
|
||||||
|
|
||||||
|
if (securitySectionIndex >= 0) {
|
||||||
|
const scannerLineIndex = lines.findIndex((line, index) =>
|
||||||
|
index > securitySectionIndex && scannerRegex.test(line)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scannerLineIndex >= 0) {
|
||||||
|
lines[scannerLineIndex] = scannerLine;
|
||||||
|
} else {
|
||||||
|
lines.splice(securitySectionIndex + 1, 0, scannerLine);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lines[lines.length - 1] !== '') {
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
lines.push('[install.security]');
|
||||||
|
lines.push(scannerLine);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: lines.join(os.EOL), changed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes safe-chain-bun scanner configuration from TOML content
|
||||||
|
* @param {string} content - Existing TOML content
|
||||||
|
* @returns {{content: string, changed: boolean}} Updated content and change status
|
||||||
|
*/
|
||||||
|
export function removeScannerFromToml(content) {
|
||||||
|
const scannerLine = 'scanner = "@aikidosec/safe-chain-bun"';
|
||||||
|
|
||||||
|
if (!content.includes(scannerLine)) {
|
||||||
|
return { content, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split(/[\r\n\u2028\u2029]+/);
|
||||||
|
const installSecurityRegex = /^\[install\.security\]$/;
|
||||||
|
const scannerRegex = /^scanner\s*=\s*"@aikidosec\/safe-chain-bun"$/;
|
||||||
|
|
||||||
|
const securitySectionIndex = lines.findIndex(line => installSecurityRegex.test(line));
|
||||||
|
|
||||||
|
if (securitySectionIndex >= 0) {
|
||||||
|
const scannerLineIndex = lines.findIndex((line, index) =>
|
||||||
|
index > securitySectionIndex && scannerRegex.test(line)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scannerLineIndex >= 0) {
|
||||||
|
lines.splice(scannerLineIndex, 1);
|
||||||
|
|
||||||
|
// Check if [install.security] section is now empty
|
||||||
|
let isEmpty = true;
|
||||||
|
for (let i = securitySectionIndex + 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (line === '') continue;
|
||||||
|
if (line.startsWith('#')) continue;
|
||||||
|
if (line.startsWith('[')) break;
|
||||||
|
isEmpty = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
let sectionEnd = lines.length;
|
||||||
|
for (let i = securitySectionIndex + 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (line.startsWith('[')) {
|
||||||
|
sectionEnd = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let removeStart = securitySectionIndex;
|
||||||
|
let removeEnd = sectionEnd;
|
||||||
|
|
||||||
|
while (removeEnd > securitySectionIndex + 1 && lines[removeEnd - 1].trim() === '') {
|
||||||
|
removeEnd--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeStart > 0 && lines[removeStart - 1].trim() === '') {
|
||||||
|
removeStart--;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.splice(removeStart, removeEnd - removeStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: lines.join(os.EOL), changed: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content, changed: false };
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue