Compare commits
3 Commits
clawsweepe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe87142d89 | ||
|
|
782e028abe | ||
|
|
2a9b353b21 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-15, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
@ -48,6 +48,11 @@ jobs:
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --version
|
||||
- run: pnpm check
|
||||
if: matrix.os != 'macos-15'
|
||||
|
||||
- name: Check without type-aware oxlint
|
||||
if: matrix.os == 'macos-15'
|
||||
run: pnpm format:check && pnpm typecheck
|
||||
|
||||
- name: Verify generated schema is committed
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
2
.github/workflows/crabbox-hydrate.yml
vendored
2
.github/workflows/crabbox-hydrate.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
runs-on: [self-hosted, crabbox, openclaw, mcporter, '${{ inputs.crabbox_runner_label }}']
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
|
||||
2
.github/workflows/pages.yml
vendored
2
.github/workflows/pages.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
24
package.json
24
package.json
@ -72,31 +72,31 @@
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"acorn": "^8.16.0",
|
||||
"commander": "^14.0.3",
|
||||
"es-toolkit": "^1.47.0",
|
||||
"acorn": "^8.17.0",
|
||||
"commander": "^15.0.0",
|
||||
"es-toolkit": "^1.48.1",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"ora": "^9.4.0",
|
||||
"rolldown": "1.0.1",
|
||||
"ora": "^9.4.1",
|
||||
"rolldown": "1.1.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "^1.0.9",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.9.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260514.1",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"@types/node": "^26.0.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260623.1",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"bun-types": "^1.3.14",
|
||||
"cross-env": "^10.1.0",
|
||||
"express": "^5.2.1",
|
||||
"oxfmt": "^0.49.0",
|
||||
"oxlint": "^1.69.0",
|
||||
"oxlint-tsgolint": "^0.22.1",
|
||||
"oxfmt": "^0.56.0",
|
||||
"oxlint": "^1.71.0",
|
||||
"oxlint-tsgolint": "^0.23.0",
|
||||
"rimraf": "^6.1.3",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"devEngines": {
|
||||
"runtime": [
|
||||
|
||||
954
pnpm-lock.yaml
generated
954
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -452,18 +452,24 @@ async function prepareSocket(socketPath: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function cleanupArtifacts(options: DaemonHostOptions): Promise<void> {
|
||||
await cleanupDaemonArtifactsIfOwned(options, process.pid);
|
||||
}
|
||||
|
||||
export async function cleanupDaemonArtifactsIfOwned(
|
||||
paths: Pick<DaemonHostOptions, 'metadataPath' | 'socketPath'>,
|
||||
ownerPid: number
|
||||
): Promise<void> {
|
||||
// A superseded daemon may finish shutting down after its replacement has
|
||||
// already rebound the same paths. Never let that old process unlink the
|
||||
// replacement daemon's live socket and metadata.
|
||||
const metadata = await readJsonFile<{ pid?: number; socketPath?: string }>(paths.metadataPath).catch(() => undefined);
|
||||
if (metadata?.pid !== ownerPid || metadata.socketPath !== paths.socketPath) {
|
||||
return;
|
||||
}
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
await fs.unlink(options.socketPath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.unlink(options.metadataPath);
|
||||
} catch {
|
||||
// ignore
|
||||
await fs.unlink(paths.socketPath).catch(() => {});
|
||||
}
|
||||
await fs.unlink(paths.metadataPath).catch(() => {});
|
||||
}
|
||||
|
||||
async function handleSocketRequest(
|
||||
|
||||
@ -7,20 +7,45 @@ import { metadataPathForArtifact, readCliMetadata } from '../src/cli-metadata.js
|
||||
describe('readCliMetadata', () => {
|
||||
it('prefers embedded metadata over stale sidecar metadata', async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-metadata-'));
|
||||
const artifact = path.join(tempDir, 'artifact');
|
||||
const artifact = path.join(tempDir, process.platform === 'win32' ? 'artifact.exe' : 'artifact');
|
||||
const embedded = metadataPayload('embedded');
|
||||
const sidecar = metadataPayload('sidecar');
|
||||
await fs.writeFile(
|
||||
artifact,
|
||||
`#!/usr/bin/env node\nconsole.log(${JSON.stringify(JSON.stringify(embedded))});\n`,
|
||||
'utf8'
|
||||
);
|
||||
await fs.chmod(artifact, 0o755);
|
||||
const previousEmbeddedMetadata = process.env.MCPORTER_TEST_EMBEDDED_METADATA;
|
||||
const previousNodeOptions = process.env.NODE_OPTIONS;
|
||||
process.env.MCPORTER_TEST_EMBEDDED_METADATA = JSON.stringify(embedded);
|
||||
if (process.platform === 'win32') {
|
||||
const preload = path.join(tempDir, 'inspect-preload.cjs');
|
||||
await fs.copyFile(process.execPath, artifact);
|
||||
await fs.writeFile(
|
||||
preload,
|
||||
'console.log(process.env.MCPORTER_TEST_EMBEDDED_METADATA); process.exit(0);\n',
|
||||
'utf8'
|
||||
);
|
||||
const requirePath = preload.replaceAll(path.sep, path.posix.sep);
|
||||
process.env.NODE_OPTIONS = `${previousNodeOptions ? `${previousNodeOptions} ` : ''}--require ${requirePath}`;
|
||||
} else {
|
||||
const artifactContent = '#!/usr/bin/env node\nconsole.log(process.env.MCPORTER_TEST_EMBEDDED_METADATA);\n';
|
||||
await fs.writeFile(artifact, artifactContent, 'utf8');
|
||||
await fs.chmod(artifact, 0o755);
|
||||
}
|
||||
await fs.writeFile(metadataPathForArtifact(artifact), JSON.stringify(sidecar), 'utf8');
|
||||
|
||||
await expect(readCliMetadata(artifact)).resolves.toMatchObject({
|
||||
server: { name: 'embedded' },
|
||||
});
|
||||
try {
|
||||
await expect(readCliMetadata(artifact)).resolves.toMatchObject({
|
||||
server: { name: 'embedded' },
|
||||
});
|
||||
} finally {
|
||||
if (previousEmbeddedMetadata === undefined) {
|
||||
delete process.env.MCPORTER_TEST_EMBEDDED_METADATA;
|
||||
} else {
|
||||
process.env.MCPORTER_TEST_EMBEDDED_METADATA = previousEmbeddedMetadata;
|
||||
}
|
||||
if (previousNodeOptions === undefined) {
|
||||
delete process.env.NODE_OPTIONS;
|
||||
} else {
|
||||
process.env.NODE_OPTIONS = previousNodeOptions;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -5,7 +5,12 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ServerDefinition } from '../src/config.js';
|
||||
import { __testProcessRequest, isDaemonResponding, metadataMatches } from '../src/daemon/host.js';
|
||||
import {
|
||||
__testProcessRequest,
|
||||
cleanupDaemonArtifactsIfOwned,
|
||||
isDaemonResponding,
|
||||
metadataMatches,
|
||||
} from '../src/daemon/host.js';
|
||||
import type { DaemonRequest } from '../src/daemon/protocol.js';
|
||||
import type { Runtime } from '../src/runtime.js';
|
||||
|
||||
@ -246,6 +251,41 @@ describe('metadataMatches', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('daemon artifact cleanup', () => {
|
||||
let dir: string;
|
||||
let metadataPath: string;
|
||||
let socketPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-cleanup-'));
|
||||
metadataPath = path.join(dir, 'daemon.json');
|
||||
socketPath = path.join(dir, 'daemon.sock');
|
||||
await fs.writeFile(socketPath, 'socket', 'utf8');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('removes artifacts still owned by the stopping daemon', async () => {
|
||||
await fs.writeFile(metadataPath, JSON.stringify({ pid: 4321, socketPath }), 'utf8');
|
||||
|
||||
await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321);
|
||||
|
||||
await expect(fs.access(metadataPath)).rejects.toThrow();
|
||||
await expect(fs.access(socketPath)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('preserves artifacts replaced by a newer daemon', async () => {
|
||||
await fs.writeFile(metadataPath, JSON.stringify({ pid: 9876, socketPath }), 'utf8');
|
||||
|
||||
await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321);
|
||||
|
||||
await expect(fs.access(metadataPath)).resolves.toBeUndefined();
|
||||
await expect(fs.access(socketPath)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function createRuntimeDouble(): Pick<Runtime, 'callTool' | 'listTools'> {
|
||||
return {
|
||||
callTool: vi.fn().mockResolvedValue({ ok: true }),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user