agentic-forge 0.0.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/.gitattributes +24 -0
- package/.github/workflows/ci.yml +70 -0
- package/.markdownlint-cli2.jsonc +16 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/.vscode/agentic-forge.code-workspace +26 -0
- package/CHANGELOG.md +100 -0
- package/CLAUDE.md +158 -0
- package/CONTRIBUTING.md +152 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/agentic-forge-banner.png +0 -0
- package/biome.json +21 -0
- package/package.json +5 -0
- package/scripts/copy-assets.js +21 -0
- package/src/agents/explorer.md +97 -0
- package/src/agents/reviewer.md +137 -0
- package/src/checkpoints/manager.ts +119 -0
- package/src/claude/.claude/skills/analyze/SKILL.md +241 -0
- package/src/claude/.claude/skills/analyze/references/bug.md +62 -0
- package/src/claude/.claude/skills/analyze/references/debt.md +76 -0
- package/src/claude/.claude/skills/analyze/references/doc.md +67 -0
- package/src/claude/.claude/skills/analyze/references/security.md +76 -0
- package/src/claude/.claude/skills/analyze/references/style.md +72 -0
- package/src/claude/.claude/skills/create-checkpoint/SKILL.md +88 -0
- package/src/claude/.claude/skills/create-log/SKILL.md +75 -0
- package/src/claude/.claude/skills/fix-analyze/SKILL.md +102 -0
- package/src/claude/.claude/skills/git-branch/SKILL.md +71 -0
- package/src/claude/.claude/skills/git-commit/SKILL.md +107 -0
- package/src/claude/.claude/skills/git-pr/SKILL.md +96 -0
- package/src/claude/.claude/skills/orchestrate/SKILL.md +120 -0
- package/src/claude/.claude/skills/sdlc-plan/SKILL.md +163 -0
- package/src/claude/.claude/skills/sdlc-plan/references/bug.md +115 -0
- package/src/claude/.claude/skills/sdlc-plan/references/chore.md +105 -0
- package/src/claude/.claude/skills/sdlc-plan/references/feature.md +130 -0
- package/src/claude/.claude/skills/sdlc-review/SKILL.md +215 -0
- package/src/claude/.claude/skills/workflow-builder/SKILL.md +185 -0
- package/src/claude/.claude/skills/workflow-builder/references/REFERENCE.md +487 -0
- package/src/claude/.claude/skills/workflow-builder/references/workflow-example.yaml +427 -0
- package/src/cli.ts +182 -0
- package/src/commands/config-cmd.ts +28 -0
- package/src/commands/index.ts +21 -0
- package/src/commands/init.ts +96 -0
- package/src/commands/release-notes.ts +85 -0
- package/src/commands/resume.ts +103 -0
- package/src/commands/run.ts +234 -0
- package/src/commands/shortcuts.ts +11 -0
- package/src/commands/skills-dir.ts +11 -0
- package/src/commands/status.ts +112 -0
- package/src/commands/update.ts +64 -0
- package/src/commands/version.ts +27 -0
- package/src/commands/workflows.ts +129 -0
- package/src/config.ts +129 -0
- package/src/console.ts +790 -0
- package/src/executor.ts +354 -0
- package/src/git/worktree.ts +236 -0
- package/src/logging/logger.ts +95 -0
- package/src/orchestrator.ts +815 -0
- package/src/parser.ts +225 -0
- package/src/progress.ts +306 -0
- package/src/prompts/agentic-system.md +31 -0
- package/src/ralph-loop.ts +260 -0
- package/src/renderer.ts +164 -0
- package/src/runner.ts +634 -0
- package/src/signal-manager.ts +55 -0
- package/src/steps/base.ts +71 -0
- package/src/steps/conditional-step.ts +144 -0
- package/src/steps/index.ts +15 -0
- package/src/steps/parallel-step.ts +213 -0
- package/src/steps/prompt-step.ts +121 -0
- package/src/steps/ralph-loop-step.ts +186 -0
- package/src/steps/serial-step.ts +84 -0
- package/src/templates/analysis/bug.md.j2 +35 -0
- package/src/templates/analysis/debt.md.j2 +38 -0
- package/src/templates/analysis/doc.md.j2 +45 -0
- package/src/templates/analysis/security.md.j2 +35 -0
- package/src/templates/analysis/style.md.j2 +44 -0
- package/src/templates/analysis-summary.md.j2 +58 -0
- package/src/templates/checkpoint.md.j2 +27 -0
- package/src/templates/implementation-report.md.j2 +81 -0
- package/src/templates/memory.md.j2 +16 -0
- package/src/templates/plan-bug.md.j2 +42 -0
- package/src/templates/plan-chore.md.j2 +27 -0
- package/src/templates/plan-feature.md.j2 +41 -0
- package/src/templates/progress.json.j2 +16 -0
- package/src/templates/ralph-report.md.j2 +45 -0
- package/src/types.ts +141 -0
- package/src/workflows/analyze-codebase-merge.yaml +328 -0
- package/src/workflows/analyze-codebase.yaml +196 -0
- package/src/workflows/analyze-single.yaml +56 -0
- package/src/workflows/demo.yaml +180 -0
- package/src/workflows/one-shot.yaml +54 -0
- package/src/workflows/plan-build-review.yaml +160 -0
- package/src/workflows/ralph-loop.yaml +73 -0
- package/tests/config.test.ts +219 -0
- package/tests/console.test.ts +506 -0
- package/tests/executor.test.ts +339 -0
- package/tests/init.test.ts +86 -0
- package/tests/logger.test.ts +110 -0
- package/tests/parser.test.ts +290 -0
- package/tests/progress.test.ts +345 -0
- package/tests/ralph-loop.test.ts +418 -0
- package/tests/renderer.test.ts +350 -0
- package/tests/runner.test.ts +497 -0
- package/tests/setup.test.ts +7 -0
- package/tests/signal-manager.test.ts +26 -0
- package/tests/steps.test.ts +412 -0
- package/tests/worktree.test.ts +411 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Update command handler. */
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
import { getVersion } from "./version.js";
|
|
6
|
+
|
|
7
|
+
function commandExists(cmd: string): boolean {
|
|
8
|
+
try {
|
|
9
|
+
execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
|
|
10
|
+
stdio: "ignore",
|
|
11
|
+
});
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function cmdUpdate(options: { check?: boolean }): void {
|
|
19
|
+
const currentVersion = getVersion();
|
|
20
|
+
if (currentVersion === "unknown") {
|
|
21
|
+
process.stderr.write("Error: agentic-forge is not installed\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!commandExists("npm")) {
|
|
26
|
+
process.stderr.write("Error: npm is required for updating agentic-forge\n");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (options.check) {
|
|
31
|
+
process.stdout.write(`Current version: ${currentVersion}\n`);
|
|
32
|
+
try {
|
|
33
|
+
const result = execFileSync("npm", ["view", "agentic-forge", "version"], {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
36
|
+
}).trim();
|
|
37
|
+
if (result && result !== currentVersion) {
|
|
38
|
+
process.stdout.write(`Latest version: ${result}\n`);
|
|
39
|
+
} else {
|
|
40
|
+
process.stdout.write("Already at the latest version.\n");
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
process.stdout.write("Could not check for updates.\n");
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write(`Current version: ${currentVersion}\n`);
|
|
47
|
+
process.stdout.write("Upgrading...\n");
|
|
48
|
+
try {
|
|
49
|
+
execFileSync("npm", ["install", "-g", "agentic-forge"], {
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
+
});
|
|
53
|
+
const newVersion = getVersion();
|
|
54
|
+
if (newVersion !== currentVersion) {
|
|
55
|
+
process.stdout.write(`Successfully updated to version ${newVersion}\n`);
|
|
56
|
+
} else {
|
|
57
|
+
process.stdout.write("Already at the latest version.\n");
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
process.stderr.write("Update failed.\n");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Version command handler. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export function cmdVersion(): void {
|
|
10
|
+
try {
|
|
11
|
+
const packageJsonPath = path.join(__dirname, "..", "..", "package.json");
|
|
12
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version: string };
|
|
13
|
+
process.stdout.write(`agentic-forge ${pkg.version}\n`);
|
|
14
|
+
} catch {
|
|
15
|
+
process.stdout.write("agentic-forge version unknown (package not installed)\n");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getVersion(): string {
|
|
20
|
+
try {
|
|
21
|
+
const packageJsonPath = path.join(__dirname, "..", "..", "package.json");
|
|
22
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version: string };
|
|
23
|
+
return pkg.version;
|
|
24
|
+
} catch {
|
|
25
|
+
return "unknown";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Workflows command handler. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
|
|
7
|
+
import { getBundledWorkflowsDir, getProjectWorkflowsDir, getUserWorkflowsDir } from "./run.js";
|
|
8
|
+
|
|
9
|
+
interface WorkflowMetadata {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
variables: { name: string; required?: boolean }[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getWorkflowMetadata(filePath: string): WorkflowMetadata {
|
|
16
|
+
try {
|
|
17
|
+
const content = yaml.load(readFileSync(filePath, "utf-8")) as Record<string, unknown> | null;
|
|
18
|
+
if (!content || typeof content !== "object") {
|
|
19
|
+
return { name: path.basename(filePath, ".yaml"), description: "", variables: [] };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
name: (content.name as string) ?? path.basename(filePath, ".yaml"),
|
|
23
|
+
description: (content.description as string) ?? "",
|
|
24
|
+
variables: (content.variables as WorkflowMetadata["variables"]) ?? [],
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
return { name: path.basename(filePath, ".yaml"), description: "", variables: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listAllWorkflows(): [string, string, string, WorkflowMetadata][] {
|
|
32
|
+
const workflows: [string, string, string, WorkflowMetadata][] = [];
|
|
33
|
+
|
|
34
|
+
const searchLocations: [string, string][] = [
|
|
35
|
+
[getProjectWorkflowsDir(), "project"],
|
|
36
|
+
[getUserWorkflowsDir(), "user"],
|
|
37
|
+
[getBundledWorkflowsDir(), "bundled"],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const [directory, locationType] of searchLocations) {
|
|
41
|
+
if (existsSync(directory)) {
|
|
42
|
+
const files = readdirSync(directory)
|
|
43
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
44
|
+
.sort();
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const fullPath = path.join(directory, file);
|
|
47
|
+
const metadata = getWorkflowMetadata(fullPath);
|
|
48
|
+
workflows.push([path.basename(file, ".yaml"), fullPath, locationType, metadata]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return workflows;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function cmdWorkflows(options: { verbose?: boolean }): void {
|
|
57
|
+
const workflows = listAllWorkflows();
|
|
58
|
+
|
|
59
|
+
if (workflows.length === 0) {
|
|
60
|
+
process.stdout.write("No workflows found.\n");
|
|
61
|
+
process.stdout.write("\nSearched locations:\n");
|
|
62
|
+
process.stdout.write(` - Project: ${getProjectWorkflowsDir()}\n`);
|
|
63
|
+
process.stdout.write(` - User: ${getUserWorkflowsDir()}\n`);
|
|
64
|
+
process.stdout.write(` - Bundled: ${getBundledWorkflowsDir()}\n`);
|
|
65
|
+
process.stdout.write("\nUse 'agentic-forge init' to copy bundled workflows locally.\n");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const verbose = options.verbose ?? false;
|
|
70
|
+
|
|
71
|
+
// Group by location
|
|
72
|
+
const byLocation: Record<string, [string, string, WorkflowMetadata][]> = {};
|
|
73
|
+
for (const [name, wfPath, location, metadata] of workflows) {
|
|
74
|
+
if (!byLocation[location]) {
|
|
75
|
+
byLocation[location] = [];
|
|
76
|
+
}
|
|
77
|
+
byLocation[location].push([name, wfPath, metadata]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.stdout.write("Available Workflows\n");
|
|
81
|
+
process.stdout.write(`${"=".repeat(50)}\n\n`);
|
|
82
|
+
|
|
83
|
+
const locationOrder = ["project", "user", "bundled"];
|
|
84
|
+
const locationLabels: Record<string, string> = {
|
|
85
|
+
project: "Project",
|
|
86
|
+
user: "User",
|
|
87
|
+
bundled: "Bundled",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (const location of locationOrder) {
|
|
91
|
+
if (!byLocation[location]) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
process.stdout.write(`${locationLabels[location]}:\n`);
|
|
96
|
+
for (const [name, , metadata] of byLocation[location]) {
|
|
97
|
+
let desc = metadata.description;
|
|
98
|
+
if (desc) {
|
|
99
|
+
if (desc.length > 60 && !verbose) {
|
|
100
|
+
desc = `${desc.slice(0, 57)}...`;
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write(` ${name.padEnd(25)} ${desc}\n`);
|
|
103
|
+
} else {
|
|
104
|
+
process.stdout.write(` ${name}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (verbose && metadata.variables.length > 0) {
|
|
108
|
+
const required = metadata.variables.filter((v) => v.required).map((v) => v.name);
|
|
109
|
+
const optional = metadata.variables.filter((v) => !v.required).map((v) => v.name);
|
|
110
|
+
if (required.length > 0) {
|
|
111
|
+
process.stdout.write(` Required: ${required.join(", ")}\n`);
|
|
112
|
+
}
|
|
113
|
+
if (optional.length > 0) {
|
|
114
|
+
process.stdout.write(` Optional: ${optional.join(", ")}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
process.stdout.write(`Total: ${workflows.length} workflow(s)\n\n`);
|
|
122
|
+
process.stdout.write("Usage:\n");
|
|
123
|
+
process.stdout.write(" agentic-forge run <workflow-name>\n");
|
|
124
|
+
process.stdout.write(" agentic-forge run <workflow-name> --var key=value\n\n");
|
|
125
|
+
process.stdout.write("Examples:\n");
|
|
126
|
+
process.stdout.write(' agentic-forge run one-shot --var task="Add login button"\n');
|
|
127
|
+
process.stdout.write(" agentic-forge run analyze-single --var analysis_type=bug\n");
|
|
128
|
+
process.stdout.write(' agentic-forge run plan-build-review --var task="Refactor auth"\n');
|
|
129
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Configuration management for agentic-forge. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG: Record<string, unknown> = {
|
|
7
|
+
outputDirectory: "agentic",
|
|
8
|
+
logging: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
level: "Error",
|
|
11
|
+
},
|
|
12
|
+
git: {
|
|
13
|
+
mainBranch: "main",
|
|
14
|
+
autoCommit: true,
|
|
15
|
+
autoPr: true,
|
|
16
|
+
},
|
|
17
|
+
defaults: {
|
|
18
|
+
model: "sonnet",
|
|
19
|
+
maxRetry: 3,
|
|
20
|
+
timeoutMinutes: 60,
|
|
21
|
+
trackProgress: true,
|
|
22
|
+
terminalOutput: "base",
|
|
23
|
+
},
|
|
24
|
+
execution: {
|
|
25
|
+
maxWorkers: 4,
|
|
26
|
+
pollingIntervalSeconds: 5,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function getConfigPath(repoRoot?: string): string {
|
|
31
|
+
const root = repoRoot ?? process.cwd();
|
|
32
|
+
return path.join(root, "agentic", "config.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getDefaultConfig(): Record<string, unknown> {
|
|
36
|
+
return deepCopy(DEFAULT_CONFIG);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadConfig(repoRoot?: string): Record<string, unknown> {
|
|
40
|
+
const configPath = getConfigPath(repoRoot);
|
|
41
|
+
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
const content = readFileSync(configPath, "utf-8");
|
|
44
|
+
const userConfig = JSON.parse(content) as Record<string, unknown>;
|
|
45
|
+
return deepMerge(getDefaultConfig(), userConfig);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return getDefaultConfig();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function saveConfig(config: Record<string, unknown>, repoRoot?: string): void {
|
|
52
|
+
const configPath = getConfigPath(repoRoot);
|
|
53
|
+
const dir = path.dirname(configPath);
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getConfigValue(key: string, repoRoot?: string): unknown {
|
|
59
|
+
const config = loadConfig(repoRoot);
|
|
60
|
+
const parts = key.split(".");
|
|
61
|
+
let value: unknown = config;
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
if (
|
|
64
|
+
value !== null &&
|
|
65
|
+
typeof value === "object" &&
|
|
66
|
+
!Array.isArray(value) &&
|
|
67
|
+
part in (value as Record<string, unknown>)
|
|
68
|
+
) {
|
|
69
|
+
value = (value as Record<string, unknown>)[part];
|
|
70
|
+
} else {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function setConfigValue(key: string, value: string, repoRoot?: string): void {
|
|
78
|
+
const config = loadConfig(repoRoot);
|
|
79
|
+
const parts = key.split(".");
|
|
80
|
+
let target = config;
|
|
81
|
+
for (const part of parts.slice(0, -1)) {
|
|
82
|
+
if (!(part in target)) {
|
|
83
|
+
target[part] = {};
|
|
84
|
+
}
|
|
85
|
+
target = target[part] as Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let parsedValue: unknown = value;
|
|
89
|
+
if (value.toLowerCase() === "true") {
|
|
90
|
+
parsedValue = true;
|
|
91
|
+
} else if (value.toLowerCase() === "false") {
|
|
92
|
+
parsedValue = false;
|
|
93
|
+
} else if (/^\d+$/.test(value)) {
|
|
94
|
+
parsedValue = Number.parseInt(value, 10);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
target[parts[parts.length - 1]] = parsedValue;
|
|
98
|
+
saveConfig(config, repoRoot);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deepMerge(
|
|
102
|
+
base: Record<string, unknown>,
|
|
103
|
+
override: Record<string, unknown>,
|
|
104
|
+
): Record<string, unknown> {
|
|
105
|
+
const result = { ...base };
|
|
106
|
+
for (const key of Object.keys(override)) {
|
|
107
|
+
if (
|
|
108
|
+
key in result &&
|
|
109
|
+
typeof result[key] === "object" &&
|
|
110
|
+
result[key] !== null &&
|
|
111
|
+
!Array.isArray(result[key]) &&
|
|
112
|
+
typeof override[key] === "object" &&
|
|
113
|
+
override[key] !== null &&
|
|
114
|
+
!Array.isArray(override[key])
|
|
115
|
+
) {
|
|
116
|
+
result[key] = deepMerge(
|
|
117
|
+
result[key] as Record<string, unknown>,
|
|
118
|
+
override[key] as Record<string, unknown>,
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
result[key] = override[key];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function deepCopy(obj: Record<string, unknown>): Record<string, unknown> {
|
|
128
|
+
return JSON.parse(JSON.stringify(obj)) as Record<string, unknown>;
|
|
129
|
+
}
|