From cc0dbd8894c2cef8ccfe5fada26af14e2a9e98bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 08:59:44 +0800 Subject: [PATCH 1/7] Upgrade actions/cache to v5 in setup-node-pnpm, setup-rust, and setup-tauri workflows --- .github/actions/setup-node-pnpm/action.yml | 23 ++++++++-------------- .github/actions/setup-rust/action.yml | 2 +- .github/actions/setup-tauri/action.yml | 4 ++-- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index b22a603a..456047ac 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -14,26 +14,19 @@ inputs: runs: using: composite steps: - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - - name: Get pnpm store directory - id: pnpm-cache + - name: Setup pnpm shell: bash - run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Cache pnpm store - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + run: | + corepack enable + corepack prepare "$(node -p "require('./package.json').packageManager")" --activate + pnpm --version - name: Install workspace dependencies if: inputs.install == 'true' diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 5c08148f..048bf750 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -24,7 +24,7 @@ runs: targets: ${{ inputs.targets }} - name: Cache cargo - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cargo/registry diff --git a/.github/actions/setup-tauri/action.yml b/.github/actions/setup-tauri/action.yml index 8255d2ec..40c1f27b 100644 --- a/.github/actions/setup-tauri/action.yml +++ b/.github/actions/setup-tauri/action.yml @@ -23,7 +23,7 @@ runs: steps: - name: Cache Linux package archives if: runner.os == 'Linux' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: /var/cache/apt/archives key: ${{ runner.os }}-tauri-apt-${{ hashFiles('.github/actions/setup-tauri/action.yml') }} @@ -80,7 +80,7 @@ runs: echo "hash=$HASH" >> $GITHUB_OUTPUT - name: Cache cargo + tauri target - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cargo/registry From c570868a16b51206d01f3651b3200f3568d67343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 09:29:48 +0800 Subject: [PATCH 2/7] Bump package versions to 2026.10330.108 in Cargo.lock and update ESLint config to include test files - Updated version of memory-sync-gui, tnmsc, tnmsc-logger, tnmsc-md-compiler, and tnmsc-script-runtime in Cargo.lock to 2026.10330.108. - Modified ESLint configuration to allow default project for test files. - Refactored cleanup.ts for improved readability and consistency in code style. - Updated tsconfig files to include test files and ensure proper module resolution. --- Cargo.lock | 10 +- cli/eslint.config.ts | 2 +- cli/test/native-binding/cleanup.ts | 424 ++++++++++++++--------------- cli/test/setup-native-binding.ts | 178 ++++++------ cli/tsconfig.eslint.json | 7 +- cli/tsconfig.json | 26 +- 6 files changed, 314 insertions(+), 333 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71f19719..5d843bf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2094,7 +2094,7 @@ dependencies = [ [[package]] name = "memory-sync-gui" -version = "2026.10329.110" +version = "2026.10330.108" dependencies = [ "dirs", "proptest", @@ -4372,7 +4372,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tnmsc" -version = "2026.10329.110" +version = "2026.10330.108" dependencies = [ "clap", "dirs", @@ -4394,7 +4394,7 @@ dependencies = [ [[package]] name = "tnmsc-logger" -version = "2026.10329.110" +version = "2026.10330.108" dependencies = [ "chrono", "napi", @@ -4406,7 +4406,7 @@ dependencies = [ [[package]] name = "tnmsc-md-compiler" -version = "2026.10329.110" +version = "2026.10330.108" dependencies = [ "markdown", "napi", @@ -4421,7 +4421,7 @@ dependencies = [ [[package]] name = "tnmsc-script-runtime" -version = "2026.10329.110" +version = "2026.10330.108" dependencies = [ "napi", "napi-build", diff --git a/cli/eslint.config.ts b/cli/eslint.config.ts index bfe7f6c3..9c891393 100644 --- a/cli/eslint.config.ts +++ b/cli/eslint.config.ts @@ -11,7 +11,7 @@ const config = await eslint10({ strictTypescriptEslint: true, tsconfigPath: resolve(configDir, 'tsconfig.eslint.json'), parserOptions: { - allowDefaultProject: ['*.config.ts'] + allowDefaultProject: ['*.config.ts', 'test/**/*.ts'] } }, ignores: [ diff --git a/cli/test/native-binding/cleanup.ts b/cli/test/native-binding/cleanup.ts index c7aa3c7a..93f3aad7 100644 --- a/cli/test/native-binding/cleanup.ts +++ b/cli/test/native-binding/cleanup.ts @@ -1,4 +1,3 @@ -import type { DeletionError } from "./desk-paths"; import type { ILogger, OutputCleanContext, @@ -6,17 +5,17 @@ import type { OutputCleanupPathDeclaration, OutputFileDeclaration, OutputPlugin, - PluginOptions, -} from "../../src/plugins/plugin-core"; -import type { ProtectedPathRule, ProtectionMode, ProtectionRuleMatcher } from "../../src/ProtectedDeletionGuard"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import glob from "fast-glob"; -import { buildDiagnostic, buildFileOperationDiagnostic, diagnosticLines } from "@/diagnostics"; -import { compactDeletionTargets } from "../../src/cleanup/delete-targets"; -import { planWorkspaceEmptyDirectoryCleanup } from "../../src/cleanup/empty-directories"; -import { deleteEmptyDirectories, deleteTargets as deskDeleteTargets } from "./desk-paths"; -import { collectAllPluginOutputs } from "../../src/plugins/plugin-core"; + PluginOptions +} from '../../src/plugins/plugin-core' +import type {ProtectedPathRule, ProtectionMode, ProtectionRuleMatcher} from '../../src/ProtectedDeletionGuard' +import type {DeletionError} from './desk-paths' +import * as fs from 'node:fs' +import * as path from 'node:path' +import glob from 'fast-glob' +import {buildDiagnostic, buildFileOperationDiagnostic, diagnosticLines} from '@/diagnostics' +import {compactDeletionTargets} from '../../src/cleanup/delete-targets' +import {planWorkspaceEmptyDirectoryCleanup} from '../../src/cleanup/empty-directories' +import {collectAllPluginOutputs} from '../../src/plugins/plugin-core' import { buildComparisonKeys, collectConfiguredAindexInputRules, @@ -25,121 +24,122 @@ import { createProtectedDeletionGuard, logProtectedDeletionGuardError, partitionDeletionTargets, - resolveAbsolutePath, -} from "../../src/ProtectedDeletionGuard"; + resolveAbsolutePath +} from '../../src/ProtectedDeletionGuard' +import {deleteEmptyDirectories, deleteTargets as deskDeleteTargets} from './desk-paths' /** * Result of cleanup operation */ export interface CleanupResult { - readonly deletedFiles: number; - readonly deletedDirs: number; - readonly errors: readonly CleanupError[]; - readonly violations: readonly import("../../src/ProtectedDeletionGuard").ProtectedPathViolation[]; - readonly conflicts: readonly CleanupProtectionConflict[]; - readonly message?: string; + readonly deletedFiles: number + readonly deletedDirs: number + readonly errors: readonly CleanupError[] + readonly violations: readonly import('../../src/ProtectedDeletionGuard').ProtectedPathViolation[] + readonly conflicts: readonly CleanupProtectionConflict[] + readonly message?: string } /** * Error during cleanup operation */ export interface CleanupError { - readonly path: string; - readonly type: "file" | "directory"; - readonly error: unknown; + readonly path: string + readonly type: 'file' | 'directory' + readonly error: unknown } export interface CleanupProtectionConflict { - readonly outputPath: string; - readonly outputPlugin: string; - readonly protectedPath: string; - readonly protectionMode: ProtectionMode; - readonly protectedBy: string; - readonly reason: string; + readonly outputPath: string + readonly outputPlugin: string + readonly protectedPath: string + readonly protectionMode: ProtectionMode + readonly protectedBy: string + readonly reason: string } export class CleanupProtectionConflictError extends Error { - readonly conflicts: readonly CleanupProtectionConflict[]; + readonly conflicts: readonly CleanupProtectionConflict[] constructor(conflicts: readonly CleanupProtectionConflict[]) { - super(buildCleanupProtectionConflictMessage(conflicts)); - this.name = "CleanupProtectionConflictError"; - this.conflicts = conflicts; + super(buildCleanupProtectionConflictMessage(conflicts)) + this.name = 'CleanupProtectionConflictError' + this.conflicts = conflicts } } interface CleanupTargetCollections { - readonly filesToDelete: string[]; - readonly dirsToDelete: string[]; - readonly emptyDirsToDelete: string[]; - readonly violations: readonly import("../../src/ProtectedDeletionGuard").ProtectedPathViolation[]; - readonly conflicts: readonly CleanupProtectionConflict[]; - readonly excludedScanGlobs: string[]; + readonly filesToDelete: string[] + readonly dirsToDelete: string[] + readonly emptyDirsToDelete: string[] + readonly violations: readonly import('../../src/ProtectedDeletionGuard').ProtectedPathViolation[] + readonly conflicts: readonly CleanupProtectionConflict[] + readonly excludedScanGlobs: string[] } -const DEFAULT_CLEANUP_SCAN_EXCLUDE_GLOBS = ["**/node_modules/**", "**/.git/**", "**/.turbo/**", "**/.pnpm-store/**", "**/.yarn/**", "**/.next/**"] as const; +const DEFAULT_CLEANUP_SCAN_EXCLUDE_GLOBS = ['**/node_modules/**', '**/.git/**', '**/.turbo/**', '**/.pnpm-store/**', '**/.yarn/**', '**/.next/**'] as const function normalizeGlobPattern(pattern: string): string { - return resolveAbsolutePath(pattern).replaceAll("\\", "/"); + return resolveAbsolutePath(pattern).replaceAll('\\', '/') } function expandCleanupGlob(pattern: string, ignoreGlobs: readonly string[]): readonly string[] { - const normalizedPattern = normalizeGlobPattern(pattern); + const normalizedPattern = normalizeGlobPattern(pattern) return glob.sync(normalizedPattern, { onlyFiles: false, dot: true, absolute: true, followSymbolicLinks: false, - ignore: [...ignoreGlobs], - }); + ignore: [...ignoreGlobs] + }) } function shouldExcludeCleanupMatch(matchedPath: string, target: OutputCleanupPathDeclaration): boolean { - if (target.excludeBasenames == null || target.excludeBasenames.length === 0) return false; - const basename = path.basename(matchedPath); - return target.excludeBasenames.includes(basename); + if (target.excludeBasenames == null || target.excludeBasenames.length === 0) return false + const basename = path.basename(matchedPath) + return target.excludeBasenames.includes(basename) } async function collectPluginCleanupDeclarations(plugin: OutputPlugin, cleanCtx: OutputCleanContext): Promise { - if (plugin.declareCleanupPaths == null) return {}; - return plugin.declareCleanupPaths({ ...cleanCtx, dryRun: true }); + if (plugin.declareCleanupPaths == null) return {} + return plugin.declareCleanupPaths({...cleanCtx, dryRun: true}) } async function collectPluginCleanupSnapshot( plugin: OutputPlugin, cleanCtx: OutputCleanContext, - predeclaredOutputs?: ReadonlyMap, + predeclaredOutputs?: ReadonlyMap ): Promise<{ - readonly plugin: OutputPlugin; - readonly outputs: Awaited>; - readonly cleanup: OutputCleanupDeclarations; + readonly plugin: OutputPlugin + readonly outputs: Awaited> + readonly cleanup: OutputCleanupDeclarations }> { - const existingOutputDeclarations = predeclaredOutputs?.get(plugin); + const existingOutputDeclarations = predeclaredOutputs?.get(plugin) const [outputs, cleanup] = await Promise.all([ - existingOutputDeclarations != null ? Promise.resolve(existingOutputDeclarations) : plugin.declareOutputFiles({ ...cleanCtx, dryRun: true }), - collectPluginCleanupDeclarations(plugin, cleanCtx), - ]); + existingOutputDeclarations != null ? Promise.resolve(existingOutputDeclarations) : plugin.declareOutputFiles({...cleanCtx, dryRun: true}), + collectPluginCleanupDeclarations(plugin, cleanCtx) + ]) - return { plugin, outputs, cleanup }; + return {plugin, outputs, cleanup} } function buildCleanupProtectionConflictMessage(conflicts: readonly CleanupProtectionConflict[]): string { - const pathList = conflicts.map((conflict) => conflict.outputPath).join(", "); - return `Cleanup protection conflict: ${conflicts.length} output path(s) are also protected: ${pathList}`; + const pathList = conflicts.map(conflict => conflict.outputPath).join(', ') + return `Cleanup protection conflict: ${conflicts.length} output path(s) are also protected: ${pathList}` } function detectCleanupProtectionConflicts( outputPathOwners: ReadonlyMap, - guard: ReturnType, + guard: ReturnType ): CleanupProtectionConflict[] { - const conflicts: CleanupProtectionConflict[] = []; + const conflicts: CleanupProtectionConflict[] = [] for (const [outputPath, outputPlugins] of outputPathOwners.entries()) { - const outputKeys = new Set(buildComparisonKeys(outputPath)); + const outputKeys = new Set(buildComparisonKeys(outputPath)) for (const rule of guard.compiledRules) { - const isExactMatch = rule.comparisonKeys.some((ruleKey) => outputKeys.has(ruleKey)); - if (!isExactMatch) continue; + const isExactMatch = rule.comparisonKeys.some(ruleKey => outputKeys.has(ruleKey)) + if (!isExactMatch) continue for (const outputPlugin of outputPlugins) { conflicts.push({ @@ -148,50 +148,50 @@ function detectCleanupProtectionConflicts( protectedPath: rule.path, protectionMode: rule.protectionMode, protectedBy: rule.source, - reason: rule.reason, - }); + reason: rule.reason + }) } } } return conflicts.sort((a, b) => { - const pathDiff = a.outputPath.localeCompare(b.outputPath); - if (pathDiff !== 0) return pathDiff; - return a.protectedPath.localeCompare(b.protectedPath); - }); + const pathDiff = a.outputPath.localeCompare(b.outputPath) + if (pathDiff !== 0) return pathDiff + return a.protectedPath.localeCompare(b.protectedPath) + }) } function logCleanupProtectionConflicts(logger: ILogger, conflicts: readonly CleanupProtectionConflict[]): void { - const firstConflict = conflicts[0]; + const firstConflict = conflicts[0] logger.error( buildDiagnostic({ - code: "CLEANUP_PROTECTION_CONFLICT_DETECTED", - title: "Cleanup output paths conflict with protected inputs", + code: 'CLEANUP_PROTECTION_CONFLICT_DETECTED', + title: 'Cleanup output paths conflict with protected inputs', rootCause: diagnosticLines( `tnmsc found ${conflicts.length} output path(s) that also match protected cleanup rules.`, firstConflict == null - ? "No conflict details were captured." - : `Example conflict: "${firstConflict.outputPath}" is protected by "${firstConflict.protectedPath}".`, + ? 'No conflict details were captured.' + : `Example conflict: "${firstConflict.outputPath}" is protected by "${firstConflict.protectedPath}".` ), - exactFix: diagnosticLines("Separate generated output paths from protected source or reserved workspace paths before running cleanup again."), + exactFix: diagnosticLines('Separate generated output paths from protected source or reserved workspace paths before running cleanup again.'), possibleFixes: [ - diagnosticLines("Update cleanup protect declarations so they do not overlap generated outputs."), - diagnosticLines("Move the conflicting output target to a generated-only directory."), + diagnosticLines('Update cleanup protect declarations so they do not overlap generated outputs.'), + diagnosticLines('Move the conflicting output target to a generated-only directory.') ], details: { count: conflicts.length, - conflicts: conflicts.map((conflict) => ({ + conflicts: conflicts.map(conflict => ({ outputPath: conflict.outputPath, outputPlugin: conflict.outputPlugin, protectedPath: conflict.protectedPath, protectionMode: conflict.protectionMode, protectedBy: conflict.protectedBy, - reason: conflict.reason, - })), - }, - }), - ); + reason: conflict.reason + })) + } + }) + ) } /** @@ -200,44 +200,44 @@ function logCleanupProtectionConflicts(logger: ILogger, conflicts: readonly Clea async function collectCleanupTargets( outputPlugins: readonly OutputPlugin[], cleanCtx: OutputCleanContext, - predeclaredOutputs?: ReadonlyMap, + predeclaredOutputs?: ReadonlyMap ): Promise { - const deleteFiles = new Set(); - const deleteDirs = new Set(); - const protectedRules = new Map(); - const excludeScanGlobSet = new Set(DEFAULT_CLEANUP_SCAN_EXCLUDE_GLOBS); - const outputPathOwners = new Map(); + const deleteFiles = new Set() + const deleteDirs = new Set() + const protectedRules = new Map() + const excludeScanGlobSet = new Set(DEFAULT_CLEANUP_SCAN_EXCLUDE_GLOBS) + const outputPathOwners = new Map() - const pluginSnapshots = await Promise.all(outputPlugins.map(async (plugin) => collectPluginCleanupSnapshot(plugin, cleanCtx, predeclaredOutputs))); + const pluginSnapshots = await Promise.all(outputPlugins.map(async plugin => collectPluginCleanupSnapshot(plugin, cleanCtx, predeclaredOutputs))) - const addDeletePath = (rawPath: string, kind: "file" | "directory"): void => { - if (kind === "directory") deleteDirs.add(resolveAbsolutePath(rawPath)); - else deleteFiles.add(resolveAbsolutePath(rawPath)); - }; + const addDeletePath = (rawPath: string, kind: 'file' | 'directory'): void => { + if (kind === 'directory') deleteDirs.add(resolveAbsolutePath(rawPath)) + else deleteFiles.add(resolveAbsolutePath(rawPath)) + } - const addProtectRule = (rawPath: string, protectionMode: ProtectionMode, reason: string, source: string, matcher: ProtectionRuleMatcher = "path"): void => { - const resolvedPath = resolveAbsolutePath(rawPath); + const addProtectRule = (rawPath: string, protectionMode: ProtectionMode, reason: string, source: string, matcher: ProtectionRuleMatcher = 'path'): void => { + const resolvedPath = resolveAbsolutePath(rawPath) protectedRules.set(`${matcher}:${protectionMode}:${resolvedPath}`, { path: resolvedPath, protectionMode, reason, source, - matcher, - }); - }; + matcher + }) + } const defaultProtectionModeForTarget = (target: OutputCleanupPathDeclaration): ProtectionMode => { - if (target.protectionMode != null) return target.protectionMode; - return target.kind === "file" ? "direct" : "recursive"; - }; + if (target.protectionMode != null) return target.protectionMode + return target.kind === 'file' ? 'direct' : 'recursive' + } for (const rule of collectProtectedInputSourceRules(cleanCtx.collectedOutputContext)) - addProtectRule(rule.path, rule.protectionMode, rule.reason, rule.source); + { addProtectRule(rule.path, rule.protectionMode, rule.reason, rule.source) } if (cleanCtx.collectedOutputContext.aindexDir != null && cleanCtx.pluginOptions != null) { for (const rule of collectConfiguredAindexInputRules(cleanCtx.pluginOptions as Required, cleanCtx.collectedOutputContext.aindexDir, { - workspaceDir: cleanCtx.collectedOutputContext.workspace.directory.path, + workspaceDir: cleanCtx.collectedOutputContext.workspace.directory.path })) { - addProtectRule(rule.path, rule.protectionMode, rule.reason, rule.source, rule.matcher); + addProtectRule(rule.path, rule.protectionMode, rule.reason, rule.source, rule.matcher) } } @@ -245,67 +245,67 @@ async function collectCleanupTargets( addProtectRule( rule.path, rule.protectionMode, - rule.reason ?? "configured cleanup protection rule", - "configured-cleanup-protection", - rule.matcher ?? "path", - ); + rule.reason ?? 'configured cleanup protection rule', + 'configured-cleanup-protection', + rule.matcher ?? 'path' + ) } for (const snapshot of pluginSnapshots) { for (const declaration of snapshot.outputs) { - const resolvedOutputPath = resolveAbsolutePath(declaration.path); - addDeletePath(resolvedOutputPath, "file"); - const existingOwners = outputPathOwners.get(resolvedOutputPath); - if (existingOwners == null) outputPathOwners.set(resolvedOutputPath, [snapshot.plugin.name]); - else if (!existingOwners.includes(snapshot.plugin.name)) existingOwners.push(snapshot.plugin.name); + const resolvedOutputPath = resolveAbsolutePath(declaration.path) + addDeletePath(resolvedOutputPath, 'file') + const existingOwners = outputPathOwners.get(resolvedOutputPath) + if (existingOwners == null) outputPathOwners.set(resolvedOutputPath, [snapshot.plugin.name]) + else if (!existingOwners.includes(snapshot.plugin.name)) existingOwners.push(snapshot.plugin.name) } - for (const ignoreGlob of snapshot.cleanup.excludeScanGlobs ?? []) excludeScanGlobSet.add(normalizeGlobPattern(ignoreGlob)); + for (const ignoreGlob of snapshot.cleanup.excludeScanGlobs ?? []) excludeScanGlobSet.add(normalizeGlobPattern(ignoreGlob)) } - const excludeScanGlobs = [...excludeScanGlobSet]; + const excludeScanGlobs = [...excludeScanGlobSet] const resolveDeleteGlob = (target: OutputCleanupPathDeclaration): void => { for (const matchedPath of expandCleanupGlob(target.path, excludeScanGlobs)) { - if (shouldExcludeCleanupMatch(matchedPath, target)) continue; + if (shouldExcludeCleanupMatch(matchedPath, target)) continue try { - const stat = fs.lstatSync(matchedPath); - if (stat.isDirectory()) addDeletePath(matchedPath, "directory"); - else addDeletePath(matchedPath, "file"); + const stat = fs.lstatSync(matchedPath) + if (stat.isDirectory()) addDeletePath(matchedPath, 'directory') + else addDeletePath(matchedPath, 'file') } catch {} } - }; + } const resolveProtectGlob = (target: OutputCleanupPathDeclaration, pluginName: string): void => { - const protectionMode = defaultProtectionModeForTarget(target); - const reason = target.label != null ? `plugin cleanup protect declaration (${target.label})` : "plugin cleanup protect declaration"; + const protectionMode = defaultProtectionModeForTarget(target) + const reason = target.label != null ? `plugin cleanup protect declaration (${target.label})` : 'plugin cleanup protect declaration' for (const matchedPath of expandCleanupGlob(target.path, excludeScanGlobs)) { - addProtectRule(matchedPath, protectionMode, reason, `plugin-cleanup-protect:${pluginName}`); + addProtectRule(matchedPath, protectionMode, reason, `plugin-cleanup-protect:${pluginName}`) } - }; + } - for (const { plugin, cleanup } of pluginSnapshots) { + for (const {plugin, cleanup} of pluginSnapshots) { for (const target of cleanup.protect ?? []) { - if (target.kind === "glob") { - resolveProtectGlob(target, plugin.name); - continue; + if (target.kind === 'glob') { + resolveProtectGlob(target, plugin.name) + continue } addProtectRule( target.path, defaultProtectionModeForTarget(target), - target.label != null ? `plugin cleanup protect declaration (${target.label})` : "plugin cleanup protect declaration", - `plugin-cleanup-protect:${plugin.name}`, - ); + target.label != null ? `plugin cleanup protect declaration (${target.label})` : 'plugin cleanup protect declaration', + `plugin-cleanup-protect:${plugin.name}` + ) } for (const target of cleanup.delete ?? []) { - if (target.kind === "glob") { - resolveDeleteGlob(target); - continue; + if (target.kind === 'glob') { + resolveDeleteGlob(target) + continue } - if (target.kind === "directory") addDeletePath(target.path, "directory"); - else addDeletePath(target.path, "file"); + if (target.kind === 'directory') addDeletePath(target.path, 'directory') + else addDeletePath(target.path, 'file') } } @@ -313,21 +313,21 @@ async function collectCleanupTargets( workspaceDir: cleanCtx.collectedOutputContext.workspace.directory.path, projectRoots: collectProjectRoots(cleanCtx.collectedOutputContext), rules: [...protectedRules.values()], - ...(cleanCtx.collectedOutputContext.aindexDir != null ? { aindexDir: cleanCtx.collectedOutputContext.aindexDir } : {}), - }); - const conflicts = detectCleanupProtectionConflicts(outputPathOwners, guard); - if (conflicts.length > 0) throw new CleanupProtectionConflictError(conflicts); - const filePartition = partitionDeletionTargets([...deleteFiles], guard); - const dirPartition = partitionDeletionTargets([...deleteDirs], guard); - - const compactedTargets = compactDeletionTargets(filePartition.safePaths, dirPartition.safePaths); + ...cleanCtx.collectedOutputContext.aindexDir != null ? {aindexDir: cleanCtx.collectedOutputContext.aindexDir} : {} + }) + const conflicts = detectCleanupProtectionConflicts(outputPathOwners, guard) + if (conflicts.length > 0) throw new CleanupProtectionConflictError(conflicts) + const filePartition = partitionDeletionTargets([...deleteFiles], guard) + const dirPartition = partitionDeletionTargets([...deleteDirs], guard) + + const compactedTargets = compactDeletionTargets(filePartition.safePaths, dirPartition.safePaths) const emptyDirectoryPlan = planWorkspaceEmptyDirectoryCleanup({ fs, path, workspaceDir: cleanCtx.collectedOutputContext.workspace.directory.path, filesToDelete: compactedTargets.files, - dirsToDelete: compactedTargets.dirs, - }); + dirsToDelete: compactedTargets.dirs + }) return { filesToDelete: compactedTargets.files, @@ -335,96 +335,96 @@ async function collectCleanupTargets( emptyDirsToDelete: emptyDirectoryPlan.emptyDirsToDelete, violations: [...filePartition.violations, ...dirPartition.violations].sort((a, b) => a.targetPath.localeCompare(b.targetPath)), conflicts: [], - excludedScanGlobs: [...excludeScanGlobSet].sort((a, b) => a.localeCompare(b)), - }; + excludedScanGlobs: [...excludeScanGlobSet].sort((a, b) => a.localeCompare(b)) + } } export async function collectDeletionTargets( outputPlugins: readonly OutputPlugin[], cleanCtx: OutputCleanContext, - predeclaredOutputs?: ReadonlyMap, + predeclaredOutputs?: ReadonlyMap ): Promise<{ - filesToDelete: string[]; - dirsToDelete: string[]; - emptyDirsToDelete: string[]; - violations: import("../../src/ProtectedDeletionGuard").ProtectedPathViolation[]; - conflicts: CleanupProtectionConflict[]; - excludedScanGlobs: string[]; + filesToDelete: string[] + dirsToDelete: string[] + emptyDirsToDelete: string[] + violations: import('../../src/ProtectedDeletionGuard').ProtectedPathViolation[] + conflicts: CleanupProtectionConflict[] + excludedScanGlobs: string[] }> { - const targets = await collectCleanupTargets(outputPlugins, cleanCtx, predeclaredOutputs); + const targets = await collectCleanupTargets(outputPlugins, cleanCtx, predeclaredOutputs) return { filesToDelete: targets.filesToDelete, dirsToDelete: targets.dirsToDelete.sort((a, b) => a.localeCompare(b)), emptyDirsToDelete: targets.emptyDirsToDelete.sort((a, b) => a.localeCompare(b)), violations: [...targets.violations], conflicts: [...targets.conflicts], - excludedScanGlobs: [...targets.excludedScanGlobs], - }; + excludedScanGlobs: [...targets.excludedScanGlobs] + } } -function buildCleanupErrors(logger: ILogger, errors: readonly DeletionError[], type: "file" | "directory"): CleanupError[] { - return errors.map((currentError) => { - const errorMessage = currentError.error instanceof Error ? currentError.error.message : String(currentError.error); +function buildCleanupErrors(logger: ILogger, errors: readonly DeletionError[], type: 'file' | 'directory'): CleanupError[] { + return errors.map(currentError => { + const errorMessage = currentError.error instanceof Error ? currentError.error.message : String(currentError.error) logger.warn( buildFileOperationDiagnostic({ - code: type === "file" ? "CLEANUP_FILE_DELETE_FAILED" : "CLEANUP_DIRECTORY_DELETE_FAILED", - title: type === "file" ? "Cleanup could not delete a file" : "Cleanup could not delete a directory", - operation: "delete", + code: type === 'file' ? 'CLEANUP_FILE_DELETE_FAILED' : 'CLEANUP_DIRECTORY_DELETE_FAILED', + title: type === 'file' ? 'Cleanup could not delete a file' : 'Cleanup could not delete a directory', + operation: 'delete', targetKind: type, path: currentError.path, error: errorMessage, details: { - phase: "cleanup", - }, - }), - ); + phase: 'cleanup' + } + }) + ) - return { path: currentError.path, type, error: currentError.error }; - }); + return {path: currentError.path, type, error: currentError.error} + }) } async function executeCleanupTargets( targets: CleanupTargetCollections, - logger: ILogger, -): Promise<{ deletedFiles: number; deletedDirs: number; errors: CleanupError[] }> { - logger.debug("cleanup delete execution started", { + logger: ILogger +): Promise<{deletedFiles: number, deletedDirs: number, errors: CleanupError[]}> { + logger.debug('cleanup delete execution started', { filesToDelete: targets.filesToDelete.length, dirsToDelete: targets.dirsToDelete.length + targets.emptyDirsToDelete.length, - emptyDirsToDelete: targets.emptyDirsToDelete.length, - }); + emptyDirsToDelete: targets.emptyDirsToDelete.length + }) const result = await deskDeleteTargets({ files: targets.filesToDelete, - dirs: targets.dirsToDelete, - }); - const emptyDirResult = await deleteEmptyDirectories(targets.emptyDirsToDelete); + dirs: targets.dirsToDelete + }) + const emptyDirResult = await deleteEmptyDirectories(targets.emptyDirsToDelete) - const fileErrors = buildCleanupErrors(logger, result.fileErrors, "file"); - const dirErrors = buildCleanupErrors(logger, [...result.dirErrors, ...emptyDirResult.errors], "directory"); - const allErrors = [...fileErrors, ...dirErrors]; + const fileErrors = buildCleanupErrors(logger, result.fileErrors, 'file') + const dirErrors = buildCleanupErrors(logger, [...result.dirErrors, ...emptyDirResult.errors], 'directory') + const allErrors = [...fileErrors, ...dirErrors] - logger.debug("cleanup delete execution complete", { + logger.debug('cleanup delete execution complete', { deletedFiles: result.deletedFiles.length, deletedDirs: result.deletedDirs.length + emptyDirResult.deletedPaths.length, - errors: allErrors.length, - }); + errors: allErrors.length + }) return { deletedFiles: result.deletedFiles.length, deletedDirs: result.deletedDirs.length + emptyDirResult.deletedPaths.length, - errors: allErrors, - }; + errors: allErrors + } } function logCleanupPlanDiagnostics(logger: ILogger, targets: CleanupTargetCollections): void { - logger.debug("cleanup plan built", { + logger.debug('cleanup plan built', { filesToDelete: targets.filesToDelete.length, dirsToDelete: targets.dirsToDelete.length + targets.emptyDirsToDelete.length, emptyDirsToDelete: targets.emptyDirsToDelete.length, violations: targets.violations.length, conflicts: targets.conflicts.length, - excludedScanGlobs: targets.excludedScanGlobs, - }); + excludedScanGlobs: targets.excludedScanGlobs + }) } /** @@ -436,34 +436,34 @@ export async function performCleanup( outputPlugins: readonly OutputPlugin[], cleanCtx: OutputCleanContext, logger: ILogger, - predeclaredOutputs?: ReadonlyMap, + predeclaredOutputs?: ReadonlyMap ): Promise { if (predeclaredOutputs != null) { - const outputs = await collectAllPluginOutputs(outputPlugins, cleanCtx, predeclaredOutputs); - logger.debug("Collected outputs for cleanup", { + const outputs = await collectAllPluginOutputs(outputPlugins, cleanCtx, predeclaredOutputs) + logger.debug('Collected outputs for cleanup', { projectDirs: outputs.projectDirs.length, projectFiles: outputs.projectFiles.length, globalDirs: outputs.globalDirs.length, - globalFiles: outputs.globalFiles.length, - }); + globalFiles: outputs.globalFiles.length + }) } - let targets: CleanupTargetCollections; + let targets: CleanupTargetCollections try { - targets = await collectCleanupTargets(outputPlugins, cleanCtx, predeclaredOutputs); + targets = await collectCleanupTargets(outputPlugins, cleanCtx, predeclaredOutputs) } catch (error) { if (error instanceof CleanupProtectionConflictError) { - logCleanupProtectionConflicts(logger, error.conflicts); + logCleanupProtectionConflicts(logger, error.conflicts) return { deletedFiles: 0, deletedDirs: 0, errors: [], violations: [], conflicts: error.conflicts, - message: error.message, - }; + message: error.message + } } - throw error; + throw error } const cleanupTargets: CleanupTargetCollections = { filesToDelete: targets.filesToDelete, @@ -471,29 +471,29 @@ export async function performCleanup( emptyDirsToDelete: targets.emptyDirsToDelete, violations: targets.violations, conflicts: targets.conflicts, - excludedScanGlobs: targets.excludedScanGlobs, - }; - logCleanupPlanDiagnostics(logger, cleanupTargets); + excludedScanGlobs: targets.excludedScanGlobs + } + logCleanupPlanDiagnostics(logger, cleanupTargets) if (cleanupTargets.violations.length > 0) { - logProtectedDeletionGuardError(logger, "cleanup", cleanupTargets.violations); + logProtectedDeletionGuardError(logger, 'cleanup', cleanupTargets.violations) return { deletedFiles: 0, deletedDirs: 0, errors: [], violations: cleanupTargets.violations, conflicts: [], - message: `Protected deletion guard blocked cleanup for ${cleanupTargets.violations.length} path(s)`, - }; + message: `Protected deletion guard blocked cleanup for ${cleanupTargets.violations.length} path(s)` + } } - const executionResult = await executeCleanupTargets(cleanupTargets, logger); + const executionResult = await executeCleanupTargets(cleanupTargets, logger) return { deletedFiles: executionResult.deletedFiles, deletedDirs: executionResult.deletedDirs, errors: executionResult.errors, violations: [], - conflicts: [], - }; + conflicts: [] + } } diff --git a/cli/test/setup-native-binding.ts b/cli/test/setup-native-binding.ts index da543ed2..48fa5e04 100644 --- a/cli/test/setup-native-binding.ts +++ b/cli/test/setup-native-binding.ts @@ -1,45 +1,45 @@ -import type { ILogger, OutputCleanContext, OutputCleanupDeclarations, OutputPlugin } from "../src/plugins/plugin-core"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import glob from "fast-glob"; -import * as deskPaths from "./native-binding/desk-paths"; -import { FilePathKind, PluginKind } from "../src/plugins/plugin-core/enums"; +import type {ILogger, OutputCleanContext, OutputCleanupDeclarations, OutputPlugin} from '../src/plugins/plugin-core' +import * as fs from 'node:fs' +import * as path from 'node:path' +import glob from 'fast-glob' +import {FilePathKind, PluginKind} from '../src/plugins/plugin-core/enums' +import * as deskPaths from './native-binding/desk-paths' interface NativeCleanupTarget { - readonly path: string; - readonly kind: "file" | "directory" | "glob"; - readonly excludeBasenames?: readonly string[]; - readonly protectionMode?: "direct" | "recursive"; - readonly scope?: string; - readonly label?: string; + readonly path: string + readonly kind: 'file' | 'directory' | 'glob' + readonly excludeBasenames?: readonly string[] + readonly protectionMode?: 'direct' | 'recursive' + readonly scope?: string + readonly label?: string } interface NativeCleanupDeclarations { - readonly delete?: readonly NativeCleanupTarget[]; - readonly protect?: readonly NativeCleanupTarget[]; - readonly excludeScanGlobs?: readonly string[]; + readonly delete?: readonly NativeCleanupTarget[] + readonly protect?: readonly NativeCleanupTarget[] + readonly excludeScanGlobs?: readonly string[] } interface NativePluginCleanupSnapshot { - readonly pluginName: string; - readonly outputs: readonly string[]; - readonly cleanup: NativeCleanupDeclarations; + readonly pluginName: string + readonly outputs: readonly string[] + readonly cleanup: NativeCleanupDeclarations } interface NativeProtectedRule { - readonly path: string; - readonly protectionMode: "direct" | "recursive"; - readonly reason: string; - readonly source: string; - readonly matcher?: "path" | "glob"; + readonly path: string + readonly protectionMode: 'direct' | 'recursive' + readonly reason: string + readonly source: string + readonly matcher?: 'path' | 'glob' } interface NativeCleanupSnapshot { - readonly workspaceDir: string; - readonly aindexDir?: string; - readonly projectRoots: readonly string[]; - readonly protectedRules: readonly NativeProtectedRule[]; - readonly pluginSnapshots: readonly NativePluginCleanupSnapshot[]; + readonly workspaceDir: string + readonly aindexDir?: string + readonly projectRoots: readonly string[] + readonly protectedRules: readonly NativeProtectedRule[] + readonly pluginSnapshots: readonly NativePluginCleanupSnapshot[] } function createMockLogger(): ILogger { @@ -49,8 +49,8 @@ function createMockLogger(): ILogger { info: () => {}, warn: () => {}, error: () => {}, - fatal: () => {}, - } as ILogger; + fatal: () => {} + } as ILogger } function createSyntheticOutputPlugin(snapshot: NativePluginCleanupSnapshot): OutputPlugin { @@ -61,38 +61,38 @@ function createSyntheticOutputPlugin(snapshot: NativePluginCleanupSnapshot): Out declarativeOutput: true, outputCapabilities: {}, async declareOutputFiles() { - return snapshot.outputs.map((output) => ({ path: output, source: {} })); + return snapshot.outputs.map(output => ({path: output, source: {}})) }, async declareCleanupPaths(): Promise { return { - ...(snapshot.cleanup.delete != null ? { delete: [...snapshot.cleanup.delete] as OutputCleanupDeclarations["delete"] } : {}), - ...(snapshot.cleanup.protect != null ? { protect: [...snapshot.cleanup.protect] as OutputCleanupDeclarations["protect"] } : {}), - ...(snapshot.cleanup.excludeScanGlobs != null ? { excludeScanGlobs: [...snapshot.cleanup.excludeScanGlobs] } : {}), - }; + ...snapshot.cleanup.delete != null ? {delete: [...snapshot.cleanup.delete] as OutputCleanupDeclarations['delete']} : {}, + ...snapshot.cleanup.protect != null ? {protect: [...snapshot.cleanup.protect] as OutputCleanupDeclarations['protect']} : {}, + ...snapshot.cleanup.excludeScanGlobs != null ? {excludeScanGlobs: [...snapshot.cleanup.excludeScanGlobs]} : {} + } }, async convertContent() { - return ""; - }, - }; + return '' + } + } } async function createSyntheticCleanContext(snapshot: NativeCleanupSnapshot): Promise { - const { mergeConfig } = await import("../src/config"); - const workspaceDir = path.resolve(snapshot.workspaceDir); - const cleanupProtectionRules = snapshot.protectedRules.map((rule) => ({ + const {mergeConfig} = await import('../src/config') + const workspaceDir = path.resolve(snapshot.workspaceDir) + const cleanupProtectionRules = snapshot.protectedRules.map(rule => ({ path: rule.path, protectionMode: rule.protectionMode, reason: rule.reason, - matcher: rule.matcher ?? "path", - })); + matcher: rule.matcher ?? 'path' + })) if (snapshot.aindexDir != null) { cleanupProtectionRules.push({ path: snapshot.aindexDir, - protectionMode: "direct", - reason: "resolved aindex root", - matcher: "path", - }); + protectionMode: 'direct', + reason: 'resolved aindex root', + matcher: 'path' + }) } return { @@ -104,8 +104,8 @@ async function createSyntheticCleanContext(snapshot: NativeCleanupSnapshot): Pro pluginOptions: mergeConfig({ workspaceDir, cleanupProtection: { - rules: cleanupProtectionRules, - }, + rules: cleanupProtectionRules + } }), collectedOutputContext: { workspace: { @@ -113,28 +113,28 @@ async function createSyntheticCleanContext(snapshot: NativeCleanupSnapshot): Pro pathKind: FilePathKind.Absolute, path: workspaceDir, getDirectoryName: () => path.basename(workspaceDir), - getAbsolutePath: () => workspaceDir, + getAbsolutePath: () => workspaceDir }, - projects: snapshot.projectRoots.map((projectRoot) => ({ + projects: snapshot.projectRoots.map(projectRoot => ({ dirFromWorkspacePath: { pathKind: FilePathKind.Relative, - path: path.relative(workspaceDir, projectRoot) || ".", + path: path.relative(workspaceDir, projectRoot) || '.', basePath: workspaceDir, getDirectoryName: () => path.basename(projectRoot), - getAbsolutePath: () => projectRoot, - }, - })), - }, - }, - } as unknown as OutputCleanContext; + getAbsolutePath: () => projectRoot + } + })) + } + } + } as unknown as OutputCleanContext } async function planCleanup(snapshotJson: string): Promise { - const { collectDeletionTargets } = await import("./native-binding/cleanup"); - const snapshot = JSON.parse(snapshotJson) as NativeCleanupSnapshot; - const outputPlugins = snapshot.pluginSnapshots.map(createSyntheticOutputPlugin); - const cleanCtx = await createSyntheticCleanContext(snapshot); - const result = await collectDeletionTargets(outputPlugins, cleanCtx); + const {collectDeletionTargets} = await import('./native-binding/cleanup') + const snapshot = JSON.parse(snapshotJson) as NativeCleanupSnapshot + const outputPlugins = snapshot.pluginSnapshots.map(createSyntheticOutputPlugin) + const cleanCtx = await createSyntheticCleanContext(snapshot) + const result = await collectDeletionTargets(outputPlugins, cleanCtx) return JSON.stringify({ filesToDelete: result.filesToDelete, @@ -142,58 +142,58 @@ async function planCleanup(snapshotJson: string): Promise { emptyDirsToDelete: result.emptyDirsToDelete, violations: result.violations, conflicts: result.conflicts, - excludedScanGlobs: result.excludedScanGlobs, - }); + excludedScanGlobs: result.excludedScanGlobs + }) } async function runCleanup(snapshotJson: string): Promise { - const { performCleanup } = await import("./native-binding/cleanup"); - const snapshot = JSON.parse(snapshotJson) as NativeCleanupSnapshot; - const outputPlugins = snapshot.pluginSnapshots.map(createSyntheticOutputPlugin); - const cleanCtx = await createSyntheticCleanContext(snapshot); - const result = await performCleanup(outputPlugins, cleanCtx, createMockLogger()); + const {performCleanup} = await import('./native-binding/cleanup') + const snapshot = JSON.parse(snapshotJson) as NativeCleanupSnapshot + const outputPlugins = snapshot.pluginSnapshots.map(createSyntheticOutputPlugin) + const cleanCtx = await createSyntheticCleanContext(snapshot) + const result = await performCleanup(outputPlugins, cleanCtx, createMockLogger()) return JSON.stringify({ deletedFiles: result.deletedFiles, deletedDirs: result.deletedDirs, - errors: result.errors.map((error) => ({ + errors: result.errors.map(error => ({ path: error.path, kind: error.type, - error: error.error instanceof Error ? error.error.message : String(error.error), + error: error.error instanceof Error ? error.error.message : String(error.error) })), violations: result.violations, conflicts: result.conflicts, filesToDelete: [], dirsToDelete: [], emptyDirsToDelete: [], - excludedScanGlobs: [], - }); + excludedScanGlobs: [] + }) } function resolveEffectiveIncludeSeries(topLevel?: readonly string[], typeSpecific?: readonly string[]): string[] { - if (topLevel == null && typeSpecific == null) return []; - return [...new Set([...(topLevel ?? []), ...(typeSpecific ?? [])])]; + if (topLevel == null && typeSpecific == null) return [] + return [...new Set([...topLevel ?? [], ...typeSpecific ?? []])] } function matchesSeries(seriName: string | readonly string[] | null | undefined, effectiveIncludeSeries: readonly string[]): boolean { - if (seriName == null) return true; - if (effectiveIncludeSeries.length === 0) return true; - if (typeof seriName === "string") return effectiveIncludeSeries.includes(seriName); - return seriName.some((name) => effectiveIncludeSeries.includes(name)); + if (seriName == null) return true + if (effectiveIncludeSeries.length === 0) return true + if (typeof seriName === 'string') return effectiveIncludeSeries.includes(seriName) + return seriName.some(name => effectiveIncludeSeries.includes(name)) } function resolveSubSeries( topLevel?: Readonly>, - typeSpecific?: Readonly>, + typeSpecific?: Readonly> ): Record { - if (topLevel == null && typeSpecific == null) return {}; - const merged: Record = {}; - for (const [key, values] of Object.entries(topLevel ?? {})) merged[key] = [...values]; + if (topLevel == null && typeSpecific == null) return {} + const merged: Record = {} + for (const [key, values] of Object.entries(topLevel ?? {})) merged[key] = [...values] for (const [key, values] of Object.entries(typeSpecific ?? {})) { - const existingValues = merged[key] ?? []; - merged[key] = Object.hasOwn(merged, key) ? [...new Set([...existingValues, ...values])] : [...values]; + const existingValues = merged[key] ?? [] + merged[key] = Object.hasOwn(merged, key) ? [...new Set([...existingValues, ...values])] : [...values] } - return merged; + return merged } globalThis.__TNMSC_TEST_NATIVE_BINDING__ = { @@ -211,5 +211,5 @@ globalThis.__TNMSC_TEST_NATIVE_BINDING__ = { performCleanup: runCleanup, resolveEffectiveIncludeSeries, matchesSeries, - resolveSubSeries, -}; + resolveSubSeries +} diff --git a/cli/tsconfig.eslint.json b/cli/tsconfig.eslint.json index 585b38ee..62f8268e 100644 --- a/cli/tsconfig.eslint.json +++ b/cli/tsconfig.eslint.json @@ -9,15 +9,12 @@ "src/**/*.ts", "src/**/*.test.ts", "src/**/*.spec.ts", + "test/**/*.ts", "env.d.ts", "eslint.config.ts", "tsdown.config.ts", "vite.config.ts", "vitest.config.ts" ], - "exclude": [ - "../node_modules", - "dist", - "coverage" - ] + "exclude": ["../node_modules", "dist", "coverage"] } diff --git a/cli/tsconfig.json b/cli/tsconfig.json index e27cc557..9006c87e 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -5,17 +5,13 @@ "incremental": true, "composite": false, "target": "ESNext", - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "moduleDetection": "force", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Bundler", "paths": { - "@/*": [ - "./src/*" - ], + "@/*": ["./src/*"], "@truenine/desk-paths": ["./src/core/desk-paths.ts"], "@truenine/desk-paths/*": ["./src/core/desk-paths/*"], "@truenine/plugin-output-shared": ["./src/plugins/plugin-output-shared/index.ts"], @@ -44,9 +40,7 @@ "@truenine/plugin-zed": ["./src/plugins/plugin-zed.ts"] }, "resolveJsonModule": true, - "types": [ - "node" - ], + "types": ["node"], "allowImportingTsExtensions": true, "strict": true, "strictBindCallApply": true, @@ -83,16 +77,6 @@ "verbatimModuleSyntax": true, "skipLibCheck": true }, - "include": [ - "src/**/*", - "env.d.ts", - "eslint.config.ts", - "tsdown.config.ts", - "vite.config.ts", - "vitest.config.ts" - ], - "exclude": [ - "../node_modules", - "dist" - ] + "include": ["src/**/*", "test/**/*.ts", "env.d.ts", "eslint.config.ts", "tsdown.config.ts", "vite.config.ts", "vitest.config.ts"], + "exclude": ["../node_modules", "dist"] } From 9f6d7ed607758cfe52d766d8154e4ecda614d54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 10:07:08 +0800 Subject: [PATCH 3/7] Fix pnpm setup order in GitHub action --- .github/actions/setup-node-pnpm/action.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index 456047ac..d2ef2cbd 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -5,15 +5,18 @@ inputs: node-version: description: Node.js version required: false - default: '25' + default: "25" install: description: Whether to run pnpm install required: false - default: 'true' + default: "true" runs: using: composite steps: + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Setup Node uses: actions/setup-node@v6 with: @@ -21,13 +24,6 @@ runs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Setup pnpm - shell: bash - run: | - corepack enable - corepack prepare "$(node -p "require('./package.json').packageManager")" --activate - pnpm --version - - name: Install workspace dependencies if: inputs.install == 'true' shell: bash From f979e549f4c3e1f626def78de246c03f8e110186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 10:15:31 +0800 Subject: [PATCH 4/7] Fix CLI native binding test global typing --- cli/env.d.ts | 10 +++-- cli/src/core/native-binding.ts | 76 +++++++++++++++------------------- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/cli/env.d.ts b/cli/env.d.ts index 54f2842b..0eaf95d1 100644 --- a/cli/env.d.ts +++ b/cli/env.d.ts @@ -5,14 +5,18 @@ /** * CLI version injected at build time from package.json */ -declare const __CLI_VERSION__: string +declare const __CLI_VERSION__: string; /** * CLI package name injected at build time from package.json */ -declare const __CLI_PACKAGE_NAME__: string +declare const __CLI_PACKAGE_NAME__: string; /** * Kiro global powers registry JSON string injected at build time */ -declare const __KIRO_GLOBAL_POWERS_REGISTRY__: string +declare const __KIRO_GLOBAL_POWERS_REGISTRY__: string; + +interface GlobalThis { + __TNMSC_TEST_NATIVE_BINDING__?: object; +} diff --git a/cli/src/core/native-binding.ts b/cli/src/core/native-binding.ts index 0ab5605d..a22b9d8c 100644 --- a/cli/src/core/native-binding.ts +++ b/cli/src/core/native-binding.ts @@ -1,40 +1,33 @@ -import {createRequire} from 'node:module' -import process from 'node:process' - -declare global { - interface GlobalThis { - __TNMSC_TEST_NATIVE_BINDING__?: object - } -} +import { createRequire } from "node:module"; +import process from "node:process"; function shouldSkipNativeBinding(): boolean { - if (process.env['TNMSC_FORCE_NATIVE_BINDING'] === '1') return false - if (process.env['TNMSC_DISABLE_NATIVE_BINDING'] === '1') return true + if (process.env["TNMSC_FORCE_NATIVE_BINDING"] === "1") return false; + if (process.env["TNMSC_DISABLE_NATIVE_BINDING"] === "1") return true; - return process.env['NODE_ENV'] === 'test' - || process.env['VITEST'] != null - || process.env['VITEST_WORKER_ID'] != null + return process.env["NODE_ENV"] === "test" || process.env["VITEST"] != null || process.env["VITEST_WORKER_ID"] != null; } export function tryLoadNativeBinding(): T | undefined { - const testBinding: unknown = globalThis.__TNMSC_TEST_NATIVE_BINDING__ - if (testBinding != null && typeof testBinding === 'object') return testBinding as T - if (shouldSkipNativeBinding()) return void 0 + const testGlobals = globalThis as typeof globalThis & { __TNMSC_TEST_NATIVE_BINDING__?: object }; + const testBinding: unknown = testGlobals.__TNMSC_TEST_NATIVE_BINDING__; + if (testBinding != null && typeof testBinding === "object") return testBinding as T; + if (shouldSkipNativeBinding()) return void 0; const suffixMap: Readonly> = { - 'win32-x64': 'win32-x64-msvc', - 'linux-x64': 'linux-x64-gnu', - 'linux-arm64': 'linux-arm64-gnu', - 'darwin-arm64': 'darwin-arm64', - 'darwin-x64': 'darwin-x64' - } - const suffix = suffixMap[`${process.platform}-${process.arch}`] - if (suffix == null) return void 0 + "win32-x64": "win32-x64-msvc", + "linux-x64": "linux-x64-gnu", + "linux-arm64": "linux-arm64-gnu", + "darwin-arm64": "darwin-arm64", + "darwin-x64": "darwin-x64", + }; + const suffix = suffixMap[`${process.platform}-${process.arch}`]; + if (suffix == null) return void 0; try { - const _require = createRequire(import.meta.url) - const packageName = `@truenine/memory-sync-cli-${suffix}` - const binaryFile = `napi-memory-sync-cli.${suffix}.node` + const _require = createRequire(import.meta.url); + const packageName = `@truenine/memory-sync-cli-${suffix}`; + const binaryFile = `napi-memory-sync-cli.${suffix}.node`; const candidates = [ packageName, `${packageName}/${binaryFile}`, @@ -42,32 +35,29 @@ export function tryLoadNativeBinding(): T | undefined { `../npm/${suffix}`, `../npm/${suffix}/${binaryFile}`, `../../npm/${suffix}`, - `../../npm/${suffix}/${binaryFile}` - ] + `../../npm/${suffix}/${binaryFile}`, + ]; for (const specifier of candidates) { try { - const loaded = _require(specifier) as unknown + const loaded = _require(specifier) as unknown; const possibleBindings = [ - (loaded as {config?: unknown})?.config, - (loaded as {default?: {config?: unknown}})?.default?.config, - (loaded as {default?: unknown})?.default, - loaded - ] + (loaded as { config?: unknown })?.config, + (loaded as { default?: { config?: unknown } })?.default?.config, + (loaded as { default?: unknown })?.default, + loaded, + ]; for (const candidate of possibleBindings) { - if (candidate != null && typeof candidate === 'object') return candidate as T + if (candidate != null && typeof candidate === "object") return candidate as T; } - } - catch {} + } catch {} } - } - catch { - } + } catch {} - return void 0 + return void 0; } export function getNativeBinding(): T | undefined { - return tryLoadNativeBinding() + return tryLoadNativeBinding(); } From e163aebe8fd8df7ce037aa26051bca055746c58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 10:20:02 +0800 Subject: [PATCH 5/7] Fix CLI lint for native binding typing --- cli/env.d.ts | 8 ++--- cli/src/core/native-binding.ts | 62 +++++++++++++++++----------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/cli/env.d.ts b/cli/env.d.ts index 0eaf95d1..df4bb8c8 100644 --- a/cli/env.d.ts +++ b/cli/env.d.ts @@ -5,18 +5,18 @@ /** * CLI version injected at build time from package.json */ -declare const __CLI_VERSION__: string; +declare const __CLI_VERSION__: string /** * CLI package name injected at build time from package.json */ -declare const __CLI_PACKAGE_NAME__: string; +declare const __CLI_PACKAGE_NAME__: string /** * Kiro global powers registry JSON string injected at build time */ -declare const __KIRO_GLOBAL_POWERS_REGISTRY__: string; +declare const __KIRO_GLOBAL_POWERS_REGISTRY__: string interface GlobalThis { - __TNMSC_TEST_NATIVE_BINDING__?: object; + __TNMSC_TEST_NATIVE_BINDING__?: object } diff --git a/cli/src/core/native-binding.ts b/cli/src/core/native-binding.ts index a22b9d8c..d761cef8 100644 --- a/cli/src/core/native-binding.ts +++ b/cli/src/core/native-binding.ts @@ -1,33 +1,33 @@ -import { createRequire } from "node:module"; -import process from "node:process"; +import {createRequire} from 'node:module' +import process from 'node:process' function shouldSkipNativeBinding(): boolean { - if (process.env["TNMSC_FORCE_NATIVE_BINDING"] === "1") return false; - if (process.env["TNMSC_DISABLE_NATIVE_BINDING"] === "1") return true; + if (process.env['TNMSC_FORCE_NATIVE_BINDING'] === '1') return false + if (process.env['TNMSC_DISABLE_NATIVE_BINDING'] === '1') return true - return process.env["NODE_ENV"] === "test" || process.env["VITEST"] != null || process.env["VITEST_WORKER_ID"] != null; + return process.env['NODE_ENV'] === 'test' || process.env['VITEST'] != null || process.env['VITEST_WORKER_ID'] != null } export function tryLoadNativeBinding(): T | undefined { - const testGlobals = globalThis as typeof globalThis & { __TNMSC_TEST_NATIVE_BINDING__?: object }; - const testBinding: unknown = testGlobals.__TNMSC_TEST_NATIVE_BINDING__; - if (testBinding != null && typeof testBinding === "object") return testBinding as T; - if (shouldSkipNativeBinding()) return void 0; + const testGlobals = globalThis as typeof globalThis & {__TNMSC_TEST_NATIVE_BINDING__?: object} + const testBinding: unknown = testGlobals.__TNMSC_TEST_NATIVE_BINDING__ + if (testBinding != null && typeof testBinding === 'object') return testBinding as T + if (shouldSkipNativeBinding()) return void 0 const suffixMap: Readonly> = { - "win32-x64": "win32-x64-msvc", - "linux-x64": "linux-x64-gnu", - "linux-arm64": "linux-arm64-gnu", - "darwin-arm64": "darwin-arm64", - "darwin-x64": "darwin-x64", - }; - const suffix = suffixMap[`${process.platform}-${process.arch}`]; - if (suffix == null) return void 0; + 'win32-x64': 'win32-x64-msvc', + 'linux-x64': 'linux-x64-gnu', + 'linux-arm64': 'linux-arm64-gnu', + 'darwin-arm64': 'darwin-arm64', + 'darwin-x64': 'darwin-x64' + } + const suffix = suffixMap[`${process.platform}-${process.arch}`] + if (suffix == null) return void 0 try { - const _require = createRequire(import.meta.url); - const packageName = `@truenine/memory-sync-cli-${suffix}`; - const binaryFile = `napi-memory-sync-cli.${suffix}.node`; + const _require = createRequire(import.meta.url) + const packageName = `@truenine/memory-sync-cli-${suffix}` + const binaryFile = `napi-memory-sync-cli.${suffix}.node` const candidates = [ packageName, `${packageName}/${binaryFile}`, @@ -35,29 +35,29 @@ export function tryLoadNativeBinding(): T | undefined { `../npm/${suffix}`, `../npm/${suffix}/${binaryFile}`, `../../npm/${suffix}`, - `../../npm/${suffix}/${binaryFile}`, - ]; + `../../npm/${suffix}/${binaryFile}` + ] for (const specifier of candidates) { try { - const loaded = _require(specifier) as unknown; + const loaded = _require(specifier) as unknown const possibleBindings = [ - (loaded as { config?: unknown })?.config, - (loaded as { default?: { config?: unknown } })?.default?.config, - (loaded as { default?: unknown })?.default, - loaded, - ]; + (loaded as {config?: unknown})?.config, + (loaded as {default?: {config?: unknown}})?.default?.config, + (loaded as {default?: unknown})?.default, + loaded + ] for (const candidate of possibleBindings) { - if (candidate != null && typeof candidate === "object") return candidate as T; + if (candidate != null && typeof candidate === 'object') return candidate as T } } catch {} } } catch {} - return void 0; + return void 0 } export function getNativeBinding(): T | undefined { - return tryLoadNativeBinding(); + return tryLoadNativeBinding() } From df3d097fe02876f087cb2f11a8388b3f3fc7d669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 10:30:20 +0800 Subject: [PATCH 6/7] Fix CLI napi build for MCP tests --- cli/package.json | 4 +- cli/test/native-binding/cleanup.ts | 7 +-- cli/test/native-binding/desk-paths.ts | 76 ++++++++++----------------- 3 files changed, 35 insertions(+), 52 deletions(-) diff --git a/cli/package.json b/cli/package.json index 8cd72ad5..5f5c09ed 100644 --- a/cli/package.json +++ b/cli/package.json @@ -49,7 +49,9 @@ }, "scripts": { "build": "run-s build:deps build:napi bundle finalize:bundle generate:schema", - "build:napi": "tsx ../scripts/copy-napi.ts", + "build:napi": "run-s build:native build:napi:copy", + "build:napi:copy": "tsx ../scripts/copy-napi.ts", + "build:native": "napi build --platform --release --output-dir dist -- --features napi", "build:deps": "pnpm -F @truenine/logger -F @truenine/md-compiler -F @truenine/script-runtime run build", "build:deps:ts": "pnpm -F @truenine/logger -F @truenine/md-compiler -F @truenine/script-runtime run build:ts", "bundle": "tsx ../scripts/build-quiet.ts", diff --git a/cli/test/native-binding/cleanup.ts b/cli/test/native-binding/cleanup.ts index 93f3aad7..d1320e98 100644 --- a/cli/test/native-binding/cleanup.ts +++ b/cli/test/native-binding/cleanup.ts @@ -12,9 +12,9 @@ import type {DeletionError} from './desk-paths' import * as fs from 'node:fs' import * as path from 'node:path' import glob from 'fast-glob' -import {buildDiagnostic, buildFileOperationDiagnostic, diagnosticLines} from '@/diagnostics' import {compactDeletionTargets} from '../../src/cleanup/delete-targets' import {planWorkspaceEmptyDirectoryCleanup} from '../../src/cleanup/empty-directories' +import {buildDiagnostic, buildFileOperationDiagnostic, diagnosticLines} from '../../src/diagnostics' import {collectAllPluginOutputs} from '../../src/plugins/plugin-core' import { buildComparisonKeys, @@ -231,8 +231,9 @@ async function collectCleanupTargets( return target.kind === 'file' ? 'direct' : 'recursive' } - for (const rule of collectProtectedInputSourceRules(cleanCtx.collectedOutputContext)) - { addProtectRule(rule.path, rule.protectionMode, rule.reason, rule.source) } + for (const rule of collectProtectedInputSourceRules(cleanCtx.collectedOutputContext)) { + addProtectRule(rule.path, rule.protectionMode, rule.reason, rule.source) + } if (cleanCtx.collectedOutputContext.aindexDir != null && cleanCtx.pluginOptions != null) { for (const rule of collectConfiguredAindexInputRules(cleanCtx.pluginOptions as Required, cleanCtx.collectedOutputContext.aindexDir, { workspaceDir: cleanCtx.collectedOutputContext.workspace.directory.path diff --git a/cli/test/native-binding/desk-paths.ts b/cli/test/native-binding/desk-paths.ts index 289cf5d6..243ac6bf 100644 --- a/cli/test/native-binding/desk-paths.ts +++ b/cli/test/native-binding/desk-paths.ts @@ -3,8 +3,8 @@ import type {LoggerDiagnosticInput} from '../../src/plugins/plugin-core' import * as fs from 'node:fs' import path from 'node:path' import process from 'node:process' -import {buildFileOperationDiagnostic} from '@/diagnostics' -import {resolveRuntimeEnvironment, resolveUserPath} from '@/runtime-environment' +import {buildFileOperationDiagnostic} from '../../src/diagnostics' +import {resolveRuntimeEnvironment, resolveUserPath} from '../../src/runtime-environment' type PlatformFixedDir = 'win32' | 'darwin' | 'linux' @@ -41,8 +41,7 @@ export function deletePathSync(p: string): void { if (stat.isSymbolicLink()) { if (process.platform === 'win32') fs.rmSync(p, {recursive: true, force: true}) else fs.unlinkSync(p) - } - else if (stat.isDirectory()) fs.rmSync(p, {recursive: true, force: true}) + } else if (stat.isDirectory()) fs.rmSync(p, {recursive: true, force: true}) else fs.unlinkSync(p) } @@ -56,8 +55,7 @@ export function writeFileSync(filePath: string, data: string | Buffer, encoding: export function readFileSync(filePath: string, encoding: BufferEncoding = 'utf8'): string { try { return fs.readFileSync(filePath, encoding) - } - catch (error) { + } catch (error) { const msg = error instanceof Error ? error.message : String(error) throw new Error(`Failed to read file "${filePath}": ${msg}`) } @@ -98,8 +96,7 @@ async function deletePath(p: string): Promise { await fs.promises.unlink(p) return true - } - catch (error) { + } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false throw error } @@ -109,19 +106,14 @@ async function deleteEmptyDirectory(p: string): Promise { try { await fs.promises.rmdir(p) return true - } - catch (error) { + } catch (error) { const {code} = error as NodeJS.ErrnoException if (code === 'ENOENT' || code === 'ENOTEMPTY') return false throw error } } -async function mapWithConcurrencyLimit( - items: readonly T[], - concurrency: number, - worker: (item: T) => Promise -): Promise { +async function mapWithConcurrencyLimit(items: readonly T[], concurrency: number, worker: (item: T) => Promise): Promise { if (items.length === 0) return [] const results: TResult[] = [] @@ -147,20 +139,14 @@ async function mapWithConcurrencyLimit( return results } -async function deletePaths( - paths: readonly string[], - options?: {readonly sortByDepthDescending?: boolean} -): Promise { - const sortedPaths = options?.sortByDepthDescending === true - ? [...paths].sort((a, b) => b.length - a.length || b.localeCompare(a)) - : [...paths] +async function deletePaths(paths: readonly string[], options?: {readonly sortByDepthDescending?: boolean}): Promise { + const sortedPaths = options?.sortByDepthDescending === true ? [...paths].sort((a, b) => b.length - a.length || b.localeCompare(a)) : [...paths] const results = await mapWithConcurrencyLimit(sortedPaths, DELETE_CONCURRENCY, async currentPath => { try { const deleted = await deletePath(currentPath) return {path: currentPath, deleted} - } - catch (error) { + } catch (error) { return {path: currentPath, error} } }) @@ -201,8 +187,7 @@ export async function deleteEmptyDirectories(dirs: readonly string[]): Promise { - const [fileResult, dirResult] = await Promise.all([ - deleteFiles(targets.files ?? []), - deleteDirectories(targets.dirs ?? []) - ]) +export async function deleteTargets(targets: {readonly files?: readonly string[], readonly dirs?: readonly string[]}): Promise { + const [fileResult, dirResult] = await Promise.all([deleteFiles(targets.files ?? []), deleteDirectories(targets.dirs ?? [])]) return { deletedFiles: fileResult.deletedPaths, @@ -264,21 +243,22 @@ export function writeFileSafe(options: SafeWriteOptions): SafeWriteResult { writeFileSync(fullPath, content) logger.trace({action: 'write', type, path: fullPath}) return {path: relativePath, success: true} - } - catch (error) { + } catch (error) { const errMsg = error instanceof Error ? error.message : String(error) - logger.error(buildFileOperationDiagnostic({ - code: 'OUTPUT_FILE_WRITE_FAILED', - title: `Failed to write ${type} output`, - operation: 'write', - targetKind: `${type} output file`, - path: fullPath, - error: errMsg, - details: { - relativePath, - type - } - })) + logger.error( + buildFileOperationDiagnostic({ + code: 'OUTPUT_FILE_WRITE_FAILED', + title: `Failed to write ${type} output`, + operation: 'write', + targetKind: `${type} output file`, + path: fullPath, + error: errMsg, + details: { + relativePath, + type + } + }) + ) return {path: relativePath, success: false, error: error as Error} } } From 932223c0114171bfb4cb1365c7084cd958039a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 30 Mar 2026 10:44:39 +0800 Subject: [PATCH 7/7] fix: make native binding loading lazy to fix MCP test - Convert eager native binding loading to lazy loading in filters.ts - This allows test setup file to execute before binding is required - Add VITEST and NODE_ENV to MCP test subprocess environment --- cli/src/plugins/plugin-core/filters.ts | 83 +++++++++++--------------- mcp/src/server.test.ts | 41 +++++-------- mcp/test/setup-native-binding.ts | 42 +++++++++++++ mcp/tsconfig.json | 24 ++------ mcp/tsconfig.lib.json | 7 +-- 5 files changed, 99 insertions(+), 98 deletions(-) create mode 100644 mcp/test/setup-native-binding.ts diff --git a/cli/src/plugins/plugin-core/filters.ts b/cli/src/plugins/plugin-core/filters.ts index dbbf0f7e..0cec8122 100644 --- a/cli/src/plugins/plugin-core/filters.ts +++ b/cli/src/plugins/plugin-core/filters.ts @@ -1,45 +1,51 @@ -import type { - ProjectConfig, - RulePrompt, - SeriName -} from './types' +import type {ProjectConfig, RulePrompt, SeriName} from './types' import * as fs from 'node:fs' import * as path from 'node:path' import {getNativeBinding} from '@/core/native-binding' interface SeriesFilterFns { - readonly resolveEffectiveIncludeSeries: ( - topLevel?: readonly string[], - typeSpecific?: readonly string[] - ) => string[] - readonly matchesSeries: ( - seriName: string | readonly string[] | null | undefined, - effectiveIncludeSeries: readonly string[] - ) => boolean + readonly resolveEffectiveIncludeSeries: (topLevel?: readonly string[], typeSpecific?: readonly string[]) => string[] + readonly matchesSeries: (seriName: string | readonly string[] | null | undefined, effectiveIncludeSeries: readonly string[]) => boolean readonly resolveSubSeries: ( topLevel?: Readonly>, typeSpecific?: Readonly> ) => Record } -function requireSeriesFilterFns(): SeriesFilterFns { +let seriesFilterFnsCache: SeriesFilterFns | undefined + +function getSeriesFilterFns(): SeriesFilterFns { + if (seriesFilterFnsCache != null) return seriesFilterFnsCache + const candidate = getNativeBinding() if (candidate == null) { throw new TypeError('Native series-filter binding is required. Build or install the Rust NAPI package before running tnmsc.') } - if (typeof candidate.matchesSeries !== 'function' + if ( + typeof candidate.matchesSeries !== 'function' || typeof candidate.resolveEffectiveIncludeSeries !== 'function' - || typeof candidate.resolveSubSeries !== 'function') { + || typeof candidate.resolveSubSeries !== 'function' + ) { throw new TypeError('Native series-filter binding is incomplete. Rebuild the Rust NAPI package before running tnmsc.') } + seriesFilterFnsCache = candidate return candidate } -const { - resolveEffectiveIncludeSeries, - matchesSeries, - resolveSubSeries -}: SeriesFilterFns = requireSeriesFilterFns() +function resolveEffectiveIncludeSeries(topLevel?: readonly string[], typeSpecific?: readonly string[]): string[] { + return getSeriesFilterFns().resolveEffectiveIncludeSeries(topLevel, typeSpecific) +} + +function matchesSeries(seriName: string | readonly string[] | null | undefined, effectiveIncludeSeries: readonly string[]): boolean { + return getSeriesFilterFns().matchesSeries(seriName, effectiveIncludeSeries) +} + +function resolveSubSeries( + topLevel?: Readonly>, + typeSpecific?: Readonly> +): Record { + return getSeriesFilterFns().resolveSubSeries(topLevel, typeSpecific) +} /** * Interface for items that can be filtered by series name @@ -58,10 +64,7 @@ export function filterByProjectConfig( projectConfig: ProjectConfig | undefined, configPath: FilterConfigPath ): readonly T[] { - const effectiveSeries = resolveEffectiveIncludeSeries( - projectConfig?.includeSeries, - projectConfig?.[configPath]?.includeSeries - ) + const effectiveSeries = resolveEffectiveIncludeSeries(projectConfig?.includeSeries, projectConfig?.[configPath]?.includeSeries) return items.filter(item => matchesSeries(item.seriName, effectiveSeries)) } @@ -77,10 +80,7 @@ function smartConcatGlob(prefix: string, glob: string): string { return `${prefix}/${glob}` } -function extractPrefixAndBaseGlob( - glob: string, - prefixes: readonly string[] -): {prefix: string | null, baseGlob: string} { +function extractPrefixAndBaseGlob(glob: string, prefixes: readonly string[]): {prefix: string | null, baseGlob: string} { for (const prefix of prefixes) { const normalizedPrefix = prefix.replaceAll(/\/+$/g, '') const patterns = [ @@ -95,10 +95,7 @@ function extractPrefixAndBaseGlob( return {prefix: null, baseGlob: glob} } -export function applySubSeriesGlobPrefix( - rules: readonly RulePrompt[], - projectConfig: ProjectConfig | undefined -): readonly RulePrompt[] { +export function applySubSeriesGlobPrefix(rules: readonly RulePrompt[], projectConfig: ProjectConfig | undefined): readonly RulePrompt[] { const subSeries = resolveSubSeries(projectConfig?.subSeries, projectConfig?.rules?.subSeries) if (Object.keys(subSeries).length === 0) return rules @@ -115,9 +112,7 @@ export function applySubSeriesGlobPrefix( const matchedPrefixes: string[] = [] for (const [subdir, seriNames] of Object.entries(normalizedSubSeries)) { - const matched = Array.isArray(rule.seriName) - ? rule.seriName.some(name => seriNames.includes(name)) - : seriNames.includes(rule.seriName) + const matched = Array.isArray(rule.seriName) ? rule.seriName.some(name => seriNames.includes(name)) : seriNames.includes(rule.seriName) if (matched) matchedPrefixes.push(subdir) } @@ -168,9 +163,7 @@ export function resolveGitInfoDir(projectDir: string): string | null { const gitdir = path.resolve(projectDir, match[1]) return path.join(gitdir, 'info') } - } - catch { - } // ignore read errors + } catch {} // ignore read errors } return null @@ -193,8 +186,7 @@ export function findAllGitRepos(rootDir: string, maxDepth = 5): string[] { const raw = fs.readdirSync(dir, {withFileTypes: true}) if (!Array.isArray(raw)) return entries = raw - } - catch { + } catch { return } @@ -229,8 +221,7 @@ export function findGitModuleInfoDirs(dotGitDir: string): string[] { const raw = fs.readdirSync(dir, {withFileTypes: true}) if (!Array.isArray(raw)) return entries = raw - } - catch { + } catch { return } @@ -245,8 +236,7 @@ export function findGitModuleInfoDirs(dotGitDir: string): string[] { const raw = fs.readdirSync(path.join(dir, 'modules'), {withFileTypes: true}) if (!Array.isArray(raw)) return subEntries = raw - } - catch { + } catch { return } for (const sub of subEntries) { @@ -259,8 +249,7 @@ export function findGitModuleInfoDirs(dotGitDir: string): string[] { const raw = fs.readdirSync(modulesDir, {withFileTypes: true}) if (!Array.isArray(raw)) return results topEntries = raw - } - catch { + } catch { return results } diff --git a/mcp/src/server.test.ts b/mcp/src/server.test.ts index 451568ef..e66e8d4d 100644 --- a/mcp/src/server.test.ts +++ b/mcp/src/server.test.ts @@ -12,6 +12,7 @@ const tempDirs: string[] = [] const serverMainPath = fileURLToPath(new URL('./main.ts', import.meta.url)) const tsxPackageJsonPath = fileURLToPath(new URL('../node_modules/tsx/package.json', import.meta.url)) const tsxCliPath = path.join(path.dirname(tsxPackageJsonPath), 'dist', 'cli.mjs') +const cliNativeBindingSetupPath = fileURLToPath(new URL('../test/setup-native-binding.ts', import.meta.url)) function createTempDir(prefix: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)) @@ -20,8 +21,7 @@ function createTempDir(prefix: string): string { } function createTransportEnv(homeDir: string): Record { - const envEntries = Object.entries(process.env) - .filter((entry): entry is [string, string] => typeof entry[1] === 'string') + const envEntries = Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === 'string') return { ...Object.fromEntries(envEntries), @@ -30,7 +30,9 @@ function createTransportEnv(homeDir: string): Record { XDG_CACHE_HOME: path.join(homeDir, '.cache'), XDG_CONFIG_HOME: path.join(homeDir, '.config'), XDG_DATA_HOME: path.join(homeDir, '.local', 'share'), - XDG_STATE_HOME: path.join(homeDir, '.local', 'state') + XDG_STATE_HOME: path.join(homeDir, '.local', 'state'), + VITEST: 'true', + NODE_ENV: 'test' } } @@ -40,17 +42,13 @@ interface TextContentBlock { } function getTextBlock(result: unknown): string { - if ( - typeof result !== 'object' - || result == null - || !('content' in result) - || !Array.isArray(result.content) - ) { + if (typeof result !== 'object' || result == null || !('content' in result) || !Array.isArray(result.content)) { throw new Error('Expected content blocks in MCP result') } - const textBlock = result.content - .find((block): block is TextContentBlock => typeof block === 'object' && block != null && 'type' in block && (block as {type?: unknown}).type === 'text') + const textBlock = result.content.find( + (block): block is TextContentBlock => typeof block === 'object' && block != null && 'type' in block && (block as {type?: unknown}).type === 'text' + ) if (textBlock?.text == null) throw new Error('Expected a text content block in MCP result') return textBlock.text } @@ -73,28 +71,21 @@ describe('memory-sync MCP stdio server', () => { }) const transport = new StdioClientTransport({ command: process.execPath, - args: [tsxCliPath, serverMainPath], + args: [tsxCliPath, '--import', cliNativeBindingSetupPath, serverMainPath], cwd: workspaceDir, env: createTransportEnv(homeDir), stderr: 'pipe' }) let stderrOutput = '' - transport.stderr?.on('data', (chunk: Buffer | string) => stderrOutput += String(chunk)) + transport.stderr?.on('data', (chunk: Buffer | string) => (stderrOutput += String(chunk))) try { await client.connect(transport) const tools = await client.listTools() - expect(tools.tools.map(tool => tool.name).sort()).toEqual([ - 'apply_prompt_translation', - 'get_prompt', - 'list_prompts', - 'upsert_prompt_src' - ]) - expect( - tools.tools.find(tool => tool.name === 'apply_prompt_translation')?.inputSchema.properties - ).toMatchObject({ + expect(tools.tools.map(tool => tool.name).sort()).toEqual(['apply_prompt_translation', 'get_prompt', 'list_prompts', 'upsert_prompt_src']) + expect(tools.tools.find(tool => tool.name === 'apply_prompt_translation')?.inputSchema.properties).toMatchObject({ promptId: expect.any(Object), enContent: expect.any(Object), distContent: expect.any(Object) @@ -241,14 +232,12 @@ describe('memory-sync MCP stdio server', () => { }) const filteredPrompts = parseToolResult<{prompts: {promptId: string}[]}>(filteredListResult) expect(filteredPrompts.prompts.map(prompt => prompt.promptId)).toEqual(['command:demo/build']) - } - catch (error) { + } catch (error) { if (stderrOutput.trim().length === 0) throw error const errorMessage = error instanceof Error ? error.message : String(error) throw new Error(`${errorMessage}\nMCP stderr:\n${stderrOutput.trim()}`) - } - finally { + } finally { await client.close() } }) diff --git a/mcp/test/setup-native-binding.ts b/mcp/test/setup-native-binding.ts new file mode 100644 index 00000000..458ba3c2 --- /dev/null +++ b/mcp/test/setup-native-binding.ts @@ -0,0 +1,42 @@ +function resolveEffectiveIncludeSeries(topLevel?: readonly string[], typeSpecific?: readonly string[]): string[] { + if (topLevel == null && typeSpecific == null) return [] + return [...new Set([...topLevel ?? [], ...typeSpecific ?? []])] +} + +function matchesSeries(seriName: string | readonly string[] | null | undefined, effectiveIncludeSeries: readonly string[]): boolean { + if (seriName == null) return true + if (effectiveIncludeSeries.length === 0) return true + if (typeof seriName === 'string') return effectiveIncludeSeries.includes(seriName) + return seriName.some(name => effectiveIncludeSeries.includes(name)) +} + +function resolveSubSeries( + topLevel?: Readonly>, + typeSpecific?: Readonly> +): Record { + if (topLevel == null && typeSpecific == null) return {} + + const merged: Record = {} + for (const [key, values] of Object.entries(topLevel ?? {})) merged[key] = [...values] + + for (const [key, values] of Object.entries(typeSpecific ?? {})) { + const existingValues = merged[key] ?? [] + merged[key] = Object.hasOwn(merged, key) ? [...new Set([...existingValues, ...values])] : [...values] + } + + return merged +} + +const testGlobals = globalThis as typeof globalThis & { + __TNMSC_TEST_NATIVE_BINDING__?: { + resolveEffectiveIncludeSeries: typeof resolveEffectiveIncludeSeries + matchesSeries: typeof matchesSeries + resolveSubSeries: typeof resolveSubSeries + } +} + +testGlobals.__TNMSC_TEST_NATIVE_BINDING__ = { + resolveEffectiveIncludeSeries, + matchesSeries, + resolveSubSeries +} diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json index 9bab1cc0..defaae3d 100644 --- a/mcp/tsconfig.json +++ b/mcp/tsconfig.json @@ -5,22 +5,16 @@ "incremental": true, "composite": false, "target": "ESNext", - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "moduleDetection": "force", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Bundler", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, "resolveJsonModule": true, - "types": [ - "node" - ], + "types": ["node"], "allowImportingTsExtensions": true, "strict": true, "strictBindCallApply": true, @@ -57,14 +51,6 @@ "verbatimModuleSyntax": true, "skipLibCheck": true }, - "include": [ - "src/**/*", - "eslint.config.ts", - "tsdown.config.ts", - "vitest.config.ts" - ], - "exclude": [ - "../node_modules", - "dist" - ] + "include": ["src/**/*", "test/**/*.ts", "eslint.config.ts", "tsdown.config.ts", "vitest.config.ts"], + "exclude": ["../node_modules", "dist"] } diff --git a/mcp/tsconfig.lib.json b/mcp/tsconfig.lib.json index 12ffb0d0..4da48c3b 100644 --- a/mcp/tsconfig.lib.json +++ b/mcp/tsconfig.lib.json @@ -3,10 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": [ - "src/**/*", - "eslint.config.ts", - "tsdown.config.ts", - "vitest.config.ts" - ] + "include": ["src/**/*", "test/**/*.ts", "eslint.config.ts", "tsdown.config.ts", "vitest.config.ts"] }