From 5f0dd92af91c4612c01933276cf892172387beea Mon Sep 17 00:00:00 2001 From: Kyle McCullough Date: Fri, 10 Jul 2026 05:16:21 -0300 Subject: [PATCH 1/2] Bundle atomic Business session persistence --- adapters/business/run.js | 55 +++++++++----- scripts/test-pack.mjs | 17 +++++ test/business-adapter.test.mjs | 132 +++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 test/business-adapter.test.mjs diff --git a/adapters/business/run.js b/adapters/business/run.js index ee4b182..c285280 100755 --- a/adapters/business/run.js +++ b/adapters/business/run.js @@ -140,6 +140,8 @@ var CLI_AUTH_ERROR_QUERY_PARAM = "error"; var CLI_AUTH_ERROR_DESCRIPTION_QUERY_PARAM = "error_description"; // ../cli-auth/src/session.ts +import { randomUUID } from "node:crypto"; +import { mkdir, open, readFile, rename, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; function stateHome(env2) { @@ -160,6 +162,28 @@ function resolveCliPaths(appName, env2 = process.env) { sessionFile: path.join(stateDir, "session.json") }; } +async function writeCliSessionFile(sessionFile, session) { + const stateDir = path.dirname(sessionFile); + const tempFile = path.join( + stateDir, + `.${path.basename(sessionFile)}.${process.pid}.${randomUUID()}.tmp` + ); + let handle; + await mkdir(stateDir, { recursive: true }); + try { + handle = await open(tempFile, "wx", 384); + await handle.writeFile(`${JSON.stringify(session, null, 2)} +`, "utf8"); + await handle.sync(); + await handle.close(); + handle = void 0; + await rename(tempFile, sessionFile); + } catch (error) { + await handle?.close().catch(() => void 0); + await rm(tempFile, { force: true }).catch(() => void 0); + throw error; + } +} function workspaceBaseUrl(workspace) { if (/^https?:\/\//i.test(workspace.host)) { return workspace.host.replace(/\/$/, ""); @@ -362,7 +386,7 @@ async function logoutRemoteSession(input2) { } // src/store.ts -import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { readFile as readFile2, rm as rm2 } from "node:fs/promises"; var DEFAULT_CONSOLE_URL = process.env.MERE_BUSINESS_BASE_URL?.trim() || process.env.ZEROSMB_CONSOLE_URL?.trim() || "https://mere.business"; var PRIMARY_APP_NAME = "mere-business"; var LEGACY_APP_NAME = "zerosmb"; @@ -410,7 +434,7 @@ function coerceLocalSession(value) { } async function readSessionFile(sessionFile) { try { - const raw = await readFile(sessionFile, "utf8"); + const raw = await readFile2(sessionFile, "utf8"); return coerceLocalSession(JSON.parse(raw)); } catch (error) { if (error.code === "ENOENT") { @@ -431,13 +455,10 @@ async function loadSession(paths = resolveCliPaths2()) { return readSessionFile(legacyPaths.sessionFile); } async function saveSession(session, paths = resolveCliPaths2()) { - await mkdir(paths.stateDir, { recursive: true }); - await writeFile(paths.sessionFile, `${JSON.stringify(session, null, 2)} -`, "utf8"); - await chmod(paths.sessionFile, 384).catch(() => void 0); + await writeCliSessionFile(paths.sessionFile, session); } async function clearSession(paths = resolveCliPaths2()) { - await rm(paths.sessionFile, { force: true }); + await rm2(paths.sessionFile, { force: true }); } function workspaceBaseUrl2(workspace) { return workspaceBaseUrl(workspace); @@ -572,7 +593,7 @@ async function refreshRemoteSession2(session, options = {}) { } // src/http.ts -import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "node:fs/promises"; +import { mkdir as mkdir2, stat, writeFile } from "node:fs/promises"; import { basename, dirname, join, resolve as resolvePath, sep as pathSeparator } from "node:path"; // ../shared/contracts/cli.ts @@ -1003,7 +1024,7 @@ async function downloadWorkspaceResource(workspace, accessToken, pathname, optio ); const targetPath = await resolveDownloadTarget(options.outputPath, filename); await mkdir2(dirname(targetPath), { recursive: true }); - await writeFile2(targetPath, bytes); + await writeFile(targetPath, bytes); return { path: targetPath, filename, @@ -1130,12 +1151,12 @@ async function openBusinessWaitlist(input2) { } // src/commands.ts -import { readdir, readFile as readFile2, stat as stat2 } from "node:fs/promises"; +import { readdir, readFile as readFile3, stat as stat2 } from "node:fs/promises"; import { basename as basename2, join as join2, relative, resolve as resolvePath2 } from "node:path"; import { parseArgs } from "node:util"; // ../local-plane/src/index.ts -import { createHash, randomUUID } from "node:crypto"; +import { createHash, randomUUID as randomUUID2 } from "node:crypto"; import { mkdir as mkdir3 } from "node:fs/promises"; import os2 from "node:os"; import path2 from "node:path"; @@ -1168,7 +1189,7 @@ function json(value) { return JSON.stringify(value ?? {}); } function makePlaneId(prefix) { - return `${prefix}_${randomUUID().replaceAll("-", "").slice(0, 24)}`; + return `${prefix}_${randomUUID2().replaceAll("-", "").slice(0, 24)}`; } async function loadNodeSqlite() { return import(["node", "sqlite"].join(":")); @@ -5944,7 +5965,7 @@ async function parseJsonObjectFile(filePath, optionName) { if (!filePath) return void 0; let text; try { - text = await readFile2(resolvePath2(filePath), "utf8"); + text = await readFile3(resolvePath2(filePath), "utf8"); } catch (error) { throw usageError( `Option --${optionName} could not be read: ${error instanceof Error ? error.message : "unknown error"}.` @@ -5990,7 +6011,7 @@ function safeBundlePath(pathname) { return parts.join("/"); } async function localFilePayload(filePath) { - const data = await readFile2(filePath); + const data = await readFile3(filePath); const filename = basename2(filePath); return { filename, @@ -6006,7 +6027,7 @@ async function staticBundlePayload(input2) { const title = input2.title; const entryPath = input2.entryPath ?? "index.html"; if (input2.zip) { - const data = await readFile2(input2.zip); + const data = await readFile3(input2.zip); return { title, entryPath, @@ -6030,7 +6051,7 @@ async function staticBundlePayload(input2) { } if (!entry.isFile()) continue; const path4 = safeBundlePath(relative(root, absolute)); - const data = await readFile2(absolute); + const data = await readFile3(absolute); files.push({ path: path4, contentType: inferContentType(path4), @@ -6652,7 +6673,7 @@ next: ${payload.nextUrl}` : ""}`; if (!localInput.file && !localInput.eventJson) { throw usageError("Pass --file or --event-json."); } - const raw = localInput.file ? await readFile2(localInput.file, "utf8") : localInput.eventJson ?? ""; + const raw = localInput.file ? await readFile3(localInput.file, "utf8") : localInput.eventJson ?? ""; const envelope = parseJsonText(raw, localInput.file ? `file ${localInput.file}` : "event-json"); return withLocalBusinessPlane(localInput.localDbPath, ({ dbPath, db }) => { const result = recordLocalProjectionEnvelope(db, { diff --git a/scripts/test-pack.mjs b/scripts/test-pack.mjs index 4bf3bf1..08d7950 100644 --- a/scripts/test-pack.mjs +++ b/scripts/test-pack.mjs @@ -39,6 +39,8 @@ try { const bin = path.join(prefix, 'bin', 'mere'); const env = { ...process.env, + HOME: path.join(tmp, 'home'), + XDG_STATE_HOME: path.join(tmp, 'state'), MERE_CLI_SOURCE: 'bundled', MERE_ROOT: path.join(tmp, 'missing-mere-root'), MERE_CLI_BIN: bin @@ -51,6 +53,21 @@ try { throw new Error('Installed apps list did not resolve every adapter from bundled source.'); } parseJsonCommand(bin, ['apps', 'manifest', '--app', 'projects', '--json'], env); + parseJsonCommand(bin, ['apps', 'manifest', '--app', 'business', '--json'], env); + const businessNoSession = spawnSync(bin, ['business', 'workspace', 'list', '--json'], { + cwd: packageRoot, + env, + stdio: ['ignore', 'pipe', 'pipe'], + encoding: 'utf8' + }); + if (businessNoSession.error) throw businessNoSession.error; + if (businessNoSession.status !== 2) { + throw new Error(`Packed Business adapter returned ${businessNoSession.status}; expected the no-session exit status.\n${businessNoSession.stderr}${businessNoSession.stdout}`); + } + const businessError = JSON.parse(businessNoSession.stderr); + if (businessError.error?.code !== 'usage_error' || !businessError.error?.message?.includes('No local session found')) { + throw new Error(`Packed Business adapter did not preserve root-canonical leading --json handling: ${businessNoSession.stderr}`); + } run(bin, ['completion', 'bash'], { capture: true, env }); run(process.execPath, ['scripts/mcp-tools-smoke.mjs'], { cwd: packageRoot, env }); console.log(JSON.stringify({ ok: true, tarball: tarballName, prefix })); diff --git a/test/business-adapter.test.mjs b/test/business-adapter.test.mjs new file mode 100644 index 0000000..5eb8210 --- /dev/null +++ b/test/business-adapter.test.mjs @@ -0,0 +1,132 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'; +import http from 'node:http'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const execFileAsync = promisify(execFile); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const rootCli = path.join(repoRoot, 'dist', 'run.js'); + +function sessionPayload(requestId) { + const now = Math.floor(Date.now() / 1000); + const displayName = requestId === 'large' ? 'L'.repeat(4_000_000) : 'S'; + return { + refreshToken: `fake-refresh-${requestId}`, + accessToken: `fake-access-${requestId}`, + workspace: { + id: 'ws_atomic', + slug: 'atomic', + name: 'Atomic Test', + host: 'atomic.example.test', + role: 'owner', + }, + workspaces: [ + { + id: 'ws_atomic', + slug: 'atomic', + name: 'Atomic Test', + host: 'atomic.example.test', + role: 'owner', + }, + ], + user: { + userId: 'user_atomic', + primaryEmail: 'owner@example.test', + emailVerified: true, + displayName, + email: 'owner@example.test', + orgId: 'ws_atomic', + orgRole: 'org:admin', + }, + accessTokenClaims: { + sub: 'user_atomic', + email: 'owner@example.test', + workspaceId: 'ws_atomic', + workspaceSlug: 'atomic', + workspaceHost: 'atomic.example.test', + role: 'owner', + iat: now, + exp: now + 3_600, + typ: 'mere-cli-access', + }, + defaultWorkspaceId: 'ws_atomic', + expiresAt: new Date((now + 3_600) * 1_000).toISOString(), + }; +} + +async function readBody(request) { + const chunks = []; + for await (const chunk of request) chunks.push(chunk); + return Buffer.concat(chunks).toString('utf8'); +} + +test('bundled Business adapter atomically replaces a shared session under concurrent root CLI writes', async (t) => { + const stateHome = await mkdtemp(path.join(os.tmpdir(), 'mere-business-bundled-race-')); + t.after(() => rm(stateHome, { recursive: true, force: true })); + + const waiting = new Map(); + const server = http.createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/api/cli/v1/auth/device/exchange') { + response.writeHead(404).end(); + return; + } + + const body = JSON.parse(await readBody(request)); + waiting.set(body.requestId, response); + if (waiting.size !== 2) return; + + for (const requestId of ['large', 'small']) { + const pending = waiting.get(requestId); + pending.writeHead(200, { 'content-type': 'application/json' }); + pending.end(JSON.stringify(sessionPayload(requestId))); + } + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + t.after(() => new Promise((resolve) => server.close(resolve))); + + const address = server.address(); + assert.ok(address && typeof address === 'object'); + const consoleUrl = `http://127.0.0.1:${address.port}`; + const env = { + ...process.env, + HOME: stateHome, + XDG_STATE_HOME: stateHome, + MERE_ROOT: path.join(stateHome, 'missing-mere-root'), + MERE_CLI_SOURCE: 'bundled', + }; + const invoke = (requestId) => + execFileAsync( + process.execPath, + [ + rootCli, + 'business', + 'auth', + 'device', + 'poll', + '--request-id', + requestId, + '--console-url', + consoleUrl, + '--json', + ], + { cwd: repoRoot, env, maxBuffer: 10 * 1024 * 1024 }, + ); + + const results = await Promise.all([invoke('large'), invoke('small')]); + for (const result of results) { + assert.equal(result.stderr, ''); + assert.doesNotThrow(() => JSON.parse(result.stdout)); + } + + const stateDir = path.join(stateHome, 'mere-business'); + const sessionFile = path.join(stateDir, 'session.json'); + const saved = JSON.parse(await readFile(sessionFile, 'utf8')); + assert.ok([1, 4_000_000].includes(saved.user.displayName.length)); + assert.equal((await stat(sessionFile)).mode & 0o777, 0o600); + assert.deepEqual((await readdir(stateDir)).filter((entry) => entry.endsWith('.tmp')), []); +}); From e6fcdf39586eed31a0551e7aae2a7febfd976ec4 Mon Sep 17 00:00:00 2001 From: Kyle McCullough Date: Sat, 11 Jul 2026 08:09:54 -0300 Subject: [PATCH 2/2] Rebuild business adapter from post-#86 mere-business main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fresh pnpm build:adapters run against current product repo mains; only the business adapter and manifest timestamps changed — every other adapter reproduced byte-identical, confirming the pin-free email build. Co-Authored-By: Claude Fable 5 --- adapters/business/run.js | 38 +++++++++++++++++++++++++++++++++++--- adapters/manifest.json | 34 +++++++++++++++++----------------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/adapters/business/run.js b/adapters/business/run.js index fc5f997..f6b8cb9 100755 --- a/adapters/business/run.js +++ b/adapters/business/run.js @@ -5826,6 +5826,35 @@ var globalOptions = [ stringOption("confirm", "confirm", "Exact confirmation target for high-impact destructive actions."), booleanOption("help", "help", "Show help for this command.", "h") ]; +var leadingBooleanGlobals = /* @__PURE__ */ new Set(["--json", "--no-interactive", "--yes", "-y", "--help", "-h"]); +var leadingStringGlobals = /* @__PURE__ */ new Set(["--workspace", "--confirm"]); +function normalizeGlobalFlagPlacement(argv) { + const leading = []; + let index = 0; + while (index < argv.length) { + const argument = argv[index]; + if (!argument) break; + if (leadingBooleanGlobals.has(argument)) { + leading.push(argument); + index += 1; + continue; + } + if (leadingStringGlobals.has(argument)) { + leading.push(argument); + const value = argv[index + 1]; + if (value !== void 0) leading.push(value); + index += value === void 0 ? 1 : 2; + continue; + } + if ([...leadingStringGlobals].some((name) => argument.startsWith(`${name}=`))) { + leading.push(argument); + index += 1; + continue; + } + break; + } + return leading.length === 0 ? argv : [...argv.slice(index), ...leading]; +} var EXTERNAL_COMMANDS = /* @__PURE__ */ new Set([ "calendar.subscription.sync", "campaigns.send", @@ -8823,16 +8852,18 @@ function startsWithPath(argv, path4) { return path4.every((segment, index) => argv[index] === segment); } function findCommand(argv) { - return sortedCommands.find((command) => startsWithPath(argv, command.path)) ?? null; + const normalized = normalizeGlobalFlagPlacement(argv); + return sortedCommands.find((command) => startsWithPath(normalized, command.path)) ?? null; } function parseCommand(argv) { - const command = findCommand(argv); + const normalized = normalizeGlobalFlagPlacement(argv); + const command = findCommand(normalized); if (!command) { throw usageError("Unknown command."); } const optionSpecs = [...globalOptions, ...command.options ?? []]; const parsed = parseArgs({ - args: argv.slice(command.path.length), + args: normalized.slice(command.path.length), allowPositionals: true, strict: true, options: Object.fromEntries( @@ -9601,6 +9632,7 @@ async function createRuntime(globalFlags) { }; } async function run(argv) { + argv = normalizeGlobalFlagPlacement(argv); if (argv.length === 0) { output2.write(`${renderHelp()} `); diff --git a/adapters/manifest.json b/adapters/manifest.json index 1b90056..99c019d 100644 --- a/adapters/manifest.json +++ b/adapters/manifest.json @@ -1,118 +1,118 @@ { "schemaVersion": 1, - "builtAt": "2026-07-11T01:44:36.337Z", + "builtAt": "2026-07-11T11:08:14.576Z", "adapters": [ { "app": "business", "sourceRepoPath": "mere-business", "sourceArtifactPath": "mere-business/packages/cli/dist/index.js", "adapterPath": "adapters/business/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "finance", "sourceRepoPath": "mere-finance", "sourceArtifactPath": "mere-finance/packages/cli/bin/merefi.ts", "adapterPath": "adapters/finance/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "dynasite", "sourceRepoPath": "mere-dynasite", "sourceArtifactPath": "mere-dynasite/dist/run.js", "adapterPath": "adapters/dynasite/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "projects", "sourceRepoPath": "mere-projects", "sourceArtifactPath": "mere-projects/dist/run.js", "adapterPath": "adapters/projects/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "agent", "sourceRepoPath": "mere-agent", "sourceArtifactPath": "mere-agent/cli-dist/run.js", "adapterPath": "adapters/agent/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "today", "sourceRepoPath": "mere-today", "sourceArtifactPath": "mere-today/dist/run.js", "adapterPath": "adapters/today/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "zone", "sourceRepoPath": "mere-zone", "sourceArtifactPath": "mere-zone/dist/run.js", "adapterPath": "adapters/zone/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "video", "sourceRepoPath": "mere-video", "sourceArtifactPath": "mere-video/dist/run.js", "adapterPath": "adapters/video/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "network", "sourceRepoPath": "mere-network", "sourceArtifactPath": "mere-network/dist/run.js", "adapterPath": "adapters/network/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "email", "sourceRepoPath": "mere-email", "sourceArtifactPath": "mere-email/dist/run.js", "adapterPath": "adapters/email/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "im", "sourceRepoPath": "mere-im", "sourceArtifactPath": "mere-im/dist/run.js", "adapterPath": "adapters/im/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "gives", "sourceRepoPath": "mere-gives", "sourceArtifactPath": "mere-gives/dist/run.js", "adapterPath": "adapters/gives/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "works", "sourceRepoPath": "mere-works", "sourceArtifactPath": "mere-works/dist/run.js", "adapterPath": "adapters/works/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "media", "sourceRepoPath": "mere-media", "sourceArtifactPath": "mere-media/dist/run.js", "adapterPath": "adapters/media/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "deliver", "sourceRepoPath": "mere-deliver", "sourceArtifactPath": "mere-deliver/cli/run.js", "adapterPath": "adapters/deliver/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" }, { "app": "link", "sourceRepoPath": "merekit-link", "sourceArtifactPath": "merekit-link/dist/run.js", "adapterPath": "adapters/link/run.js", - "builtAt": "2026-07-11T01:44:36.337Z" + "builtAt": "2026-07-11T11:08:14.576Z" } ] }