solidity-argus 0.2.0 → 0.3.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/AGENTS.md +3 -3
- package/README.md +93 -37
- package/package.json +33 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +24 -7
- package/src/agents/pythia-prompt.ts +3 -4
- package/src/agents/scribe-prompt.ts +7 -2
- package/src/agents/sentinel-prompt.ts +32 -3
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +4 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/tool-tracking-hook.ts +104 -50
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -36
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +16 -3
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- package/src/tools/contract-analyzer-tool.ts +60 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +185 -190
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +268 -200
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +216 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +103 -74
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
package/src/managers/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuditState } from "../state/types"
|
|
1
|
+
import type { AuditState } from "../state/types"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* BackgroundManager interface
|
|
@@ -12,32 +12,32 @@ export interface BackgroundManager {
|
|
|
12
12
|
* @param options - Optional configuration (priority, timeout, etc.)
|
|
13
13
|
* @returns taskId - Unique identifier for tracking this task
|
|
14
14
|
*/
|
|
15
|
-
dispatch(agentName: string, prompt: string, options?: { priority?: number }): string
|
|
15
|
+
dispatch(agentName: string, prompt: string, options?: { priority?: number }): string
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Cancel a running background task
|
|
19
19
|
* @param taskId - The task ID to cancel
|
|
20
20
|
*/
|
|
21
|
-
cancel(taskId: string): void
|
|
21
|
+
cancel(taskId: string): void
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Get the result of a completed background task
|
|
25
25
|
* @param taskId - The task ID to retrieve results for
|
|
26
26
|
* @returns Promise resolving to the task result
|
|
27
27
|
*/
|
|
28
|
-
getResult(taskId: string): Promise<unknown
|
|
28
|
+
getResult(taskId: string): Promise<unknown>
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Register a callback to be invoked when a task completes
|
|
32
32
|
* @param callback - Function called with (taskId, result) when task finishes
|
|
33
33
|
*/
|
|
34
|
-
onComplete(callback: (taskId: string, result: unknown) => void): void
|
|
34
|
+
onComplete(callback: (taskId: string, result: unknown) => void): void
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Get the number of currently active/running tasks
|
|
38
38
|
* @returns Number of active tasks
|
|
39
39
|
*/
|
|
40
|
-
getActiveCount(): number
|
|
40
|
+
getActiveCount(): number
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -49,30 +49,35 @@ export interface AuditStateManager {
|
|
|
49
49
|
* Load audit state from persistent storage
|
|
50
50
|
* @returns Promise resolving to AuditState or null if not found
|
|
51
51
|
*/
|
|
52
|
-
load(): Promise<AuditState | null
|
|
52
|
+
load(): Promise<AuditState | null>
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Save audit state to persistent storage
|
|
56
56
|
* @param state - The AuditState to persist
|
|
57
57
|
*/
|
|
58
|
-
save(state: AuditState): Promise<void
|
|
58
|
+
save(state: AuditState): Promise<void>
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Get the current in-memory audit state
|
|
62
62
|
* @returns The current AuditState or null if not loaded
|
|
63
63
|
*/
|
|
64
|
-
get(): AuditState | null
|
|
64
|
+
get(): AuditState | null
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Update the audit state with a partial patch
|
|
68
68
|
* @param patch - Partial AuditState object with fields to update
|
|
69
69
|
*/
|
|
70
|
-
update(patch: Partial<AuditState>): Promise<void
|
|
70
|
+
update(patch: Partial<AuditState>): Promise<void>
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Reset the audit state (clear all data)
|
|
74
74
|
*/
|
|
75
|
-
reset(): Promise<void
|
|
75
|
+
reset(): Promise<void>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Archive current state (if meaningful) then reset
|
|
79
|
+
*/
|
|
80
|
+
archive(): Promise<void>
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
@@ -80,6 +85,6 @@ export interface AuditStateManager {
|
|
|
80
85
|
* Container for all manager instances
|
|
81
86
|
*/
|
|
82
87
|
export type Managers = {
|
|
83
|
-
backgroundManager: BackgroundManager
|
|
84
|
-
auditStateManager: AuditStateManager
|
|
85
|
-
}
|
|
88
|
+
backgroundManager: BackgroundManager
|
|
89
|
+
auditStateManager: AuditStateManager
|
|
90
|
+
}
|
package/src/plugin-interface.ts
CHANGED
|
@@ -11,12 +11,12 @@ export function createPluginInterface(args: {
|
|
|
11
11
|
}): PluginReturn {
|
|
12
12
|
const { tools, hooks } = args
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const result: PluginReturn = {
|
|
15
|
+
tool: tools,
|
|
16
|
+
config: hooks.config,
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
if (hooks["chat.params"]) {
|
|
20
20
|
result["chat.params"] = hooks["chat.params"]
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -25,13 +25,11 @@ export function createPluginInterface(args: {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (hooks["experimental.chat.system.transform"]) {
|
|
28
|
-
result["experimental.chat.system.transform"] =
|
|
29
|
-
hooks["experimental.chat.system.transform"]
|
|
28
|
+
result["experimental.chat.system.transform"] = hooks["experimental.chat.system.transform"]
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
if (hooks["experimental.session.compacting"]) {
|
|
33
|
-
result["experimental.session.compacting"] =
|
|
34
|
-
hooks["experimental.session.compacting"]
|
|
32
|
+
result["experimental.session.compacting"] = hooks["experimental.session.compacting"]
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
if (hooks["tool.execute.after"]) {
|
|
@@ -1,64 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { createLogger } from "./logger"
|
|
4
|
+
|
|
5
|
+
const logger = createLogger()
|
|
4
6
|
|
|
5
7
|
export function hasBinary(name: string): boolean {
|
|
6
8
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
const result = Bun.spawnSync(["which", name], {
|
|
10
|
+
stdout: "ignore",
|
|
11
|
+
stderr: "ignore",
|
|
12
|
+
timeout: 5_000,
|
|
13
|
+
})
|
|
14
|
+
return result.exitCode === 0
|
|
9
15
|
} catch (_e) {
|
|
10
|
-
return false
|
|
16
|
+
return false
|
|
11
17
|
}
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
export function parseSolcVersion(target: string): string | undefined {
|
|
15
|
-
const foundryToml = join(target, "foundry.toml")
|
|
16
|
-
if (
|
|
17
|
-
const content =
|
|
18
|
-
const match = content.match(/solc\s*=\s*["']([^"']+)["']/)
|
|
19
|
-
if (match?.[1]) return match[1]
|
|
20
|
+
export async function parseSolcVersion(target: string): Promise<string | undefined> {
|
|
21
|
+
const foundryToml = join(target, "foundry.toml")
|
|
22
|
+
if (await Bun.file(foundryToml).exists()) {
|
|
23
|
+
const content = await Bun.file(foundryToml).text()
|
|
24
|
+
const match = content.match(/solc\s*=\s*["']([^"']+)["']/)
|
|
25
|
+
if (match?.[1]) return match[1]
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
const solFiles = [
|
|
23
|
-
if (
|
|
24
|
-
solFiles.push(target)
|
|
28
|
+
const solFiles: string[] = []
|
|
29
|
+
if (target.endsWith(".sol") && (await Bun.file(target).exists())) {
|
|
30
|
+
solFiles.push(target)
|
|
25
31
|
} else {
|
|
26
|
-
const srcDir = join(target, "src")
|
|
32
|
+
const srcDir = join(target, "src")
|
|
27
33
|
if (existsSync(srcDir)) {
|
|
28
34
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const proc = Bun.spawn(["find", srcDir, "-maxdepth", "3", "-name", "*.sol"], {
|
|
36
|
+
stdout: "pipe",
|
|
37
|
+
stderr: "pipe",
|
|
38
|
+
signal: AbortSignal.timeout(10_000),
|
|
33
39
|
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
40
|
+
const exitCode = await proc.exited
|
|
41
|
+
if (exitCode === 0) {
|
|
42
|
+
const output = await new Response(proc.stdout).text()
|
|
43
|
+
solFiles.push(...output.trim().split("\n").filter(Boolean))
|
|
44
|
+
}
|
|
38
45
|
} catch (_findErr) {
|
|
46
|
+
logger.debug("find command failed for .sol files")
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
for (const file of solFiles) {
|
|
44
|
-
if (!
|
|
52
|
+
if (!file.endsWith(".sol") || !(await Bun.file(file).exists())) continue
|
|
45
53
|
try {
|
|
46
|
-
const content =
|
|
47
|
-
const pragma = content.match(/pragma\s+solidity\s+[\^~>=<]*\s*([\d.]+)/)
|
|
48
|
-
if (pragma?.[1]) return pragma[1]
|
|
54
|
+
const content = await Bun.file(file).text()
|
|
55
|
+
const pragma = content.match(/pragma\s+solidity\s+[\^~>=<]*\s*([\d.]+)/)
|
|
56
|
+
if (pragma?.[1]) return pragma[1]
|
|
49
57
|
} catch (_readErr) {
|
|
58
|
+
logger.debug("Failed to read .sol file for pragma detection")
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
|
-
return undefined
|
|
61
|
+
return undefined
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
export function extractContractNames(filePath: string): string[] {
|
|
56
|
-
if (!
|
|
64
|
+
export async function extractContractNames(filePath: string): Promise<string[]> {
|
|
65
|
+
if (!(await Bun.file(filePath).exists())) return []
|
|
57
66
|
try {
|
|
58
|
-
const content =
|
|
59
|
-
const matches = content.matchAll(/\b(?:contract|library|interface)\s+(\w+)/g)
|
|
60
|
-
return Array.from(matches, (m) => m[1]).filter(Boolean) as string[]
|
|
67
|
+
const content = await Bun.file(filePath).text()
|
|
68
|
+
const matches = content.matchAll(/\b(?:contract|library|interface)\s+(\w+)/g)
|
|
69
|
+
return Array.from(matches, (m) => m[1]).filter(Boolean) as string[]
|
|
61
70
|
} catch (_e) {
|
|
62
|
-
return []
|
|
71
|
+
return []
|
|
63
72
|
}
|
|
64
73
|
}
|
package/src/shared/deep-merge.ts
CHANGED
|
@@ -1,49 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const deduplicateObjectIds = new WeakMap<object, number>()
|
|
2
|
+
let nextDeduplicateObjectId = 1
|
|
3
|
+
|
|
4
|
+
function getDeduplicateObjectKey(obj: object): string {
|
|
5
|
+
let id = deduplicateObjectIds.get(obj)
|
|
6
|
+
if (id === undefined) {
|
|
7
|
+
id = nextDeduplicateObjectId++
|
|
8
|
+
deduplicateObjectIds.set(obj, id)
|
|
9
|
+
}
|
|
10
|
+
return `object:${id}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function deduplicateArray(arr: unknown[]): unknown[] {
|
|
14
|
+
const seen = new Set<string>()
|
|
15
|
+
const result: unknown[] = []
|
|
16
|
+
|
|
17
|
+
for (const item of arr) {
|
|
18
|
+
let key: string
|
|
19
|
+
if (typeof item === "object" && item !== null) {
|
|
20
|
+
try {
|
|
21
|
+
key = `object:${JSON.stringify(item)}`
|
|
22
|
+
} catch {
|
|
23
|
+
key = getDeduplicateObjectKey(item)
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
key = `${typeof item}:${String(item)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!seen.has(key)) {
|
|
30
|
+
seen.add(key)
|
|
31
|
+
result.push(item)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function deepMerge(target: unknown, source: unknown): unknown {
|
|
3
39
|
if (source === undefined) {
|
|
4
|
-
return target
|
|
40
|
+
return target
|
|
5
41
|
}
|
|
6
42
|
|
|
7
|
-
// If either is not an object, return source (override)
|
|
8
43
|
if (
|
|
9
44
|
typeof target !== "object" ||
|
|
10
45
|
target === null ||
|
|
11
46
|
typeof source !== "object" ||
|
|
12
47
|
source === null
|
|
13
48
|
) {
|
|
14
|
-
return source
|
|
49
|
+
return source
|
|
15
50
|
}
|
|
16
51
|
|
|
17
|
-
// If both are arrays, concatenate and deduplicate
|
|
18
52
|
if (Array.isArray(target) && Array.isArray(source)) {
|
|
19
|
-
|
|
20
|
-
// Deduplicate by filtering unique values
|
|
21
|
-
return Array.from(new Set(merged));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// If target is array but source is not, return source
|
|
25
|
-
if (Array.isArray(target) && !Array.isArray(source)) {
|
|
26
|
-
return source;
|
|
53
|
+
return deduplicateArray([...target, ...source])
|
|
27
54
|
}
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return source;
|
|
56
|
+
if (Array.isArray(target) || Array.isArray(source)) {
|
|
57
|
+
return source
|
|
32
58
|
}
|
|
33
59
|
|
|
34
|
-
|
|
35
|
-
const
|
|
60
|
+
const tgt = target as Record<string, unknown>
|
|
61
|
+
const src = source as Record<string, unknown>
|
|
62
|
+
const result: Record<string, unknown> = { ...tgt }
|
|
36
63
|
|
|
37
|
-
for (const key in
|
|
38
|
-
if (Object.
|
|
39
|
-
const sourceValue =
|
|
64
|
+
for (const key in src) {
|
|
65
|
+
if (Object.hasOwn(src, key)) {
|
|
66
|
+
const sourceValue = src[key]
|
|
40
67
|
|
|
41
|
-
// Skip undefined values from source
|
|
42
68
|
if (sourceValue === undefined) {
|
|
43
|
-
continue
|
|
69
|
+
continue
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
// If both are objects (and not arrays), recurse
|
|
47
72
|
if (
|
|
48
73
|
typeof result[key] === "object" &&
|
|
49
74
|
result[key] !== null &&
|
|
@@ -52,20 +77,14 @@ export function deepMerge(target: any, source: any): any {
|
|
|
52
77
|
sourceValue !== null &&
|
|
53
78
|
!Array.isArray(sourceValue)
|
|
54
79
|
) {
|
|
55
|
-
result[key] = deepMerge(result[key], sourceValue)
|
|
56
|
-
} else if (
|
|
57
|
-
|
|
58
|
-
Array.isArray(sourceValue)
|
|
59
|
-
) {
|
|
60
|
-
// Both are arrays, concatenate and deduplicate
|
|
61
|
-
const merged = [...result[key], ...sourceValue];
|
|
62
|
-
result[key] = Array.from(new Set(merged));
|
|
80
|
+
result[key] = deepMerge(result[key], sourceValue)
|
|
81
|
+
} else if (Array.isArray(result[key]) && Array.isArray(sourceValue)) {
|
|
82
|
+
result[key] = deduplicateArray([...(result[key] as unknown[]), ...sourceValue])
|
|
63
83
|
} else {
|
|
64
|
-
|
|
65
|
-
result[key] = sourceValue;
|
|
84
|
+
result[key] = sourceValue
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
return result
|
|
89
|
+
return result
|
|
71
90
|
}
|
package/src/shared/file-utils.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { stripJsoncComments } from "./jsonc-parser"
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { stripJsoncComments } from "./jsonc-parser"
|
|
4
4
|
|
|
5
|
-
export type ConfigFormat = "json" | "jsonc" | "none"
|
|
5
|
+
export type ConfigFormat = "json" | "jsonc" | "none"
|
|
6
6
|
|
|
7
7
|
export interface ConfigFileInfo {
|
|
8
|
-
path: string | null
|
|
9
|
-
format: ConfigFormat
|
|
8
|
+
path: string | null
|
|
9
|
+
format: ConfigFormat
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function detectConfigFile(basePath: string): ConfigFileInfo {
|
|
@@ -15,42 +15,44 @@ export function detectConfigFile(basePath: string): ConfigFileInfo {
|
|
|
15
15
|
{ path: join(basePath, ".opencode", "solidity-argus.json"), format: "json" as const },
|
|
16
16
|
{ path: join(basePath, "solidity-argus.jsonc"), format: "jsonc" as const },
|
|
17
17
|
{ path: join(basePath, "solidity-argus.json"), format: "json" as const },
|
|
18
|
-
|
|
19
|
-
{ path: join(basePath, "config.json"), format: "json" as const },
|
|
20
|
-
];
|
|
18
|
+
]
|
|
21
19
|
|
|
22
20
|
for (const candidate of candidates) {
|
|
23
21
|
if (existsSync(candidate.path)) {
|
|
24
22
|
return {
|
|
25
23
|
path: candidate.path,
|
|
26
24
|
format: candidate.format,
|
|
27
|
-
}
|
|
25
|
+
}
|
|
28
26
|
}
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
return {
|
|
32
30
|
path: null,
|
|
33
31
|
format: "none",
|
|
34
|
-
}
|
|
32
|
+
}
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
export function readJsoncFile(filePath: string): Record<string,
|
|
35
|
+
export function readJsoncFile(filePath: string): Record<string, unknown> | null {
|
|
38
36
|
try {
|
|
39
37
|
if (!existsSync(filePath)) {
|
|
40
|
-
return null
|
|
38
|
+
return null
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
const content = readFileSync(filePath, "utf-8")
|
|
41
|
+
const content = readFileSync(filePath, "utf-8")
|
|
44
42
|
|
|
45
43
|
if (!content.trim()) {
|
|
46
|
-
return null
|
|
44
|
+
return null
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
const stripped = stripJsoncComments(content)
|
|
50
|
-
const parsed = JSON.parse(stripped)
|
|
47
|
+
const stripped = stripJsoncComments(content)
|
|
48
|
+
const parsed: unknown = JSON.parse(stripped)
|
|
49
|
+
|
|
50
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
51
53
|
|
|
52
|
-
return parsed
|
|
54
|
+
return parsed as Record<string, unknown>
|
|
53
55
|
} catch (_error) {
|
|
54
|
-
return null
|
|
56
|
+
return null
|
|
55
57
|
}
|
|
56
58
|
}
|
package/src/shared/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { deepMerge } from "./deep-merge"
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export { extractContractNames, hasBinary, parseSolcVersion } from "./binary-utils"
|
|
2
|
+
export { deepMerge } from "./deep-merge"
|
|
3
|
+
export {
|
|
4
|
+
type ConfigFileInfo,
|
|
5
|
+
type ConfigFormat,
|
|
6
|
+
detectConfigFile,
|
|
7
|
+
readJsoncFile,
|
|
8
|
+
} from "./file-utils"
|
|
9
|
+
export { stripJsoncComments } from "./jsonc-parser"
|
|
10
|
+
export { createLogger, type Logger, type LoggerConfig } from "./logger"
|
|
11
|
+
export { findFoundryProjectDir, resolveProjectDir } from "./project-utils"
|
|
@@ -1,39 +1,134 @@
|
|
|
1
1
|
export function stripJsoncComments(jsonc: string): string {
|
|
2
|
-
let
|
|
2
|
+
let inString = false
|
|
3
|
+
let escaped = false
|
|
4
|
+
let inLineComment = false
|
|
5
|
+
let blockCommentDepth = 0
|
|
6
|
+
const chars: string[] = []
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
for (let i = 0; i < jsonc.length; i++) {
|
|
9
|
+
const ch = jsonc.charAt(i)
|
|
10
|
+
const next = jsonc.charAt(i + 1)
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
if (inLineComment) {
|
|
13
|
+
if (ch === "\n" || ch === "\r") {
|
|
14
|
+
inLineComment = false
|
|
15
|
+
chars.push(ch)
|
|
16
|
+
}
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if (blockCommentDepth > 0) {
|
|
21
|
+
if (ch === "/" && next === "*") {
|
|
22
|
+
blockCommentDepth++
|
|
23
|
+
i++
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (ch === "*" && next === "/") {
|
|
28
|
+
blockCommentDepth--
|
|
29
|
+
i++
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ch === "\n" || ch === "\r") {
|
|
34
|
+
chars.push(ch)
|
|
35
|
+
}
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (escaped) {
|
|
40
|
+
escaped = false
|
|
41
|
+
chars.push(ch)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (inString) {
|
|
46
|
+
if (ch === "\\") {
|
|
47
|
+
escaped = true
|
|
48
|
+
} else if (ch === '"') {
|
|
49
|
+
inString = false
|
|
50
|
+
}
|
|
51
|
+
chars.push(ch)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (ch === '"') {
|
|
56
|
+
inString = true
|
|
57
|
+
chars.push(ch)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ch === "/" && next === "/") {
|
|
62
|
+
inLineComment = true
|
|
63
|
+
i++
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ch === "/" && next === "*") {
|
|
68
|
+
blockCommentDepth = 1
|
|
69
|
+
i++
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
chars.push(ch)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = chars.join("")
|
|
77
|
+
const out: string[] = []
|
|
78
|
+
let inString2 = false
|
|
79
|
+
let escaped2 = false
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < result.length; i++) {
|
|
82
|
+
const ch = result.charAt(i)
|
|
83
|
+
|
|
84
|
+
if (escaped2) {
|
|
85
|
+
escaped2 = false
|
|
86
|
+
out.push(ch)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (inString2) {
|
|
91
|
+
if (ch === "\\") {
|
|
92
|
+
escaped2 = true
|
|
93
|
+
} else if (ch === '"') {
|
|
94
|
+
inString2 = false
|
|
95
|
+
}
|
|
96
|
+
out.push(ch)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ch === '"') {
|
|
101
|
+
inString2 = true
|
|
102
|
+
out.push(ch)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ch === ",") {
|
|
107
|
+
let j = i + 1
|
|
108
|
+
while (j < result.length) {
|
|
109
|
+
const lookahead = result.charAt(j)
|
|
110
|
+
if (lookahead === " " || lookahead === "\t" || lookahead === "\n" || lookahead === "\r") {
|
|
111
|
+
j++
|
|
112
|
+
continue
|
|
24
113
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
break
|
|
114
|
+
|
|
115
|
+
if (lookahead === "}" || lookahead === "]") {
|
|
116
|
+
break
|
|
28
117
|
}
|
|
118
|
+
|
|
119
|
+
out.push(ch)
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (j >= result.length) {
|
|
124
|
+
out.push(ch)
|
|
29
125
|
}
|
|
30
126
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
34
|
-
.join("\n");
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
35
129
|
|
|
36
|
-
|
|
130
|
+
out.push(ch)
|
|
131
|
+
}
|
|
37
132
|
|
|
38
|
-
return
|
|
133
|
+
return out.join("")
|
|
39
134
|
}
|
package/src/shared/logger.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { appendFileSync,
|
|
2
|
-
import { join } from "node:path"
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync } from "node:fs"
|
|
3
2
|
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
4
|
|
|
5
5
|
export interface LoggerConfig {
|
|
6
6
|
debug?: boolean
|
|
@@ -24,9 +24,22 @@ function ensureLogDir(): void {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function safeStringify(a: unknown): string {
|
|
28
|
+
if (typeof a === "string") return a
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(a)
|
|
31
|
+
} catch {
|
|
32
|
+
try {
|
|
33
|
+
return String(a)
|
|
34
|
+
} catch {
|
|
35
|
+
return "[Unserializable value]"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
function formatLine(level: string, args: unknown[]): string {
|
|
28
41
|
const ts = new Date().toISOString()
|
|
29
|
-
const msg = args.map(
|
|
42
|
+
const msg = args.map(safeStringify).join(" ")
|
|
30
43
|
return `${ts} [${level}] ${msg}\n`
|
|
31
44
|
}
|
|
32
45
|
|