ugly-app 0.1.697 → 0.1.698
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.
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/native/defineTask.d.ts.map +1 -1
- package/dist/native/defineTask.js.map +1 -1
- package/dist/native/node.d.ts.map +1 -1
- package/dist/native/node.js +37 -13
- package/dist/native/node.js.map +1 -1
- package/dist/native/protocol.d.ts +13 -0
- package/dist/native/protocol.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/native/defineTask.ts +6 -0
- package/src/native/node.test.ts +56 -1
- package/src/native/node.ts +57 -9
- package/src/native/protocol.ts +14 -0
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.698";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineTask.d.ts","sourceRoot":"","sources":["../../src/native/defineTask.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"defineTask.d.ts","sourceRoot":"","sources":["../../src/native/defineTask.ts"],"names":[],"mappings":"AAiBA,iGAAiG;AACjG,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,KAAK,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;CACxE;AAED,uFAAuF;AACvF,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1C,4EAA4E;IAC5E,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;CACtC;AA4BD,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAEpD;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,CAezD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineTask.js","sourceRoot":"","sources":["../../src/native/defineTask.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,4FAA4F;AAC5F,0FAA0F;AAC1F,0FAA0F;AAC1F,EAAE;AACF,+DAA+D;AAC/D,qDAAqD;AACrD,iBAAiB;AACjB,gBAAgB;AAChB,6FAA6F;AAC7F,+DAA+D;AAC/D,SAAS;AACT,QAAQ;AACR,qCAAqC;
|
|
1
|
+
{"version":3,"file":"defineTask.js","sourceRoot":"","sources":["../../src/native/defineTask.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,4FAA4F;AAC5F,0FAA0F;AAC1F,0FAA0F;AAC1F,EAAE;AACF,+DAA+D;AAC/D,qDAAqD;AACrD,iBAAiB;AACjB,gBAAgB;AAChB,6FAA6F;AAC7F,+DAA+D;AAC/D,SAAS;AACT,QAAQ;AACR,qCAAqC;AAqCrC,SAAS,WAAW;IAClB,MAAM,CAAC,GAAG,UAA6C,CAAC;IACxD,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CACb,qEAAqE;YACnE,wEAAwE,CAC3E,CAAC;IACJ,CAAC;IACD,OAAO,CAAgB,CAAC;AAC1B,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,UAAU,CAAC,GAAmB;IAC5C,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,WAAW;IACzB,MAAM,CAAC,GAAG,WAAW,EAAE,CAAC,QAAQ,CAAC;IACjC,OAAO;QACL,IAAI,EAAE;YACJ,OAAO,CAAC,CAAC,EAAE,CAAC;QACd,CAAC;QACD,IAAI,IAAI;YACN,OAAO,CAAC,CAAC,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,MAAM;YACR,OAAO,CAAC,CAAC,MAAW,CAAC;QACvB,CAAC;QACD,IAAI,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC;QAC1C,WAAW,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC;KACnD,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/native/node.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/native/node.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,eAAe,CAAC;AAmChE,wBAAgB,oBAAoB,IAAI,UAAU,CAwGjD"}
|
package/dist/native/node.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// Node-backed UglyNative for background-task children. A task bundle runs in a plain Node
|
|
2
|
-
// child process (full Node API, OS-confined by the host), so inside it `native.fs.*` /
|
|
3
|
-
// `native.process.*` resolve to node:fs / child_process here — no host round-trip and no
|
|
4
|
-
// permission gate (the process boundary IS the boundary). A task bundle installs it before
|
|
5
|
-
// importing the framework facade: globalThis.UglyNative = createNodeUglyNative().
|
|
6
|
-
// Device caps + other desktop namespaces (net/serve/workers/browse/mcp/...) aren't provided
|
|
7
|
-
// and reject with NativeUnavailableError.
|
|
8
|
-
import { promises as fsp, realpathSync } from 'node:fs';
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
1
|
import { NativeUnavailableError } from './protocol.js';
|
|
2
|
+
let nodeModsPromise = null;
|
|
3
|
+
function loadNodeMods() {
|
|
4
|
+
if (!nodeModsPromise) {
|
|
5
|
+
nodeModsPromise = (async () => {
|
|
6
|
+
const fs = await import('node:fs');
|
|
7
|
+
const cp = await import('node:child_process');
|
|
8
|
+
return { fsp: fs.promises, realpathSync: fs.realpathSync, spawn: cp.spawn };
|
|
9
|
+
})();
|
|
10
|
+
}
|
|
11
|
+
return nodeModsPromise;
|
|
12
|
+
}
|
|
13
|
+
/** The host bridge the task runner injects on `globalThis.uglyTask.host`, if any. */
|
|
14
|
+
function hostBridge() {
|
|
15
|
+
return globalThis.uglyTask?.host;
|
|
16
|
+
}
|
|
11
17
|
// node = full local access, so every gated namespace reports granted (the facade's
|
|
12
18
|
// permissions.ensure() reads this before fs/process calls).
|
|
13
19
|
const ALL_GRANTED = {
|
|
@@ -30,7 +36,7 @@ export function createNodeUglyNative() {
|
|
|
30
36
|
};
|
|
31
37
|
const procs = new Map();
|
|
32
38
|
let seq = 0;
|
|
33
|
-
function spawnProc(cmd, args, opts) {
|
|
39
|
+
function spawnProc(spawn, cmd, args, opts) {
|
|
34
40
|
const id = `n${seq++}`;
|
|
35
41
|
const env = opts?.replaceEnv ? (opts.env ?? {}) : { ...process.env, ...(opts?.env ?? {}) };
|
|
36
42
|
const child = spawn(cmd, args, { cwd: opts?.cwd, env, detached: opts?.detached ?? false });
|
|
@@ -45,12 +51,17 @@ export function createNodeUglyNative() {
|
|
|
45
51
|
const p = (payload ?? {});
|
|
46
52
|
const str = (k) => String(p[k] ?? '');
|
|
47
53
|
switch (channel) {
|
|
48
|
-
// permissions — node grants everything (process isolation is the boundary)
|
|
54
|
+
// permissions — node grants everything (process isolation is the boundary). Handled
|
|
55
|
+
// before loading node builtins so the (frequent) permission check stays cheap.
|
|
49
56
|
case 'permissions.query':
|
|
50
57
|
case 'permissions.request':
|
|
51
58
|
return ALL_GRANTED;
|
|
52
59
|
case 'permissions.revoke':
|
|
53
60
|
return undefined;
|
|
61
|
+
}
|
|
62
|
+
// Anything past here that we service locally needs node:fs / node:child_process.
|
|
63
|
+
const { fsp, realpathSync, spawn } = await loadNodeMods();
|
|
64
|
+
switch (channel) {
|
|
54
65
|
// fs (text)
|
|
55
66
|
case 'fs.readFile':
|
|
56
67
|
return { content: await fsp.readFile(str('path'), 'utf8') };
|
|
@@ -94,7 +105,7 @@ export function createNodeUglyNative() {
|
|
|
94
105
|
return undefined;
|
|
95
106
|
// process (id-stream; stdout/stderr/exit/error arrive as process.<kind>:<id> events)
|
|
96
107
|
case 'process.spawn':
|
|
97
|
-
return spawnProc(str('cmd'), p.args ?? [], p.opts);
|
|
108
|
+
return spawnProc(spawn, str('cmd'), p.args ?? [], p.opts);
|
|
98
109
|
case 'process.write':
|
|
99
110
|
procs.get(str('id'))?.stdin?.write(String(p.data ?? ''));
|
|
100
111
|
return undefined;
|
|
@@ -107,8 +118,14 @@ export function createNodeUglyNative() {
|
|
|
107
118
|
}
|
|
108
119
|
catch { /* dead */ }
|
|
109
120
|
return undefined;
|
|
110
|
-
default:
|
|
121
|
+
default: {
|
|
122
|
+
// Not serviceable in the Node child — proxy to the host if it gave us a bridge,
|
|
123
|
+
// else surface the standalone "needs a native shell" error.
|
|
124
|
+
const host = hostBridge();
|
|
125
|
+
if (host)
|
|
126
|
+
return host.invoke(channel, payload);
|
|
111
127
|
throw new NativeUnavailableError(channel);
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
}
|
|
114
131
|
return {
|
|
@@ -116,6 +133,13 @@ export function createNodeUglyNative() {
|
|
|
116
133
|
invoke: ((channel, payload) => invoke(channel, payload)),
|
|
117
134
|
subscribe: ((event, cb) => {
|
|
118
135
|
const key = event;
|
|
136
|
+
// `process.*` streams are emitted locally by spawnProc; everything else (browse/mcp/…
|
|
137
|
+
// events) originates host-side, so forward the subscription over the bridge when present.
|
|
138
|
+
if (!key.startsWith('process.')) {
|
|
139
|
+
const host = hostBridge();
|
|
140
|
+
if (host)
|
|
141
|
+
return host.subscribe(key, cb);
|
|
142
|
+
}
|
|
119
143
|
let s = listeners.get(key);
|
|
120
144
|
if (!s) {
|
|
121
145
|
s = new Set();
|
package/dist/native/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/native/node.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/native/node.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,19 @@ export interface UglyNative {
|
|
|
12
12
|
invoke<C extends Channel>(channel: C, payload: PayloadOf<C>): Promise<ResultOf<C>>;
|
|
13
13
|
subscribe<E extends NativeEvent>(event: E, cb: (data: DataOf<E>) => void): () => void;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A task child's bridge to the host's real UglyNative. The host task runner injects this on
|
|
17
|
+
* `globalThis.uglyTask.host` so a Node-child task can proxy any channel it can't service
|
|
18
|
+
* locally — browse/capture/clipboard/mcp/terminal/net/… — back to the parent Ugly Studio
|
|
19
|
+
* process, which executes it against the desktop native bridge (`routeUglyNative`). `fs.*` /
|
|
20
|
+
* `process.*` stay local in the child (no round-trip); everything else forwards here.
|
|
21
|
+
* Untyped (string channels/events) because it spans the whole contract and crosses the IPC
|
|
22
|
+
* boundary; `createNodeUglyNative` adapts it to the typed `UglyNative` surface.
|
|
23
|
+
*/
|
|
24
|
+
export interface TaskHostBridge {
|
|
25
|
+
invoke(channel: string, payload: unknown): Promise<unknown>;
|
|
26
|
+
subscribe(event: string, cb: (data: unknown) => void): () => void;
|
|
27
|
+
}
|
|
15
28
|
export interface AndroidTransport {
|
|
16
29
|
invoke(requestId: string, channel: string, payloadJson: string): void;
|
|
17
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/native/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAElG,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC;AAEnE,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;gBAClB,SAAS,EAAE,SAAS;CAKjC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,MAAM,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,SAAS,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACvF;AAQD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACvE;AACD,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,MAAM,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE;YAAE,UAAU,CAAC,EAAE,eAAe,CAAA;SAAE,CAAA;KAAE,CAAC;IAChE,mBAAmB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CACpF"}
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/native/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAElG,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC;AAEnE,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;gBAClB,SAAS,EAAE,SAAS;CAKjC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,MAAM,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,SAAS,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACvF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACnE;AAQD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACvE;AACD,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,MAAM,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE;YAAE,UAAU,CAAC,EAAE,eAAe,CAAA;SAAE,CAAA;KAAE,CAAC;IAChE,mBAAmB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CACpF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.698",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
6
6
|
"files": [
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.698";
|
package/src/native/defineTask.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// });
|
|
14
14
|
// t.setSnapshot({ turn: 'idle' });
|
|
15
15
|
|
|
16
|
+
import type { TaskHostBridge } from './protocol.js';
|
|
17
|
+
|
|
16
18
|
/** A task's request/response methods, callable via the task facade's `call(method, payload)`. */
|
|
17
19
|
export interface TaskDefinition {
|
|
18
20
|
onCall: Record<string, (payload: never) => unknown | Promise<unknown>>;
|
|
@@ -39,6 +41,10 @@ interface TaskGlobals {
|
|
|
39
41
|
params: unknown;
|
|
40
42
|
emit(event: string, data?: unknown): void;
|
|
41
43
|
setSnapshot(snapshot: unknown): void;
|
|
44
|
+
// Injected by the host task runner so `createNodeUglyNative` can proxy non-local
|
|
45
|
+
// channels (browse/capture/clipboard/mcp/…) back to the parent process. Absent when
|
|
46
|
+
// a bundle runs outside a host that supports it (then those channels reject).
|
|
47
|
+
host?: TaskHostBridge;
|
|
42
48
|
};
|
|
43
49
|
}
|
|
44
50
|
|
package/src/native/node.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterAll } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterAll, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { promises as fsp } from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -58,3 +58,58 @@ describe('createNodeUglyNative', () => {
|
|
|
58
58
|
if (dir) await fsp.rm(dir, { recursive: true, force: true });
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
describe('createNodeUglyNative with a host bridge', () => {
|
|
63
|
+
const g = globalThis as unknown as { uglyTask?: unknown };
|
|
64
|
+
const invokeCalls: { channel: string; payload: unknown }[] = [];
|
|
65
|
+
const subCalls: string[] = [];
|
|
66
|
+
const subCbs = new Map<string, (d: unknown) => void>();
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
invokeCalls.length = 0; subCalls.length = 0; subCbs.clear();
|
|
70
|
+
g.uglyTask = {
|
|
71
|
+
host: {
|
|
72
|
+
invoke: (channel: string, payload: unknown) => {
|
|
73
|
+
invokeCalls.push({ channel, payload });
|
|
74
|
+
return Promise.resolve({ proxied: channel });
|
|
75
|
+
},
|
|
76
|
+
subscribe: (event: string, cb: (d: unknown) => void) => {
|
|
77
|
+
subCalls.push(event); subCbs.set(event, cb);
|
|
78
|
+
return () => subCbs.delete(event);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => { delete g.uglyTask; });
|
|
84
|
+
|
|
85
|
+
it('forwards non-local channels to the host bridge instead of throwing', async () => {
|
|
86
|
+
const hn = createNodeUglyNative();
|
|
87
|
+
const r = await hn.invoke('browse.read' as never, { url: 'x' } as never);
|
|
88
|
+
expect(r).toEqual({ proxied: 'browse.read' });
|
|
89
|
+
expect(invokeCalls).toEqual([{ channel: 'browse.read', payload: { url: 'x' } }]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('still services fs/process/permissions locally (no forward)', async () => {
|
|
93
|
+
const hn = createNodeUglyNative();
|
|
94
|
+
const r = (await hn.invoke('permissions.query', undefined)) as { granted: Record<string, string> };
|
|
95
|
+
expect(r.granted.fs).toBe('full');
|
|
96
|
+
expect(invokeCalls).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('forwards non-process event subscriptions to the host bridge', () => {
|
|
100
|
+
const hn = createNodeUglyNative();
|
|
101
|
+
const seen: unknown[] = [];
|
|
102
|
+
const off = hn.subscribe('mcp.event:1' as never, ((d: unknown) => seen.push(d)) as never);
|
|
103
|
+
expect(subCalls).toContain('mcp.event:1');
|
|
104
|
+
subCbs.get('mcp.event:1')?.({ hi: true });
|
|
105
|
+
expect(seen).toEqual([{ hi: true }]);
|
|
106
|
+
off();
|
|
107
|
+
expect(subCbs.has('mcp.event:1')).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('keeps process.* event subscriptions local even with a bridge', () => {
|
|
111
|
+
const hn = createNodeUglyNative();
|
|
112
|
+
hn.subscribe('process.stdout:n0' as never, (() => {}) as never);
|
|
113
|
+
expect(subCalls).not.toContain('process.stdout:n0');
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/native/node.ts
CHANGED
|
@@ -3,14 +3,46 @@
|
|
|
3
3
|
// `native.process.*` resolve to node:fs / child_process here — no host round-trip and no
|
|
4
4
|
// permission gate (the process boundary IS the boundary). A task bundle installs it before
|
|
5
5
|
// importing the framework facade: globalThis.UglyNative = createNodeUglyNative().
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
//
|
|
7
|
+
// Other namespaces (browse/capture/clipboard/mcp/terminal/net/...) have no local Node impl.
|
|
8
|
+
// When the host task runner injected a bridge on `globalThis.uglyTask.host`, those channels
|
|
9
|
+
// (and their events) are FORWARDED to the parent Ugly Studio process, which executes them
|
|
10
|
+
// against the real desktop native bridge — so a task gets the full UglyNative API, not just
|
|
11
|
+
// fs/process. Standalone (no host bridge) they still reject with NativeUnavailableError.
|
|
12
|
+
//
|
|
13
|
+
// `node:fs` / `node:child_process` are loaded LAZILY (dynamic import inside the methods, not
|
|
14
|
+
// a static top-level import). This module is re-exported from the `ugly-app/native` barrel,
|
|
15
|
+
// which app SHELLS import for the browser-side facade — a static `node:*` import there breaks
|
|
16
|
+
// the client bundle (rollup can't resolve `node:child_process` for the browser). The lazy
|
|
17
|
+
// load keeps the module browser-IMPORT-safe; it's only ever CALLED inside a real Node task.
|
|
18
|
+
import type { ChildProcess } from 'node:child_process';
|
|
10
19
|
import type { Channel, DataOf, HostDirent, HostStat, NativeEvent, PayloadOf, ResultOf, SpawnOpts } from './contract.js';
|
|
11
|
-
import type { UglyNative } from './protocol.js';
|
|
20
|
+
import type { TaskHostBridge, UglyNative } from './protocol.js';
|
|
12
21
|
import { NativeUnavailableError } from './protocol.js';
|
|
13
22
|
|
|
23
|
+
/** Node builtins, resolved once on first use (never at import time → browser-safe). */
|
|
24
|
+
interface NodeMods {
|
|
25
|
+
fsp: typeof import('node:fs')['promises'];
|
|
26
|
+
realpathSync: typeof import('node:fs')['realpathSync'];
|
|
27
|
+
spawn: typeof import('node:child_process')['spawn'];
|
|
28
|
+
}
|
|
29
|
+
let nodeModsPromise: Promise<NodeMods> | null = null;
|
|
30
|
+
function loadNodeMods(): Promise<NodeMods> {
|
|
31
|
+
if (!nodeModsPromise) {
|
|
32
|
+
nodeModsPromise = (async (): Promise<NodeMods> => {
|
|
33
|
+
const fs = await import('node:fs');
|
|
34
|
+
const cp = await import('node:child_process');
|
|
35
|
+
return { fsp: fs.promises, realpathSync: fs.realpathSync, spawn: cp.spawn };
|
|
36
|
+
})();
|
|
37
|
+
}
|
|
38
|
+
return nodeModsPromise;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The host bridge the task runner injects on `globalThis.uglyTask.host`, if any. */
|
|
42
|
+
function hostBridge(): TaskHostBridge | undefined {
|
|
43
|
+
return (globalThis as unknown as { uglyTask?: { host?: TaskHostBridge } }).uglyTask?.host;
|
|
44
|
+
}
|
|
45
|
+
|
|
14
46
|
// node = full local access, so every gated namespace reports granted (the facade's
|
|
15
47
|
// permissions.ensure() reads this before fs/process calls).
|
|
16
48
|
const ALL_GRANTED = {
|
|
@@ -30,7 +62,7 @@ export function createNodeUglyNative(): UglyNative {
|
|
|
30
62
|
const procs = new Map<string, ChildProcess>();
|
|
31
63
|
let seq = 0;
|
|
32
64
|
|
|
33
|
-
function spawnProc(cmd: string, args: string[], opts?: SpawnOpts): { id: string; pid: number | null } {
|
|
65
|
+
function spawnProc(spawn: NodeMods['spawn'], cmd: string, args: string[], opts?: SpawnOpts): { id: string; pid: number | null } {
|
|
34
66
|
const id = `n${seq++}`;
|
|
35
67
|
const env = opts?.replaceEnv ? (opts.env ?? {}) : { ...process.env, ...(opts?.env ?? {}) };
|
|
36
68
|
const child = spawn(cmd, args, { cwd: opts?.cwd, env, detached: opts?.detached ?? false });
|
|
@@ -46,12 +78,17 @@ export function createNodeUglyNative(): UglyNative {
|
|
|
46
78
|
const p = (payload ?? {}) as Record<string, unknown>;
|
|
47
79
|
const str = (k: string): string => String(p[k] ?? '');
|
|
48
80
|
switch (channel) {
|
|
49
|
-
// permissions — node grants everything (process isolation is the boundary)
|
|
81
|
+
// permissions — node grants everything (process isolation is the boundary). Handled
|
|
82
|
+
// before loading node builtins so the (frequent) permission check stays cheap.
|
|
50
83
|
case 'permissions.query':
|
|
51
84
|
case 'permissions.request':
|
|
52
85
|
return ALL_GRANTED;
|
|
53
86
|
case 'permissions.revoke':
|
|
54
87
|
return undefined;
|
|
88
|
+
}
|
|
89
|
+
// Anything past here that we service locally needs node:fs / node:child_process.
|
|
90
|
+
const { fsp, realpathSync, spawn } = await loadNodeMods();
|
|
91
|
+
switch (channel) {
|
|
55
92
|
// fs (text)
|
|
56
93
|
case 'fs.readFile':
|
|
57
94
|
return { content: await fsp.readFile(str('path'), 'utf8') };
|
|
@@ -84,15 +121,20 @@ export function createNodeUglyNative(): UglyNative {
|
|
|
84
121
|
await fsp.writeFile(str('path'), Buffer.from(str('data'), 'latin1')); return undefined;
|
|
85
122
|
// process (id-stream; stdout/stderr/exit/error arrive as process.<kind>:<id> events)
|
|
86
123
|
case 'process.spawn':
|
|
87
|
-
return spawnProc(str('cmd'), (p.args as string[]) ?? [], p.opts as SpawnOpts | undefined);
|
|
124
|
+
return spawnProc(spawn, str('cmd'), (p.args as string[]) ?? [], p.opts as SpawnOpts | undefined);
|
|
88
125
|
case 'process.write':
|
|
89
126
|
procs.get(str('id'))?.stdin?.write(String(p.data ?? '')); return undefined;
|
|
90
127
|
case 'process.closeStdin':
|
|
91
128
|
procs.get(str('id'))?.stdin?.end(); return undefined;
|
|
92
129
|
case 'process.kill':
|
|
93
130
|
try { procs.get(str('id'))?.kill((p.signal as NodeJS.Signals | undefined)); } catch { /* dead */ } return undefined;
|
|
94
|
-
default:
|
|
131
|
+
default: {
|
|
132
|
+
// Not serviceable in the Node child — proxy to the host if it gave us a bridge,
|
|
133
|
+
// else surface the standalone "needs a native shell" error.
|
|
134
|
+
const host = hostBridge();
|
|
135
|
+
if (host) return host.invoke(channel, payload);
|
|
95
136
|
throw new NativeUnavailableError(channel);
|
|
137
|
+
}
|
|
96
138
|
}
|
|
97
139
|
}
|
|
98
140
|
|
|
@@ -102,6 +144,12 @@ export function createNodeUglyNative(): UglyNative {
|
|
|
102
144
|
invoke(channel, payload) as Promise<ResultOf<C>>) as UglyNative['invoke'],
|
|
103
145
|
subscribe: (<E extends NativeEvent>(event: E, cb: (data: DataOf<E>) => void) => {
|
|
104
146
|
const key = event as string;
|
|
147
|
+
// `process.*` streams are emitted locally by spawnProc; everything else (browse/mcp/…
|
|
148
|
+
// events) originates host-side, so forward the subscription over the bridge when present.
|
|
149
|
+
if (!key.startsWith('process.')) {
|
|
150
|
+
const host = hostBridge();
|
|
151
|
+
if (host) return host.subscribe(key, cb as (d: unknown) => void);
|
|
152
|
+
}
|
|
105
153
|
let s = listeners.get(key);
|
|
106
154
|
if (!s) { s = new Set(); listeners.set(key, s); }
|
|
107
155
|
s.add(cb as (d: unknown) => void);
|
package/src/native/protocol.ts
CHANGED
|
@@ -24,6 +24,20 @@ export interface UglyNative {
|
|
|
24
24
|
subscribe<E extends NativeEvent>(event: E, cb: (data: DataOf<E>) => void): () => void;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A task child's bridge to the host's real UglyNative. The host task runner injects this on
|
|
29
|
+
* `globalThis.uglyTask.host` so a Node-child task can proxy any channel it can't service
|
|
30
|
+
* locally — browse/capture/clipboard/mcp/terminal/net/… — back to the parent Ugly Studio
|
|
31
|
+
* process, which executes it against the desktop native bridge (`routeUglyNative`). `fs.*` /
|
|
32
|
+
* `process.*` stay local in the child (no round-trip); everything else forwards here.
|
|
33
|
+
* Untyped (string channels/events) because it spans the whole contract and crosses the IPC
|
|
34
|
+
* boundary; `createNodeUglyNative` adapts it to the typed `UglyNative` surface.
|
|
35
|
+
*/
|
|
36
|
+
export interface TaskHostBridge {
|
|
37
|
+
invoke(channel: string, payload: unknown): Promise<unknown>;
|
|
38
|
+
subscribe(event: string, cb: (data: unknown) => void): () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
// Low-level transports install.ts normalises into one `window.UglyNative`.
|
|
28
42
|
// desktop: Electron contextBridge injects `window.UglyNative` directly (no adapter).
|
|
29
43
|
// iOS: window.webkit.messageHandlers.uglyInvoke (WKScriptMessageHandlerWithReply)
|