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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Skill package + CLI (init/keygen/print-config/status)
run: npm run verify:mcp:skill

- name: Publish (npm pack + tarball smoke, D41)
run: npm run verify:mcp:publish

semantic-real:
name: semantic recall (real model)
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ docs/
design-notes/
docs.design/

# npm pack 發布期備份(transient)
packages/*/package.json.packbak
*.tgz

# 示例執行時產物(本地留存)
examples/quickstart/.data/

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"verify:mcp:stdio": "node scripts/verify-mcp-stdio.mjs",
"verify:mcp:http": "node scripts/verify-mcp-http.mjs",
"verify:mcp:skill": "node scripts/verify-mcp-skill.mjs",
"verify:mcp:publish": "node scripts/verify-mcp-publish.mjs",
"dev": "node --loader ts-node/esm src/index.ts"
},
"dependencies": {
Expand Down
7 changes: 6 additions & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
},
"files": [
"bin",
"src"
"src",
"scripts"
],
"scripts": {
"prepack": "node scripts/publish-manifest.mjs set",
"postpack": "node scripts/publish-manifest.mjs restore"
},
"engines": {
"node": ">=20"
},
Expand Down
41 changes: 41 additions & 0 deletions packages/mcp/scripts/publish-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node
// G6.5 / D41:發布期改寫 @mebular/core 依賴。
//
// 開發期以 `file:../..` 本地連結(根包非 workspace 成員、本環境 npm 不支援 `workspace:`);
// `npm pack`/`npm publish` 的 prepack 階段改為 `^0.1.0`,postpack 還原。
//
// 用法:node scripts/publish-manifest.mjs set|restore

import { existsSync } from 'node:fs';
import { readFile, rename, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const pkgDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const pkgFile = join(pkgDir, 'package.json');
const backupFile = `${pkgFile}.packbak`;
const PUBLISH_RANGE = '^0.1.0';

const action = process.argv[2];

async function writeJson(file, data) {
await writeFile(file, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
}

if (action === 'set') {
const original = await readFile(pkgFile, 'utf-8');
if (!existsSync(backupFile)) await writeFile(backupFile, original, 'utf-8');
const pkg = JSON.parse(original);
pkg.dependencies = pkg.dependencies ?? {};
pkg.dependencies['@mebular/core'] = PUBLISH_RANGE;
await writeJson(pkgFile, pkg);
console.error(`[publish-manifest] @mebular/core -> ${PUBLISH_RANGE}(備份 ${backupFile})`);
} else if (action === 'restore') {
if (existsSync(backupFile)) {
await rename(backupFile, pkgFile);
console.error('[publish-manifest] 已還原本地 file: 依賴');
}
} else {
console.error('用法:node scripts/publish-manifest.mjs set|restore');
process.exit(2);
}
132 changes: 132 additions & 0 deletions scripts/verify-mcp-publish.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env node
// G6.5 發布驗證(E1)
//
// 1) 對三個套件 npm pack;檢驗 tarball 內容與 @mebular/mcp 內 @mebular/core 已由
// prepack 由 `file:../..` 改寫為 `^0.1.0`(D41),且 pack 後本地依賴已還原。
// 2) 用產出的 tarball 在乾淨目錄安裝;以安裝後的 `mebular status`、`mebular mcp`
// (真實 MCP client)與 `@mebular/skill/scripts/install.mjs` 做冒煙。
// 乾淨環境退出碼 0。前置:npm run build。

import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const mcpDir = join(rootDir, 'packages', 'mcp');
const skillDir = join(rootDir, 'packages', 'skill');
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';

let passed = true;
const check = (label, ok, detail = '') => {
console.log(` ${ok ? '✓' : '✗'} ${label}${detail ? `(${detail})` : ''}`);
if (!ok) passed = false;
};

const pack = (cwd, dest) => {
const out = execFileSync(npm, ['pack', '--json', '--pack-destination', dest], {
cwd,
encoding: 'utf-8',
env: process.env,
});
const meta = JSON.parse(out)[0];
return { file: join(dest, meta.filename), meta };
};
const listTarball = (tgz) => execFileSync('tar', ['-tzf', tgz], { encoding: 'utf-8' }).split('\n').filter(Boolean);
const readTarballFile = (tgz, inner) => execFileSync('tar', ['-xzf', tgz, '-O', inner], { encoding: 'utf-8' });

console.log('Mebular G6.5 發布驗證(npm pack + tarball 冒煙)');
console.log('===============================================');

if (!existsSync(join(rootDir, 'dist', 'index.js'))) {
console.log(' · dist 不存在,先 build');
execFileSync(npm, ['run', 'build'], { cwd: rootDir, stdio: 'inherit' });
}

const work = await mkdtemp(join(tmpdir(), 'mebular-publish-'));
const app = join(work, 'app');
const home = join(app, 'home');
let client = null;

try {
// ---------- npm pack ----------
const core = pack(rootDir, work);
const mcp = pack(mcpDir, work);
const skill = pack(skillDir, work);
check('三包 npm pack', true, [core.meta.name, mcp.meta.name, skill.meta.name].join(' / '));

// D41:mcp tarball 內為 ^0.1.0,pack 後本地 manifest 已還原
const mcpManifest = JSON.parse(readTarballFile(mcp.file, 'package/package.json'));
check('mcp tarball 依賴 @mebular/core=^0.1.0', mcpManifest.dependencies?.['@mebular/core'] === '^0.1.0', mcpManifest.dependencies?.['@mebular/core']);
const localManifest = JSON.parse(await readFile(join(mcpDir, 'package.json'), 'utf-8'));
check('pack 後本地依賴還原為 file:../..', localManifest.dependencies?.['@mebular/core'] === 'file:../..', localManifest.dependencies?.['@mebular/core']);
check('pack 備份已清除', !existsSync(join(mcpDir, 'package.json.packbak')));

// tarball 內容
const coreFiles = listTarball(core.file);
const mcpFiles = listTarball(mcp.file);
const skillFiles = listTarball(skill.file);
check('core tarball 含 dist/index.js', coreFiles.includes('package/dist/index.js'));
check('core tarball 含 package.json', coreFiles.includes('package/package.json'));
check('mcp tarball 含 bin/src', mcpFiles.includes('package/bin/mebular.mjs') && mcpFiles.includes('package/src/server.mjs'));
check(
'skill tarball 含 SKILL/MEMORY_POLICY/install/mcp',
['package/SKILL.md', 'package/MEMORY_POLICY.md', 'package/scripts/install.mjs', 'package/mcp/opencode.json'].every((f) => skillFiles.includes(f)),
);

// ---------- tarball 安裝冒煙 ----------
await writeFile(join(work, 'package.json'), '{"name":"mebular-publish-smoke","private":true,"version":"0.0.0"}\n', 'utf-8');
execFileSync(npm, ['install', core.file, mcp.file, skill.file, '--omit=optional', '--no-audit', '--no-fund', '--loglevel=error'], {
cwd: work,
stdio: ['ignore', 'ignore', 'inherit'],
env: process.env,
});
const installedCore = JSON.parse(await readFile(join(work, 'node_modules', '@mebular', 'core', 'package.json'), 'utf-8'));
check('tarball 安裝:@mebular/core@0.1.0', installedCore.version === '0.1.0', installedCore.version);
check('tarball 安裝:@mebular/mcp 與 @mebular/skill', existsSync(join(work, 'node_modules', '@mebular', 'mcp')) && existsSync(join(work, 'node_modules', '@mebular', 'skill')));

const bin = join(work, 'node_modules', '.bin', 'mebular');
const env = { ...process.env, MEBULAR_HOME: home, MEBULAR_STORAGE_PATH: join(home, 'store.jsonl'), MEBULAR_DEVICE_ID: 'device-publish-smoke' };

const statusOut = JSON.parse(execFileSync(process.execPath, [bin, 'status'], { cwd: work, encoding: 'utf-8', env }));
check('安裝後 mebular status', /^[0-9a-f]{64}$/.test(statusOut.stateHash ?? ''), `deviceId=${statusOut.deviceId}`);

// 安裝後的 stdio MCP 冒煙(真實 MCP client)
client = new Client({ name: 'mebular-publish-smoke', version: '0.1.0' });
const transport = new StdioClientTransport({ command: process.execPath, args: [bin, 'mcp'], env, stderr: 'pipe' });
await client.connect(transport);
const { tools } = await client.listTools();
check('安裝後 mebular mcp:tools/list=11', tools.length === 11, `count=${tools.length}`);
const written = await client.callTool({ name: 'memory_write', arguments: { items: [{ type: 'fact', content: 'publish-smoke' }] } });
const parsed = written.structuredContent ?? JSON.parse(written.content?.find((c) => c.type === 'text')?.text ?? '{}');
check('安裝後 memory_write 落圖', Array.isArray(parsed?.stored) && parsed.stored.length === 1);
const queried = await client.callTool({ name: 'memory_query', arguments: { query: 'publish-smoke' } });
const q = queried.structuredContent ?? JSON.parse(queried.content?.find((c) => c.type === 'text')?.text ?? '{}');
check('安裝後 memory_query 命中', (q?.totalMatches ?? 0) >= 1, `totalMatches=${q?.totalMatches}`);
await client.close();
client = null;

// 安裝後的 skill 安裝器
const skillTarget = join(work, 'skills');
execFileSync(process.execPath, [join(work, 'node_modules', '@mebular', 'skill', 'scripts', 'install.mjs'), '--target', skillTarget], { cwd: work, encoding: 'utf-8', env });
check('安裝後 skill install.mjs', existsSync(join(skillTarget, 'mebular-memory', 'SKILL.md')));
} catch (error) {
check('G6.5 發布驗證', false, String(error?.stderr ?? error?.message ?? error).substring(0, 500));
} finally {
if (client) await client.close().catch(() => undefined);
await rm(work, { recursive: true, force: true }).catch(() => undefined);
}

console.log('===============================================');
if (passed) {
console.log('✓ G6.5 發布驗證通過(E1)');
process.exit(0);
} else {
console.log('✗ G6.5 發布驗證失敗');
process.exit(1);
}
Loading