Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ sha2 = { workspace = true }
napi = { workspace = true, optional = true }
napi-derive = { workspace = true, optional = true }
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "json", "rustls"] }
globset = "0.4.16"
walkdir = "2.5.0"

[dev-dependencies]
proptest = "1.10.0"
Expand Down
2 changes: 2 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
"lint": "eslint --cache .",
"prepublishOnly": "run-s build",
"test": "run-s build:deps test:run",
"test:native-cleanup-smoke": "tsx scripts/cleanup-native-smoke.ts",
"test:run": "vitest run",
"benchmark:cleanup": "tsx scripts/benchmark-cleanup.ts",
"lintfix": "eslint --fix --cache .",
"typecheck": "tsc --noEmit -p tsconfig.lib.json"
},
Expand Down
154 changes: 154 additions & 0 deletions cli/scripts/benchmark-cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type {ILogger, OutputCleanContext, OutputCleanupDeclarations, OutputPlugin} from '../src/plugins/plugin-core'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import {performance} from 'node:perf_hooks'
import glob from 'fast-glob'

process.env['TNMSC_FORCE_NATIVE_BINDING'] = '1'
delete process.env['VITEST']
delete process.env['VITEST_WORKER_ID']

const cleanupModule = await import('../src/commands/CleanupUtils')
const fallbackModule = await import('../src/commands/CleanupUtils.fallback')
const pluginCore = await import('../src/plugins/plugin-core')

function createMockLogger(): ILogger {
return {
trace: () => {},
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
fatal: () => {}
} as ILogger
}

function createCleanContext(workspaceDir: string): OutputCleanContext {
return {
logger: createMockLogger(),
fs,
path,
glob,
collectedOutputContext: {
workspace: {
directory: {
pathKind: pluginCore.FilePathKind.Absolute,
path: workspaceDir,
getDirectoryName: () => path.basename(workspaceDir),
getAbsolutePath: () => workspaceDir
},
projects: Array.from({length: 40}, (_, index) => ({
dirFromWorkspacePath: {
pathKind: pluginCore.FilePathKind.Relative,
path: `project-${index}`,
basePath: workspaceDir,
getDirectoryName: () => `project-${index}`,
getAbsolutePath: () => path.join(workspaceDir, `project-${index}`)
}
}))
},
aindexDir: path.join(workspaceDir, 'aindex')
}
} as OutputCleanContext
}

function createBenchmarkPlugin(workspaceDir: string): OutputPlugin {
return {
type: pluginCore.PluginKind.Output,
name: 'BenchmarkOutputPlugin',
log: createMockLogger(),
declarativeOutput: true,
outputCapabilities: {},
async declareOutputFiles() {
return Array.from({length: 40}, (_, projectIndex) => ([
{path: path.join(workspaceDir, `project-${projectIndex}`, 'AGENTS.md'), source: {}},
{path: path.join(workspaceDir, `project-${projectIndex}`, 'commands', 'AGENTS.md'), source: {}}
])).flat()
},
async declareCleanupPaths(): Promise<OutputCleanupDeclarations> {
return {
delete: [{
kind: 'glob',
path: path.join(workspaceDir, '.codex', 'skills', '*'),
excludeBasenames: ['.system']
}, {
kind: 'glob',
path: path.join(workspaceDir, '.claude', '**', 'CLAUDE.md')
}],
protect: [{
kind: 'directory',
path: path.join(workspaceDir, '.codex', 'skills', '.system'),
protectionMode: 'recursive'
}]
}
},
async convertContent() {
return 'benchmark'
}
}
}

async function measure(label: string, iterations: number, run: () => Promise<void>): Promise<number> {
const start = performance.now()
for (let index = 0; index < iterations; index += 1) {
await run()
}
const total = performance.now() - start
const average = total / iterations
process.stdout.write(`${label}: total=${total.toFixed(2)}ms avg=${average.toFixed(2)}ms\n`)
return average
}

async function main(): Promise<void> {
if (!cleanupModule.hasNativeCleanupBinding()) {
throw new Error('Native cleanup binding is unavailable. Build the CLI NAPI module first.')
}

const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tnmsc-benchmark-cleanup-'))
const workspaceDir = path.join(tempDir, 'workspace')

try {
for (let projectIndex = 0; projectIndex < 40; projectIndex += 1) {
const rootFile = path.join(workspaceDir, `project-${projectIndex}`, 'AGENTS.md')
const childFile = path.join(workspaceDir, `project-${projectIndex}`, 'commands', 'AGENTS.md')
fs.mkdirSync(path.dirname(childFile), {recursive: true})
fs.writeFileSync(rootFile, '# root', 'utf8')
fs.writeFileSync(childFile, '# child', 'utf8')
}

const skillsDir = path.join(workspaceDir, '.codex', 'skills')
fs.mkdirSync(path.join(skillsDir, '.system'), {recursive: true})
for (let index = 0; index < 80; index += 1) {
const skillDir = path.join(skillsDir, `legacy-${index}`)
fs.mkdirSync(skillDir, {recursive: true})
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# stale', 'utf8')
}

for (let index = 0; index < 40; index += 1) {
const claudeFile = path.join(workspaceDir, '.claude', `project-${index}`, 'CLAUDE.md')
fs.mkdirSync(path.dirname(claudeFile), {recursive: true})
fs.writeFileSync(claudeFile, '# claude', 'utf8')
}

const plugin = createBenchmarkPlugin(workspaceDir)
const cleanCtx = createCleanContext(workspaceDir)
const iterations = 25

process.stdout.write(`cleanup benchmark iterations=${iterations}\n`)
const fallbackAvg = await measure('fallback-plan', iterations, async () => {
await fallbackModule.collectDeletionTargets([plugin], cleanCtx)
})
const nativeAvg = await measure('native-plan', iterations, async () => {
await cleanupModule.collectDeletionTargets([plugin], cleanCtx)
})

const delta = nativeAvg - fallbackAvg
process.stdout.write(`delta=${delta.toFixed(2)}ms (${((delta / fallbackAvg) * 100).toFixed(2)}%)\n`)
}
finally {
fs.rmSync(tempDir, {recursive: true, force: true})
}
}

await main()
141 changes: 141 additions & 0 deletions cli/scripts/cleanup-native-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type {ILogger, OutputCleanContext, OutputCleanupDeclarations, OutputPlugin} from '../src/plugins/plugin-core'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import glob from 'fast-glob'

process.env['TNMSC_FORCE_NATIVE_BINDING'] = '1'
delete process.env['VITEST']
delete process.env['VITEST_WORKER_ID']

const cleanupModule = await import('../src/commands/CleanupUtils')
const fallbackModule = await import('../src/commands/CleanupUtils.fallback')
const pluginCore = await import('../src/plugins/plugin-core')

function createMockLogger(): ILogger {
return {
trace: () => {},
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
fatal: () => {}
} as ILogger
}

function createCleanContext(workspaceDir: string): OutputCleanContext {
return {
logger: createMockLogger(),
fs,
path,
glob,
collectedOutputContext: {
workspace: {
directory: {
pathKind: pluginCore.FilePathKind.Absolute,
path: workspaceDir,
getDirectoryName: () => path.basename(workspaceDir),
getAbsolutePath: () => workspaceDir
},
projects: [{
dirFromWorkspacePath: {
pathKind: pluginCore.FilePathKind.Relative,
path: 'project-a',
basePath: workspaceDir,
getDirectoryName: () => 'project-a',
getAbsolutePath: () => path.join(workspaceDir, 'project-a')
}
}]
},
aindexDir: path.join(workspaceDir, 'aindex')
}
} as OutputCleanContext
}

function createSmokePlugin(workspaceDir: string): OutputPlugin {
return {
type: pluginCore.PluginKind.Output,
name: 'SmokeOutputPlugin',
log: createMockLogger(),
declarativeOutput: true,
outputCapabilities: {},
async declareOutputFiles() {
return [
{path: path.join(workspaceDir, 'project-a', 'AGENTS.md'), source: {}},
{path: path.join(workspaceDir, 'project-a', 'commands', 'AGENTS.md'), source: {}}
]
},
async declareCleanupPaths(): Promise<OutputCleanupDeclarations> {
return {
delete: [{
kind: 'glob',
path: path.join(workspaceDir, '.codex', 'skills', '*'),
excludeBasenames: ['.system']
}]
}
},
async convertContent() {
return 'smoke'
}
}
}

async function main(): Promise<void> {
if (!cleanupModule.hasNativeCleanupBinding()) {
throw new Error('Native cleanup binding is unavailable. Build the CLI NAPI module first.')
}

const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tnmsc-native-cleanup-smoke-'))
const workspaceDir = path.join(tempDir, 'workspace')
const legacySkillDir = path.join(workspaceDir, '.codex', 'skills', 'legacy')
const preservedSkillDir = path.join(workspaceDir, '.codex', 'skills', '.system')
const rootOutput = path.join(workspaceDir, 'project-a', 'AGENTS.md')
const childOutput = path.join(workspaceDir, 'project-a', 'commands', 'AGENTS.md')

fs.mkdirSync(path.dirname(rootOutput), {recursive: true})
fs.mkdirSync(path.dirname(childOutput), {recursive: true})
fs.mkdirSync(legacySkillDir, {recursive: true})
fs.mkdirSync(preservedSkillDir, {recursive: true})
fs.writeFileSync(rootOutput, '# root', 'utf8')
fs.writeFileSync(childOutput, '# child', 'utf8')
fs.writeFileSync(path.join(legacySkillDir, 'SKILL.md'), '# stale', 'utf8')
fs.writeFileSync(path.join(preservedSkillDir, 'SKILL.md'), '# keep', 'utf8')

try {
const plugin = createSmokePlugin(workspaceDir)
const cleanCtx = createCleanContext(workspaceDir)

const nativePlan = await cleanupModule.collectDeletionTargets([plugin], cleanCtx)
const fallbackPlan = await fallbackModule.collectDeletionTargets([plugin], cleanCtx)

const sortPaths = (value: {filesToDelete: string[], dirsToDelete: string[], excludedScanGlobs: string[]}) => ({
...value,
filesToDelete: [...value.filesToDelete].sort(),
dirsToDelete: [...value.dirsToDelete].sort(),
excludedScanGlobs: [...value.excludedScanGlobs].sort()
})

if (JSON.stringify(sortPaths(nativePlan)) !== JSON.stringify(sortPaths(fallbackPlan))) {
throw new Error(`Native cleanup plan mismatch.\nNative: ${JSON.stringify(nativePlan, null, 2)}\nFallback: ${JSON.stringify(fallbackPlan, null, 2)}`)
}

const result = await cleanupModule.performCleanup([plugin], cleanCtx, createMockLogger())
if (result.deletedFiles !== 2 || result.deletedDirs !== 1 || result.errors.length > 0) {
throw new Error(`Unexpected native cleanup result: ${JSON.stringify(result, null, 2)}`)
}

if (fs.existsSync(rootOutput) || fs.existsSync(childOutput) || fs.existsSync(legacySkillDir)) {
throw new Error('Native cleanup did not remove the expected outputs')
}
if (!fs.existsSync(preservedSkillDir)) {
throw new Error('Native cleanup removed the preserved .system skill directory')
}

process.stdout.write('cleanup-native-smoke: ok\n')
}
finally {
fs.rmSync(tempDir, {recursive: true, force: true})
}
}

await main()
Loading
Loading