nativemason 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -364,9 +364,11 @@ export class BridgeApplication {
|
|
|
364
364
|
async open(input) {
|
|
365
365
|
if ((input.nativePort === undefined) !== (input.nativeToken === undefined))
|
|
366
366
|
throw new BridgeFault('INVALID_INPUT', 'nativePort and nativeToken must be supplied together');
|
|
367
|
+
if (input.nativeHost && !input.nativeHost.startsWith('127.') && !input.nativeToken)
|
|
368
|
+
throw new BridgeFault('INVALID_INPUT', 'LAN pairing requires explicit nativePort and nativeToken');
|
|
367
369
|
const id = randomUUID(), token = input.nativeToken ?? randomUUID(), port = input.nativePort ?? 19482;
|
|
368
370
|
const source = this.deps.source(input.projectRoot);
|
|
369
|
-
const native = await this.deps.native({ port, token, scenarioId: input.scenarioId, sessionId: id, targetId: input.nativeTarget?.targetId, platform: input.nativeTarget?.platform });
|
|
371
|
+
const native = await this.deps.native({ port, token, host: input.nativeHost, scenarioId: input.scenarioId, sessionId: id, targetId: input.nativeTarget?.targetId, platform: input.nativeTarget?.platform });
|
|
370
372
|
let browser;
|
|
371
373
|
try {
|
|
372
374
|
browser = await this.deps.browser({ ...input, sessionId: id });
|
package/dist/editor/planner.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { createTwoFilesPatch } from 'diff';
|
|
3
|
-
import { Node, Project, SyntaxKind } from 'ts-morph';
|
|
3
|
+
import { Node, Project, SyntaxKind, ts } from 'ts-morph';
|
|
4
4
|
import { isLiteralStyleObject, valueText } from './source-format.js';
|
|
5
5
|
import { RnStylePatchSchema, SourceRefSchema } from '@nativemason/contracts';
|
|
6
6
|
export const hashSource = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
7
7
|
class Unsupported extends Error {
|
|
8
8
|
}
|
|
9
9
|
function reject(message) { throw new Unsupported(message); }
|
|
10
|
+
function hasTrailingComma(sourceText) {
|
|
11
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, ts.LanguageVariant.Standard, sourceText);
|
|
12
|
+
return scanner.scan() === ts.SyntaxKind.CommaToken && scanner.scan() === ts.SyntaxKind.EndOfFileToken;
|
|
13
|
+
}
|
|
10
14
|
function staticLiteral(expression) {
|
|
11
15
|
if (Node.isStringLiteral(expression) || Node.isNumericLiteral(expression) || Node.isNoSubstitutionTemplateLiteral(expression))
|
|
12
16
|
return true;
|
|
@@ -284,7 +288,7 @@ export function planSourcePatch(sourceText, sourceRef, patch, scope) {
|
|
|
284
288
|
const closing = object.getEnd() - 1;
|
|
285
289
|
const properties = object.getProperties();
|
|
286
290
|
// Inserting before the closing brace preserves comments and all unrelated text.
|
|
287
|
-
const trailingComma = properties.length > 0 &&
|
|
291
|
+
const trailingComma = properties.length > 0 && hasTrailingComma(sourceText.slice(properties.at(-1).getEnd(), closing));
|
|
288
292
|
edits.push({ start: closing, end: closing, text: `${properties.length && !trailingComma ? ',' : ''} ${additions.join(', ')} ` });
|
|
289
293
|
}
|
|
290
294
|
else if (!object) {
|
|
@@ -92,24 +92,53 @@ export function createTransactionSourcePort(projectRoot) {
|
|
|
92
92
|
catch (error) {
|
|
93
93
|
if (error.code !== 'EEXIST')
|
|
94
94
|
throw error;
|
|
95
|
-
|
|
95
|
+
const recovery = path.join(dir, 'transaction.recovery.lock');
|
|
96
|
+
let recoveryHandle;
|
|
96
97
|
try {
|
|
97
|
-
|
|
98
|
+
recoveryHandle = await open(recovery, 'wx', 0o600);
|
|
99
|
+
await recoveryHandle.writeFile(JSON.stringify({ pid: process.pid }));
|
|
100
|
+
await recoveryHandle.sync();
|
|
101
|
+
await recoveryHandle.close();
|
|
102
|
+
recoveryHandle = undefined;
|
|
98
103
|
}
|
|
99
|
-
catch {
|
|
100
|
-
|
|
104
|
+
catch (cause) {
|
|
105
|
+
const created = recoveryHandle !== undefined;
|
|
106
|
+
await recoveryHandle?.close().catch(() => undefined);
|
|
107
|
+
if (created)
|
|
108
|
+
await remove(recovery).catch(() => undefined);
|
|
109
|
+
if (cause.code === 'EEXIST' && !created)
|
|
110
|
+
throw new BridgeFault('RECOVERY_CONFLICT', 'Transaction recovery is locked; inspect .nativemason/transaction.recovery.lock for its owner before retrying');
|
|
111
|
+
throw cause;
|
|
101
112
|
}
|
|
102
113
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
let owner;
|
|
115
|
+
try {
|
|
116
|
+
const decoded = JSON.parse(await readFile(lock, 'utf8'));
|
|
117
|
+
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded) || !Number.isSafeInteger(decoded.pid) || (decoded.pid <= 0))
|
|
118
|
+
throw new Error('Invalid lock owner');
|
|
119
|
+
owner = decoded;
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
if (cause.code !== 'ENOENT')
|
|
123
|
+
throw new BridgeFault('INTERNAL_ERROR', 'Transaction lock is unreadable; recovery requires checking the owning process');
|
|
124
|
+
}
|
|
125
|
+
if (owner) {
|
|
126
|
+
try {
|
|
127
|
+
process.kill(owner.pid, 0);
|
|
128
|
+
throw new BridgeFault('INVALID_INPUT', 'Another source transaction is active');
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
if (cause instanceof BridgeFault)
|
|
132
|
+
throw cause;
|
|
133
|
+
if (cause.code !== 'ESRCH')
|
|
134
|
+
throw cause;
|
|
135
|
+
}
|
|
136
|
+
await remove(lock);
|
|
137
|
+
}
|
|
105
138
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
throw cause;
|
|
109
|
-
if (cause.code !== 'ESRCH')
|
|
110
|
-
throw cause;
|
|
139
|
+
finally {
|
|
140
|
+
await remove(recovery);
|
|
111
141
|
}
|
|
112
|
-
await remove(lock);
|
|
113
142
|
return work();
|
|
114
143
|
}
|
|
115
144
|
try {
|
package/dist/mcp/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const descriptions = {
|
|
|
20
20
|
close_session: 'Close Chromium and native transport and discard session state.',
|
|
21
21
|
};
|
|
22
22
|
export function createMcpServer(application = new BridgeApplication()) {
|
|
23
|
-
const server = new McpServer({ name: 'nativemason', version: '0.
|
|
23
|
+
const server = new McpServer({ name: 'nativemason', version: '0.3.0' });
|
|
24
24
|
server.registerPrompt('layout-workflow', { description: 'Measured layout correction workflow' }, async () => ({ messages: [{ role: 'user', content: { type: 'text', text: 'Open a session for the same named scenario on web and native. Inspect measurements and diagnostics; load or infer and review constraints. Use suggest_fix or batch trials, preview the transaction and blast radius, then apply only an accepted candidate. Verify matching committed revisions on the selected native target. Report unavailable evidence; use revert_commit when requested and verify the revert. Close the session.' } }] }));
|
|
25
25
|
for (const name of Object.keys(ToolSchemas)) {
|
|
26
26
|
server.registerTool(name, { description: descriptions[name], inputSchema: ToolSchemas[name].shape, outputSchema: ToolMcpOutputSchemas[name] }, async (input) => {
|
package/dist/native/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
-
import { BridgeFault, NativeClientMessageSchema } from '@nativemason/contracts';
|
|
3
|
+
import { BridgeFault, NativeBindHostSchema, NativeClientMessageSchema } from '@nativemason/contracts';
|
|
4
4
|
export async function createNativeBridge(options) {
|
|
5
|
-
const
|
|
5
|
+
const host = NativeBindHostSchema.safeParse(options.host ?? '127.0.0.1');
|
|
6
|
+
if (!host.success)
|
|
7
|
+
throw new BridgeFault('INVALID_INPUT', 'Native host must be a loopback or private LAN IPv4 interface address');
|
|
8
|
+
const server = new WebSocketServer({ host: host.data, port: options.port, maxPayload: 4 * 1024 * 1024 });
|
|
6
9
|
await new Promise((resolve, reject) => { server.once('listening', resolve); server.once('error', reject); });
|
|
7
10
|
let client;
|
|
8
11
|
let closed = false;
|
package/dist/scenario/index.js
CHANGED
|
@@ -87,6 +87,8 @@ export function createNativeScenarioPort(options) {
|
|
|
87
87
|
if (closed)
|
|
88
88
|
throw new BridgeFault('SESSION_NOT_FOUND', 'Native scenario driver is closed');
|
|
89
89
|
const parsed = ScenarioScriptSchema.parse(script);
|
|
90
|
+
if (options.target.platform === 'ios' && options.target.kind === 'physical' && [...parsed.reset, ...parsed.actions].some(action => action.type !== 'hook'))
|
|
91
|
+
throw new BridgeFault('UNSUPPORTED_ACTION', 'Physical iOS supports registered app hooks; Maestro UI actions require an iOS simulator. Navigate manually before capture.');
|
|
90
92
|
// Reject unsupported static actions before executing earlier hooks or UI actions.
|
|
91
93
|
for (const action of [...parsed.reset, ...parsed.actions]) {
|
|
92
94
|
if (action.type === 'hook' && !options.runHook)
|
|
@@ -238,6 +240,16 @@ export function createNativeProfilePort(options) {
|
|
|
238
240
|
return {
|
|
239
241
|
recover: () => recover(),
|
|
240
242
|
async withProfile(profile, work) {
|
|
243
|
+
if (options.target.platform === 'ios' && options.target.kind === 'physical') {
|
|
244
|
+
if (profile.fontScale !== undefined || profile.orientation !== undefined) {
|
|
245
|
+
if (!options.readProfile)
|
|
246
|
+
throw new BridgeFault('UNSUPPORTED_PROFILE', 'Physical iOS profile checks require runtime measurements');
|
|
247
|
+
const actual = await options.readProfile();
|
|
248
|
+
if ((profile.fontScale !== undefined && Math.abs(actual.fontScale - profile.fontScale) > 0.01) || (profile.orientation !== undefined && actual.orientation !== profile.orientation))
|
|
249
|
+
throw new BridgeFault('UNSUPPORTED_PROFILE', 'Change font scale or orientation manually on the iOS device, then capture its measured profile');
|
|
250
|
+
}
|
|
251
|
+
return work();
|
|
252
|
+
}
|
|
241
253
|
if (active || profileOperations.has(journal))
|
|
242
254
|
throw new BridgeFault('RECOVERY_CONFLICT', 'Native profile operations must be serialized');
|
|
243
255
|
active = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nativemason",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nativemason": "./bin/nativemason.js",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"@babel/core": "^7.28.5",
|
|
18
18
|
"@babel/types": "^7.28.5",
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
20
|
-
"@nativemason/contracts": "0.
|
|
20
|
+
"@nativemason/contracts": "0.3.0",
|
|
21
21
|
"diff": "^8.0.2",
|
|
22
22
|
"playwright": "^1.56.1",
|
|
23
|
-
"sharp": "^0.
|
|
23
|
+
"sharp": "^0.35.5",
|
|
24
24
|
"ts-morph": "^27.0.2",
|
|
25
25
|
"ws": "^8.18.3"
|
|
26
26
|
},
|