ugly-app 0.1.859 → 0.1.861

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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.859";
1
+ export declare const CLI_VERSION = "0.1.861";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.859";
2
+ export const CLI_VERSION = "0.1.861";
3
3
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,29 @@
1
+ import type { Page } from '@playwright/test';
2
+ /**
3
+ * The developer's live ugly.bot session, persisted by Ugly Studio / `ugly-app login` at
4
+ * `~/.ugly-bot/auth.json` as `{ token, userId, serverUrl }`. This is a REAL ugly.bot-issued
5
+ * JWT — a Mode-A deployed app accepts it, and it bills real AI to the developer.
6
+ */
7
+ export interface DevAuth {
8
+ token: string;
9
+ userId: string;
10
+ serverUrl: string;
11
+ }
12
+ /**
13
+ * Load the developer's real ugly.bot session token from `~/.ugly-bot/auth.json`, for tests
14
+ * that must run against the DEPLOYED app / real AI (the token that `ugly.bot/verify`
15
+ * accepts). Returns `null` when the file is absent (fresh CI) so a spec can `test.skip()`
16
+ * instead of failing. The token is read at run time — NEVER hard-code or commit it.
17
+ *
18
+ * @example
19
+ * const auth = loadDevAuth();
20
+ * test.skip(!auth, 'no ~/.ugly-bot/auth.json — run `ugly-app login`');
21
+ * await authenticateReal(page, auth!, 'code.ugly.bot');
22
+ */
23
+ export declare function loadDevAuth(): DevAuth | null;
24
+ /**
25
+ * Inject a real ugly.bot session cookie for a DEPLOYED domain (e.g. `myapp.ugly.bot`) before
26
+ * navigating — the counterpart to {@link injectAuthCookie} (which targets `localhost`).
27
+ */
28
+ export declare function authenticateReal(page: Page, auth: DevAuth, domain: string): Promise<void>;
29
+ //# sourceMappingURL=dev-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-auth.d.ts","sourceRoot":"","sources":["../../src/playwright/dev-auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,IAAI,OAAO,GAAG,IAAI,CAQ5C;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/F"}
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Load the developer's real ugly.bot session token from `~/.ugly-bot/auth.json`, for tests
6
+ * that must run against the DEPLOYED app / real AI (the token that `ugly.bot/verify`
7
+ * accepts). Returns `null` when the file is absent (fresh CI) so a spec can `test.skip()`
8
+ * instead of failing. The token is read at run time — NEVER hard-code or commit it.
9
+ *
10
+ * @example
11
+ * const auth = loadDevAuth();
12
+ * test.skip(!auth, 'no ~/.ugly-bot/auth.json — run `ugly-app login`');
13
+ * await authenticateReal(page, auth!, 'code.ugly.bot');
14
+ */
15
+ export function loadDevAuth() {
16
+ try {
17
+ const raw = JSON.parse(readFileSync(join(homedir(), '.ugly-bot', 'auth.json'), 'utf8'));
18
+ if (!raw.token || !raw.userId)
19
+ return null;
20
+ return { token: raw.token, userId: raw.userId, serverUrl: raw.serverUrl ?? 'https://ugly.bot' };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * Inject a real ugly.bot session cookie for a DEPLOYED domain (e.g. `myapp.ugly.bot`) before
28
+ * navigating — the counterpart to {@link injectAuthCookie} (which targets `localhost`).
29
+ */
30
+ export async function authenticateReal(page, auth, domain) {
31
+ await page.context().addCookies([
32
+ { name: 'auth_token', value: auth.token, domain, path: '/', secure: true, sameSite: 'None' },
33
+ ]);
34
+ }
35
+ //# sourceMappingURL=dev-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-auth.js","sourceRoot":"","sources":["../../src/playwright/dev-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAcjC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAqB,CAAC;QAC5G,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAC3C,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,kBAAkB,EAAE,CAAC;IAClG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAU,EAAE,IAAa,EAAE,MAAc;IAC9E,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC;QAC9B,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE;KAC7F,CAAC,CAAC;AACL,CAAC"}
@@ -38,4 +38,8 @@ export interface ExpectCleanOptions {
38
38
  export declare function expectClean(page: Page, opts?: ExpectCleanOptions): Promise<void>;
39
39
  export { installUglyNativeMock } from './native-mock.js';
40
40
  export type { UglyNativeMock } from './native-mock.js';
41
+ export { installRealNative, makePathResolver } from './real-native.js';
42
+ export type { RealNativeOptions } from './real-native.js';
43
+ export { loadDevAuth, authenticateReal } from './dev-auth.js';
44
+ export type { DevAuth } from './dev-auth.js';
41
45
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,YAAY,EACV,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAIpC;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAIf;AAOD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7D;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC,CAU5B;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED,wBAAsB,SAAS,CAC7B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS,GAAG,KAAK,GAAG,SAAS,GACpC,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,YAAY,EACV,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAIpC;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAIf;AAOD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7D;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC,CAU5B;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED,wBAAsB,SAAS,CAC7B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS,GAAG,KAAK,GAAG,SAAS,GACpC,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9D,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
@@ -113,4 +113,6 @@ export async function expectClean(page, opts) {
113
113
  }
114
114
  }
115
115
  export { installUglyNativeMock } from './native-mock.js';
116
+ export { installRealNative, makePathResolver } from './real-native.js';
117
+ export { loadDevAuth, authenticateReal } from './dev-auth.js';
116
118
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAU,EACV,KAAa;IAEb,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC;QAC9B;YACE,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,WAAW;YACnB,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,KAAK;SAChB;KACF,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAgB,EAChB,KAAa;IAEb,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IACrC,MAAM,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC;AACd,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAU,EACV,IAA6B;IAE7B,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE;QACtC,OAAO,EAAE,IAAI,EAAE,SAAS,IAAI,MAAM;KACnC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAU;IAC1C,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;QAC9B,OAAO,MAAM,CAAC,iBAAiB,EAAE,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,KAAa;IAEb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAS,EAAE,EAAE;QAC/C,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CACb,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,EAAE,KAAK,CAAC,CAAC;IACV,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAU,EACV,QAA6B,EAC7B,IAA2B;IAE3B,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,QAAQ,EAAE,CAAC;IACjB,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,IAAI,GAAG,CAAC,CAAC;IACjD,OAAO,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAU,EACV,MAAqC;IAErC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAChC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAU,EACV,QAAgB;IAEhB,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAS,EAAE,EAAE;QAChC,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9D,CAAC;IACJ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACf,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,IAAyB;IAEzB,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,GAAG,CAAC;IACnC,MAAM,YAAY,GAAG,IAAI,EAAE,YAAY,IAAI,CAAC,CAAC;IAC7C,MAAM,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,KAAK,CAAC;IACnD,MAAM,uBAAuB,GAAG,IAAI,EAAE,uBAAuB,IAAI,KAAK,CAAC;IACvE,MAAM,qBAAqB,GAAG,IAAI,EAAE,qBAAqB,IAAI,KAAK,CAAC;IACnE,MAAM,gBAAgB,GAAG,IAAI,EAAE,gBAAgB,IAAI,CAAC,CAAC;IAErD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC;QAC9B,QAAQ,CAAC,IAAI,CACX,aAAa,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,aAAa,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAC7F,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QAC3C,QAAQ,CAAC,IAAI,CACX,aAAa,MAAM,CAAC,SAAS,CAAC,MAAM,MAAM,YAAY,SAAS,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CACpI,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,aAAa,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,QAAQ,CAAC,IAAI,CACX,aAAa,MAAM,CAAC,QAAQ;aACzB,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;aAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,uBAAuB,IAAI,MAAM,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrE,QAAQ,CAAC,IAAI,CACX,uBAAuB,MAAM,CAAC,kBAAkB;aAC7C,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC;aACtC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,qBAAqB,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvE,QAAQ,CAAC,IAAI,CACX,2BAA2B,MAAM,CAAC,QAAQ,CAAC,aAAa;aACrD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC;aAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,aAAa,EAC7B,CAAC,CACF,CAAC;IACF,IAAI,aAAa,GAAG,gBAAgB,EAAE,CAAC;QACrC,QAAQ,CAAC,IAAI,CACX,kBAAkB,aAAa,MAAM,gBAAgB,WAAW,MAAM,CAAC,UAAU,CAAC,MAAM,eAAe,CACxG,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;AACH,CAAC;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type { Page } from '@playwright/test';
2
+ import type { NativePlatform } from '../native/protocol.js';
3
+ /**
4
+ * Options for {@link installRealNative}.
5
+ */
6
+ export interface RealNativeOptions {
7
+ /** `window.UglyNative.platform`. Must be non-`'web'` for `isNativeAvailable()` to be
8
+ * true (many apps gate their desktop shell on it). Default `'desktop'`. */
9
+ platform?: NativePlatform;
10
+ /**
11
+ * Prepend the bundled toolchain (`node`, `git`, `rg`, `uv`, …) onto every spawn's PATH,
12
+ * bundled-first, so a real `run_command` / ripgrep search / indexer resolves the SAME
13
+ * deterministic binaries Studio ships — instead of whatever happens to be on the CI
14
+ * PATH. Requires the tools to already be installed under `BINARIES_ROOT`
15
+ * (`~/.ugly-studio/binaries`); pre-provision them with `ensureTools()` from
16
+ * `ugly-app/native/server/binaries` in your global setup, or point `UGLY_BINARIES_BASE`
17
+ * at a local mirror. Best-effort: if the bundle can't be resolved it logs and falls
18
+ * back to the ambient PATH. Default `false` (ambient PATH).
19
+ */
20
+ bundledBinaries?: boolean;
21
+ /** Extra env applied to every spawned process (merged over `process.env`). */
22
+ spawnEnv?: Record<string, string>;
23
+ }
24
+ /**
25
+ * Install a `window.UglyNative` backed by the REAL filesystem + real child processes,
26
+ * **confined to `root`** (a temp project dir). Unlike {@link installUglyNativeMock}
27
+ * (canned per-channel results), this lets an app actually read/write files and spawn
28
+ * processes — so a Playwright test can assert that code really changed ON DISK, and that
29
+ * native-backed panels/search run for real — without the Studio desktop shell.
30
+ *
31
+ * **Folder confinement (the sandbox):** every `fs.*` path and every spawn `cwd` is resolved
32
+ * through {@link makePathResolver} and a path that escapes `root` (`..`, an absolute path
33
+ * outside it, or a NUL byte) THROWS. This mirrors the production capability gate
34
+ * (`fs: 'scoped'`), so a test can prove the app never touches files outside the folder it
35
+ * was given. Point `root` at a fresh `fs.mkdtempSync(...)` per test.
36
+ *
37
+ * **Scope:** implements `fs.*`, `process.spawn` (+ its stdout/stderr/exit events), and
38
+ * auto-grants `permissions.*`. It does NOT run coding **tasks** (`task.start` forks a Node
39
+ * child that only the real Studio host / a `TaskManager` can spawn) — for task-driven
40
+ * flows use {@link installUglyNativeMock} and hand-drive `task.event:<id>` events, or run
41
+ * against a real host. Must be called BEFORE `page.goto()`.
42
+ *
43
+ * @example
44
+ * const root = fs.mkdtempSync(path.join(os.tmpdir(), 'eval-'));
45
+ * fs.writeFileSync(path.join(root, 'index.ts'), 'export const x = 1;\n');
46
+ * await installRealNative(page, root, { bundledBinaries: true });
47
+ * await page.goto('/');
48
+ * // …drive the app… then assert on disk:
49
+ * expect(fs.readFileSync(path.join(root, 'index.ts'), 'utf8')).toContain('x = 2');
50
+ */
51
+ export declare function installRealNative(page: Page, root: string, opts?: RealNativeOptions): Promise<void>;
52
+ /**
53
+ * Build a sandbox path resolver confined to `root`. Returns an absolute path inside `root`,
54
+ * or THROWS on any escape (`..` traversal, an absolute path outside `root`, or a NUL byte).
55
+ * Mirrors the production `resolveScopedPath` used by the `fs: 'scoped'` capability grant, so
56
+ * a test's confinement matches how a real scoped app is limited. Exported so callers can
57
+ * reuse the same check for their own bindings.
58
+ */
59
+ export declare function makePathResolver(root: string): (p: string) => string;
60
+ //# sourceMappingURL=real-native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"real-native.d.ts","sourceRoot":"","sources":["../../src/playwright/real-native.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;gFAC4E;IAC5E,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA8If;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAQpE"}
@@ -0,0 +1,178 @@
1
+ import * as fs from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { isAbsolute, join, resolve, sep } from 'node:path';
4
+ /**
5
+ * Install a `window.UglyNative` backed by the REAL filesystem + real child processes,
6
+ * **confined to `root`** (a temp project dir). Unlike {@link installUglyNativeMock}
7
+ * (canned per-channel results), this lets an app actually read/write files and spawn
8
+ * processes — so a Playwright test can assert that code really changed ON DISK, and that
9
+ * native-backed panels/search run for real — without the Studio desktop shell.
10
+ *
11
+ * **Folder confinement (the sandbox):** every `fs.*` path and every spawn `cwd` is resolved
12
+ * through {@link makePathResolver} and a path that escapes `root` (`..`, an absolute path
13
+ * outside it, or a NUL byte) THROWS. This mirrors the production capability gate
14
+ * (`fs: 'scoped'`), so a test can prove the app never touches files outside the folder it
15
+ * was given. Point `root` at a fresh `fs.mkdtempSync(...)` per test.
16
+ *
17
+ * **Scope:** implements `fs.*`, `process.spawn` (+ its stdout/stderr/exit events), and
18
+ * auto-grants `permissions.*`. It does NOT run coding **tasks** (`task.start` forks a Node
19
+ * child that only the real Studio host / a `TaskManager` can spawn) — for task-driven
20
+ * flows use {@link installUglyNativeMock} and hand-drive `task.event:<id>` events, or run
21
+ * against a real host. Must be called BEFORE `page.goto()`.
22
+ *
23
+ * @example
24
+ * const root = fs.mkdtempSync(path.join(os.tmpdir(), 'eval-'));
25
+ * fs.writeFileSync(path.join(root, 'index.ts'), 'export const x = 1;\n');
26
+ * await installRealNative(page, root, { bundledBinaries: true });
27
+ * await page.goto('/');
28
+ * // …drive the app… then assert on disk:
29
+ * expect(fs.readFileSync(path.join(root, 'index.ts'), 'utf8')).toContain('x = 2');
30
+ */
31
+ export async function installRealNative(page, root, opts = {}) {
32
+ const platform = opts.platform ?? 'desktop';
33
+ const rp = makePathResolver(root);
34
+ // Bundled toolchain PATH for real spawns (opt-in). Resolved once, best-effort.
35
+ let spawnEnv = { ...(opts.spawnEnv ?? {}) };
36
+ if (opts.bundledBinaries) {
37
+ try {
38
+ const { bundledEnv, BINARIES_ROOT } = (await import('../native/server/binaries/index.js'));
39
+ spawnEnv = { ...bundledEnv(BINARIES_ROOT), ...spawnEnv };
40
+ }
41
+ catch (e) {
42
+ // eslint-disable-next-line no-console
43
+ console.warn('[installRealNative] bundledBinaries requested but unavailable — using ambient PATH', e);
44
+ }
45
+ }
46
+ // ── Node-side bindings, all path-confined to `root`. ──────────────────────────────────
47
+ await page.exposeFunction('__nfsReadFile', (p) => fs.readFileSync(rp(p), 'utf8'));
48
+ await page.exposeFunction('__nfsWriteFile', (p, c) => {
49
+ fs.writeFileSync(rp(p), c ?? '');
50
+ return true;
51
+ });
52
+ await page.exposeFunction('__nfsReaddir', (p) => fs.readdirSync(rp(p), { withFileTypes: true }).map((d) => ({
53
+ name: d.name,
54
+ isDirectory: d.isDirectory(),
55
+ isFile: d.isFile(),
56
+ })));
57
+ await page.exposeFunction('__nfsMkdir', (p) => { fs.mkdirSync(rp(p), { recursive: true }); return true; });
58
+ await page.exposeFunction('__nfsRm', (p) => { fs.rmSync(rp(p), { recursive: true, force: true }); return true; });
59
+ await page.exposeFunction('__nfsRename', (a, b) => { fs.renameSync(rp(a), rp(b)); return true; });
60
+ await page.exposeFunction('__nfsStat', (p) => {
61
+ const s = fs.statSync(rp(p));
62
+ return { size: s.size, isDirectory: s.isDirectory(), isFile: s.isFile(), mtimeMs: s.mtimeMs };
63
+ });
64
+ await page.exposeFunction('__nfsExists', (p) => {
65
+ try {
66
+ return fs.existsSync(rp(p));
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ });
72
+ await page.exposeFunction('__nproc', (cmd, args, cwd, env) => new Promise((res) => {
73
+ let out = '';
74
+ let err = '';
75
+ // A cwd that escapes the sandbox is silently pinned back to root (never runs outside).
76
+ let safeCwd = root;
77
+ try {
78
+ safeCwd = cwd ? rp(cwd) : root;
79
+ }
80
+ catch {
81
+ safeCwd = root;
82
+ }
83
+ const proc = spawn(cmd, args ?? [], { cwd: safeCwd, env: { ...process.env, ...spawnEnv, ...(env ?? {}) } });
84
+ proc.stdout?.on('data', (d) => { out += d.toString(); });
85
+ proc.stderr?.on('data', (d) => { err += d.toString(); });
86
+ proc.on('error', (e) => res({ stdout: out, stderr: err + String(e), code: 1 }));
87
+ proc.on('close', (code) => res({ stdout: out, stderr: err, code }));
88
+ }));
89
+ // ── In-page UglyNative: route the unified protocol to the exposed Node bindings. ──────
90
+ // Process output is collected to completion in Node then replayed as stdout/exit events
91
+ // once the app has subscribed to the per-id channels.
92
+ await page.addInitScript((platformArg) => {
93
+ // The Node bindings exposed above, as seen from the page. `Record<string,unknown>`
94
+ // payloads keep field access typed without an `any` escape hatch.
95
+ const w = window;
96
+ const listeners = {};
97
+ let seq = 0;
98
+ const emit = (e, d) => (listeners[e] ?? []).forEach((cb) => cb(d));
99
+ const s = (v) => typeof v === 'string' ? v : typeof v === 'number' || typeof v === 'boolean' ? String(v) : '';
100
+ w.UglyNative = {
101
+ platform: platformArg,
102
+ subscribe: (e, cb) => {
103
+ (listeners[e] ??= []).push(cb);
104
+ return () => { listeners[e] = (listeners[e] ?? []).filter((f) => f !== cb); };
105
+ },
106
+ invoke: async (ch, payload = {}) => {
107
+ const p = payload;
108
+ switch (ch) {
109
+ case 'permissions.request':
110
+ case 'permissions.query':
111
+ return { granted: { fs: 'full', process: 'full' } };
112
+ case 'fs.readFile':
113
+ return { content: await w.__nfsReadFile(s(p.path)) };
114
+ case 'fs.writeFile':
115
+ await w.__nfsWriteFile(s(p.path), s(p.content));
116
+ return {};
117
+ case 'fs.readdir':
118
+ return { entries: await w.__nfsReaddir(s(p.path)) };
119
+ case 'fs.mkdir':
120
+ await w.__nfsMkdir(s(p.path));
121
+ return {};
122
+ case 'fs.rm':
123
+ await w.__nfsRm(s(p.path));
124
+ return {};
125
+ case 'fs.rename':
126
+ await w.__nfsRename(s(p.from ?? p.oldPath), s(p.to ?? p.newPath));
127
+ return {};
128
+ case 'fs.stat':
129
+ return await w.__nfsStat(s(p.path));
130
+ case 'fs.exists':
131
+ return { exists: await w.__nfsExists(s(p.path)) };
132
+ case 'fs.realpath':
133
+ return { path: s(p.path) };
134
+ case 'process.spawn': {
135
+ const id = `p${seq++}`;
136
+ const args = Array.isArray(p.args) ? p.args.map(s) : [];
137
+ const env = (p.env && typeof p.env === 'object' ? p.env : {});
138
+ void w
139
+ .__nproc(s(p.cmd), args, p.cwd ? s(p.cwd) : undefined, env)
140
+ .then((r) => setTimeout(() => {
141
+ if (r.stdout)
142
+ emit('process.stdout:' + id, { chunk: r.stdout });
143
+ if (r.stderr)
144
+ emit('process.stderr:' + id, { chunk: r.stderr });
145
+ emit('process.exit:' + id, { code: r.code });
146
+ }, 0));
147
+ return { id, pid: 1000 + seq };
148
+ }
149
+ case 'process.write':
150
+ case 'process.closeStdin':
151
+ case 'process.kill':
152
+ return {};
153
+ default:
154
+ return {};
155
+ }
156
+ },
157
+ };
158
+ }, platform);
159
+ }
160
+ /**
161
+ * Build a sandbox path resolver confined to `root`. Returns an absolute path inside `root`,
162
+ * or THROWS on any escape (`..` traversal, an absolute path outside `root`, or a NUL byte).
163
+ * Mirrors the production `resolveScopedPath` used by the `fs: 'scoped'` capability grant, so
164
+ * a test's confinement matches how a real scoped app is limited. Exported so callers can
165
+ * reuse the same check for their own bindings.
166
+ */
167
+ export function makePathResolver(root) {
168
+ const base = resolve(root);
169
+ return (p) => {
170
+ if (typeof p === 'string' && p.includes('\0'))
171
+ throw new Error(`path contains NUL: ${p}`);
172
+ const r = resolve(isAbsolute(p) ? p : join(base, p));
173
+ if (r !== base && !r.startsWith(base + sep))
174
+ throw new Error(`path escapes project root: ${p}`);
175
+ return r;
176
+ };
177
+ }
178
+ //# sourceMappingURL=real-native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"real-native.js","sourceRoot":"","sources":["../../src/playwright/real-native.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SqlitePipeline.d.ts","sourceRoot":"","sources":["../../../src/server/sqlite/SqlitePipeline.ts"],"names":[],"mappings":"AAgFA,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAuFpC"}
1
+ {"version":3,"file":"SqlitePipeline.d.ts","sourceRoot":"","sources":["../../../src/server/sqlite/SqlitePipeline.ts"],"names":[],"mappings":"AAgFA,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAiIpC"}
@@ -115,6 +115,50 @@ export function translatePipeline(collection, pipeline, options) {
115
115
  parts.grouped = false;
116
116
  break;
117
117
  }
118
+ case '$project': {
119
+ // Reshape `data` to the projected field set, keeping the row shape the
120
+ // DocStore hydrates (`_id, data, created, updated, version`). Inclusion
121
+ // ({field:1}) rebuilds `data` from only the listed keys; exclusion
122
+ // ({field:0}) strips the listed keys. Nested objects/arrays are
123
+ // preserved (not double-encoded) via the json_each `type` guard. A
124
+ // following $sort/$match still reads the base `data`, so it may reference
125
+ // projected-away fields — an intentional, documented divergence from
126
+ // Mongo that matches how these pipelines are written (project then sort
127
+ // on a projected field).
128
+ if (parts.grouped)
129
+ Object.assign(parts, wrapAsSubquery(parts));
130
+ const proj = value;
131
+ const entries = Object.entries(proj);
132
+ const include = entries.filter(([, v]) => v === 1 || v === true).map(([k]) => k);
133
+ const exclude = entries.filter(([, v]) => v === 0 || v === false).map(([k]) => k);
134
+ if (include.length > 0 && exclude.length > 0) {
135
+ throw new Error('[SqlitePipeline] $project cannot mix inclusion and exclusion');
136
+ }
137
+ const dataCol = parts.from.includes('_sub') ? '"data"' : 'data';
138
+ const asName = (k) => {
139
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k)) {
140
+ throw new Error(`[SqlitePipeline] $project field not a safe name: ${k}`);
141
+ }
142
+ return k;
143
+ };
144
+ let dataExpr;
145
+ if (include.length > 0) {
146
+ const fields = include.filter((k) => !TOP_LEVEL_COLUMNS.has(k)).map(asName);
147
+ dataExpr = fields.length
148
+ ? `(SELECT json_group_object(je.key, ` +
149
+ `CASE WHEN je.type IN ('object','array') THEN json(je.value) ELSE je.value END) ` +
150
+ `FROM json_each(${dataCol}) je WHERE je.key IN (${fields.map((f) => `'${f}'`).join(', ')}))`
151
+ : `'{}'`;
152
+ }
153
+ else {
154
+ const fields = exclude.filter((k) => !TOP_LEVEL_COLUMNS.has(k)).map(asName);
155
+ dataExpr = fields.length
156
+ ? `json_remove(${dataCol}, ${fields.map((f) => `'$.${f}'`).join(', ')})`
157
+ : dataCol;
158
+ }
159
+ parts.select = `_id, ${dataExpr} AS data, created, updated, version`;
160
+ break;
161
+ }
118
162
  case '$group': {
119
163
  const group = value;
120
164
  const groupId = group._id;
@@ -1 +1 @@
1
- {"version":3,"file":"SqlitePipeline.js","sourceRoot":"","sources":["../../../src/server/sqlite/SqlitePipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SqlitePipeline.js","sourceRoot":"","sources":["../../../src/server/sqlite/SqlitePipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * menuChrome — the shared look for popup/context menus, as framework-agnostic
3
+ * HTML/CSS strings so it can be used by native shells (ugly-studio's Electron
4
+ * context menu + ugly.bot menu) as well as by web/React surfaces (which style
5
+ * their own menu markup from the SAME tokens below). One source of truth for
6
+ * menu typography, spacing, icons, and hover.
7
+ *
8
+ * Usage: drop MENU_FONTS in <head>, then MENU_TOKENS + MENU_CSS in a <style>.
9
+ * Build rows as `<div class="mi"><span class="ic">…icon…</span><span class="lbl">
10
+ * Label</span><span class="acc">⌘X</span></div>` and dividers as `<div class="sep">`.
11
+ */
12
+ /** Webfonts — IBM Plex Mono (rows) + Bricolage (display). */
13
+ export declare const MENU_FONTS: string;
14
+ /** Industrial tokens (light + dark) shared by every menu. */
15
+ export declare const MENU_TOKENS = ":root{\n --panel2:#ece6da;--panel:#f2ede3;--dock:#e0d9cb;--line:#d3cab9;--line2:#c0b6a3;\n --ink:#23201a;--dim:#6c6457;--faint:#a09784;--orange:#e8590c;--orange-d:#c2430a;--orange-soft:#f7e7d6;\n --shadow:0 24px 52px -20px rgba(20,12,4,.6),0 8px 20px -10px rgba(20,12,4,.4);\n --disp:'Bricolage Grotesque',ui-sans-serif,system-ui,sans-serif;\n --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,monospace;\n}\n@media (prefers-color-scheme:dark){:root{\n --panel2:#241e18;--panel:#1e1915;--dock:#13100c;--line:#322a22;--line2:#473a2d;\n --ink:#efe7da;--dim:#a99a86;--faint:#6f6453;--orange:#ff6a1f;--orange-d:#d44e0a;--orange-soft:rgba(255,106,31,.15);\n --shadow:0 24px 52px -20px rgba(0,0,0,.62),0 8px 20px -10px rgba(0,0,0,.5);\n}}";
16
+ /**
17
+ * The shared card + row primitives. `.menu` is the card; `.mi` a row (icon +
18
+ * label + trailing accel/chevron); `.sep` a divider. Icons work either wrapped
19
+ * in `.ic` or as a direct `<svg>` child.
20
+ */
21
+ export declare const MENU_CSS = ".menu{background:var(--panel2);border:1px solid var(--line2);border-radius:13px;box-shadow:var(--shadow);padding:7px}\n.mi{display:flex;align-items:center;gap:11px;width:100%;padding:8px 9px;border:none;background:none;border-radius:8px;color:var(--ink);font-family:var(--mono);font-size:11.5px;line-height:1.35;text-align:left;white-space:nowrap;cursor:pointer}\n.mi:hover,.mi.active{background:var(--orange-soft);color:var(--orange)}\n.mi .ic{width:15px;height:15px;flex:none;display:grid;place-items:center;color:var(--dim)}\n.mi .ic svg{width:15px;height:15px}\n.mi:hover .ic,.mi.active .ic{color:var(--orange)}\n.mi>svg{width:15px;height:15px;flex:none;color:var(--dim)}\n.mi:hover>svg,.mi.active>svg{color:var(--orange)}\n.mi .lbl{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}\n.mi .acc,.mi .chev{margin-left:auto;flex:none;color:var(--faint);font-size:10px;letter-spacing:.02em;display:grid}\n.mi .chev svg{width:14px;height:14px;display:block}\n.mi:hover .acc,.mi.active .acc,.mi:hover .chev,.mi.active .chev{color:var(--orange-d)}\n.mi.dis{color:var(--faint);cursor:default}.mi.dis:hover{background:none}.mi.dis .ic{color:var(--faint)}\n.sep{height:1px;background:var(--line);margin:6px 5px}";
22
+ //# sourceMappingURL=menuChrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menuChrome.d.ts","sourceRoot":"","sources":["../../src/shared/menuChrome.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,UAAU,QAEyJ,CAAC;AAEjL,6DAA6D;AAC7D,eAAO,MAAM,WAAW,2uBAWrB,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,QAAQ,ksCAakC,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * menuChrome — the shared look for popup/context menus, as framework-agnostic
3
+ * HTML/CSS strings so it can be used by native shells (ugly-studio's Electron
4
+ * context menu + ugly.bot menu) as well as by web/React surfaces (which style
5
+ * their own menu markup from the SAME tokens below). One source of truth for
6
+ * menu typography, spacing, icons, and hover.
7
+ *
8
+ * Usage: drop MENU_FONTS in <head>, then MENU_TOKENS + MENU_CSS in a <style>.
9
+ * Build rows as `<div class="mi"><span class="ic">…icon…</span><span class="lbl">
10
+ * Label</span><span class="acc">⌘X</span></div>` and dividers as `<div class="sep">`.
11
+ */
12
+ /** Webfonts — IBM Plex Mono (rows) + Bricolage (display). */
13
+ export const MENU_FONTS = '<link rel="preconnect" href="https://fonts.googleapis.com">' +
14
+ '<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">';
15
+ /** Industrial tokens (light + dark) shared by every menu. */
16
+ export const MENU_TOKENS = `:root{
17
+ --panel2:#ece6da;--panel:#f2ede3;--dock:#e0d9cb;--line:#d3cab9;--line2:#c0b6a3;
18
+ --ink:#23201a;--dim:#6c6457;--faint:#a09784;--orange:#e8590c;--orange-d:#c2430a;--orange-soft:#f7e7d6;
19
+ --shadow:0 24px 52px -20px rgba(20,12,4,.6),0 8px 20px -10px rgba(20,12,4,.4);
20
+ --disp:'Bricolage Grotesque',ui-sans-serif,system-ui,sans-serif;
21
+ --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,monospace;
22
+ }
23
+ @media (prefers-color-scheme:dark){:root{
24
+ --panel2:#241e18;--panel:#1e1915;--dock:#13100c;--line:#322a22;--line2:#473a2d;
25
+ --ink:#efe7da;--dim:#a99a86;--faint:#6f6453;--orange:#ff6a1f;--orange-d:#d44e0a;--orange-soft:rgba(255,106,31,.15);
26
+ --shadow:0 24px 52px -20px rgba(0,0,0,.62),0 8px 20px -10px rgba(0,0,0,.5);
27
+ }}`;
28
+ /**
29
+ * The shared card + row primitives. `.menu` is the card; `.mi` a row (icon +
30
+ * label + trailing accel/chevron); `.sep` a divider. Icons work either wrapped
31
+ * in `.ic` or as a direct `<svg>` child.
32
+ */
33
+ export const MENU_CSS = `.menu{background:var(--panel2);border:1px solid var(--line2);border-radius:13px;box-shadow:var(--shadow);padding:7px}
34
+ .mi{display:flex;align-items:center;gap:11px;width:100%;padding:8px 9px;border:none;background:none;border-radius:8px;color:var(--ink);font-family:var(--mono);font-size:11.5px;line-height:1.35;text-align:left;white-space:nowrap;cursor:pointer}
35
+ .mi:hover,.mi.active{background:var(--orange-soft);color:var(--orange)}
36
+ .mi .ic{width:15px;height:15px;flex:none;display:grid;place-items:center;color:var(--dim)}
37
+ .mi .ic svg{width:15px;height:15px}
38
+ .mi:hover .ic,.mi.active .ic{color:var(--orange)}
39
+ .mi>svg{width:15px;height:15px;flex:none;color:var(--dim)}
40
+ .mi:hover>svg,.mi.active>svg{color:var(--orange)}
41
+ .mi .lbl{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
42
+ .mi .acc,.mi .chev{margin-left:auto;flex:none;color:var(--faint);font-size:10px;letter-spacing:.02em;display:grid}
43
+ .mi .chev svg{width:14px;height:14px;display:block}
44
+ .mi:hover .acc,.mi.active .acc,.mi:hover .chev,.mi.active .chev{color:var(--orange-d)}
45
+ .mi.dis{color:var(--faint);cursor:default}.mi.dis:hover{background:none}.mi.dis .ic{color:var(--faint)}
46
+ .sep{height:1px;background:var(--line);margin:6px 5px}`;
47
+ //# sourceMappingURL=menuChrome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menuChrome.js","sourceRoot":"","sources":["../../src/shared/menuChrome.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6DAA6D;AAC7D,MAAM,CAAC,MAAM,UAAU,GACrB,6DAA6D;IAC7D,8KAA8K,CAAC;AAEjL,6DAA6D;AAC7D,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;GAWxB,CAAC;AAEJ;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;uDAa+B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.859",
3
+ "version": "0.1.861",
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": [
@@ -21,6 +21,7 @@
21
21
  "./server/ssr": "./dist/server/adapter/workers/createSsrRender.js",
22
22
  "./server/publishFrame": "./dist/server/adapter/publishFrame.js",
23
23
  "./shared": "./dist/shared/index.js",
24
+ "./menu-chrome": "./dist/shared/menuChrome.js",
24
25
  "./cli/publish": "./dist/cli/publish/orchestrator.js",
25
26
  "./cli/publish/types": "./dist/cli/publish/types.js",
26
27
  "./cli/publish/secret-storage": "./dist/cli/publish/secret-storage.js",
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.859";
2
+ export const CLI_VERSION = "0.1.861";
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import type { Page } from '@playwright/test';
5
+
6
+ /**
7
+ * The developer's live ugly.bot session, persisted by Ugly Studio / `ugly-app login` at
8
+ * `~/.ugly-bot/auth.json` as `{ token, userId, serverUrl }`. This is a REAL ugly.bot-issued
9
+ * JWT — a Mode-A deployed app accepts it, and it bills real AI to the developer.
10
+ */
11
+ export interface DevAuth {
12
+ token: string;
13
+ userId: string;
14
+ serverUrl: string;
15
+ }
16
+
17
+ /**
18
+ * Load the developer's real ugly.bot session token from `~/.ugly-bot/auth.json`, for tests
19
+ * that must run against the DEPLOYED app / real AI (the token that `ugly.bot/verify`
20
+ * accepts). Returns `null` when the file is absent (fresh CI) so a spec can `test.skip()`
21
+ * instead of failing. The token is read at run time — NEVER hard-code or commit it.
22
+ *
23
+ * @example
24
+ * const auth = loadDevAuth();
25
+ * test.skip(!auth, 'no ~/.ugly-bot/auth.json — run `ugly-app login`');
26
+ * await authenticateReal(page, auth!, 'code.ugly.bot');
27
+ */
28
+ export function loadDevAuth(): DevAuth | null {
29
+ try {
30
+ const raw = JSON.parse(readFileSync(join(homedir(), '.ugly-bot', 'auth.json'), 'utf8')) as Partial<DevAuth>;
31
+ if (!raw.token || !raw.userId) return null;
32
+ return { token: raw.token, userId: raw.userId, serverUrl: raw.serverUrl ?? 'https://ugly.bot' };
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Inject a real ugly.bot session cookie for a DEPLOYED domain (e.g. `myapp.ugly.bot`) before
40
+ * navigating — the counterpart to {@link injectAuthCookie} (which targets `localhost`).
41
+ */
42
+ export async function authenticateReal(page: Page, auth: DevAuth, domain: string): Promise<void> {
43
+ await page.context().addCookies([
44
+ { name: 'auth_token', value: auth.token, domain, path: '/', secure: true, sameSite: 'None' },
45
+ ]);
46
+ }
@@ -205,3 +205,7 @@ export async function expectClean(
205
205
 
206
206
  export { installUglyNativeMock } from './native-mock.js';
207
207
  export type { UglyNativeMock } from './native-mock.js';
208
+ export { installRealNative, makePathResolver } from './real-native.js';
209
+ export type { RealNativeOptions } from './real-native.js';
210
+ export { loadDevAuth, authenticateReal } from './dev-auth.js';
211
+ export type { DevAuth } from './dev-auth.js';
@@ -0,0 +1,219 @@
1
+ import * as fs from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { isAbsolute, join, resolve, sep } from 'node:path';
4
+ import type { Page } from '@playwright/test';
5
+ import type { NativePlatform } from '../native/protocol.js';
6
+
7
+ /**
8
+ * Options for {@link installRealNative}.
9
+ */
10
+ export interface RealNativeOptions {
11
+ /** `window.UglyNative.platform`. Must be non-`'web'` for `isNativeAvailable()` to be
12
+ * true (many apps gate their desktop shell on it). Default `'desktop'`. */
13
+ platform?: NativePlatform;
14
+ /**
15
+ * Prepend the bundled toolchain (`node`, `git`, `rg`, `uv`, …) onto every spawn's PATH,
16
+ * bundled-first, so a real `run_command` / ripgrep search / indexer resolves the SAME
17
+ * deterministic binaries Studio ships — instead of whatever happens to be on the CI
18
+ * PATH. Requires the tools to already be installed under `BINARIES_ROOT`
19
+ * (`~/.ugly-studio/binaries`); pre-provision them with `ensureTools()` from
20
+ * `ugly-app/native/server/binaries` in your global setup, or point `UGLY_BINARIES_BASE`
21
+ * at a local mirror. Best-effort: if the bundle can't be resolved it logs and falls
22
+ * back to the ambient PATH. Default `false` (ambient PATH).
23
+ */
24
+ bundledBinaries?: boolean;
25
+ /** Extra env applied to every spawned process (merged over `process.env`). */
26
+ spawnEnv?: Record<string, string>;
27
+ }
28
+
29
+ /**
30
+ * Install a `window.UglyNative` backed by the REAL filesystem + real child processes,
31
+ * **confined to `root`** (a temp project dir). Unlike {@link installUglyNativeMock}
32
+ * (canned per-channel results), this lets an app actually read/write files and spawn
33
+ * processes — so a Playwright test can assert that code really changed ON DISK, and that
34
+ * native-backed panels/search run for real — without the Studio desktop shell.
35
+ *
36
+ * **Folder confinement (the sandbox):** every `fs.*` path and every spawn `cwd` is resolved
37
+ * through {@link makePathResolver} and a path that escapes `root` (`..`, an absolute path
38
+ * outside it, or a NUL byte) THROWS. This mirrors the production capability gate
39
+ * (`fs: 'scoped'`), so a test can prove the app never touches files outside the folder it
40
+ * was given. Point `root` at a fresh `fs.mkdtempSync(...)` per test.
41
+ *
42
+ * **Scope:** implements `fs.*`, `process.spawn` (+ its stdout/stderr/exit events), and
43
+ * auto-grants `permissions.*`. It does NOT run coding **tasks** (`task.start` forks a Node
44
+ * child that only the real Studio host / a `TaskManager` can spawn) — for task-driven
45
+ * flows use {@link installUglyNativeMock} and hand-drive `task.event:<id>` events, or run
46
+ * against a real host. Must be called BEFORE `page.goto()`.
47
+ *
48
+ * @example
49
+ * const root = fs.mkdtempSync(path.join(os.tmpdir(), 'eval-'));
50
+ * fs.writeFileSync(path.join(root, 'index.ts'), 'export const x = 1;\n');
51
+ * await installRealNative(page, root, { bundledBinaries: true });
52
+ * await page.goto('/');
53
+ * // …drive the app… then assert on disk:
54
+ * expect(fs.readFileSync(path.join(root, 'index.ts'), 'utf8')).toContain('x = 2');
55
+ */
56
+ export async function installRealNative(
57
+ page: Page,
58
+ root: string,
59
+ opts: RealNativeOptions = {},
60
+ ): Promise<void> {
61
+ const platform = opts.platform ?? 'desktop';
62
+ const rp = makePathResolver(root);
63
+
64
+ // Bundled toolchain PATH for real spawns (opt-in). Resolved once, best-effort.
65
+ let spawnEnv: Record<string, string> = { ...(opts.spawnEnv ?? {}) };
66
+ if (opts.bundledBinaries) {
67
+ try {
68
+ const { bundledEnv, BINARIES_ROOT } = (await import(
69
+ '../native/server/binaries/index.js'
70
+ )) as { bundledEnv: (root: string) => Record<string, string>; BINARIES_ROOT: string };
71
+ spawnEnv = { ...bundledEnv(BINARIES_ROOT), ...spawnEnv };
72
+ } catch (e) {
73
+ // eslint-disable-next-line no-console
74
+ console.warn('[installRealNative] bundledBinaries requested but unavailable — using ambient PATH', e);
75
+ }
76
+ }
77
+
78
+ // ── Node-side bindings, all path-confined to `root`. ──────────────────────────────────
79
+ await page.exposeFunction('__nfsReadFile', (p: string) => fs.readFileSync(rp(p), 'utf8'));
80
+ await page.exposeFunction('__nfsWriteFile', (p: string, c: string) => {
81
+ fs.writeFileSync(rp(p), c ?? '');
82
+ return true;
83
+ });
84
+ await page.exposeFunction('__nfsReaddir', (p: string) =>
85
+ fs.readdirSync(rp(p), { withFileTypes: true }).map((d) => ({
86
+ name: d.name,
87
+ isDirectory: d.isDirectory(),
88
+ isFile: d.isFile(),
89
+ })),
90
+ );
91
+ await page.exposeFunction('__nfsMkdir', (p: string) => { fs.mkdirSync(rp(p), { recursive: true }); return true; });
92
+ await page.exposeFunction('__nfsRm', (p: string) => { fs.rmSync(rp(p), { recursive: true, force: true }); return true; });
93
+ await page.exposeFunction('__nfsRename', (a: string, b: string) => { fs.renameSync(rp(a), rp(b)); return true; });
94
+ await page.exposeFunction('__nfsStat', (p: string) => {
95
+ const s = fs.statSync(rp(p));
96
+ return { size: s.size, isDirectory: s.isDirectory(), isFile: s.isFile(), mtimeMs: s.mtimeMs };
97
+ });
98
+ await page.exposeFunction('__nfsExists', (p: string) => {
99
+ try { return fs.existsSync(rp(p)); } catch { return false; }
100
+ });
101
+ await page.exposeFunction(
102
+ '__nproc',
103
+ (cmd: string, args: string[], cwd: string | undefined, env: Record<string, string> | undefined) =>
104
+ new Promise<{ stdout: string; stderr: string; code: number | null }>((res) => {
105
+ let out = '';
106
+ let err = '';
107
+ // A cwd that escapes the sandbox is silently pinned back to root (never runs outside).
108
+ let safeCwd = root;
109
+ try { safeCwd = cwd ? rp(cwd) : root; } catch { safeCwd = root; }
110
+ const proc = spawn(cmd, args ?? [], { cwd: safeCwd, env: { ...process.env, ...spawnEnv, ...(env ?? {}) } });
111
+ proc.stdout?.on('data', (d: Buffer) => { out += d.toString(); });
112
+ proc.stderr?.on('data', (d: Buffer) => { err += d.toString(); });
113
+ proc.on('error', (e) => res({ stdout: out, stderr: err + String(e), code: 1 }));
114
+ proc.on('close', (code) => res({ stdout: out, stderr: err, code }));
115
+ }),
116
+ );
117
+
118
+ // ── In-page UglyNative: route the unified protocol to the exposed Node bindings. ──────
119
+ // Process output is collected to completion in Node then replayed as stdout/exit events
120
+ // once the app has subscribed to the per-id channels.
121
+ await page.addInitScript((platformArg) => {
122
+ type Cb = (d: unknown) => void;
123
+ interface ProcResult { stdout: string; stderr: string; code: number | null }
124
+ // The Node bindings exposed above, as seen from the page. `Record<string,unknown>`
125
+ // payloads keep field access typed without an `any` escape hatch.
126
+ const w = window as unknown as {
127
+ __nfsReadFile: (p: string) => Promise<string>;
128
+ __nfsWriteFile: (p: string, c: string) => Promise<boolean>;
129
+ __nfsReaddir: (p: string) => Promise<{ name: string; isDirectory: boolean; isFile: boolean }[]>;
130
+ __nfsMkdir: (p: string) => Promise<boolean>;
131
+ __nfsRm: (p: string) => Promise<boolean>;
132
+ __nfsRename: (a: string, b: string) => Promise<boolean>;
133
+ __nfsStat: (p: string) => Promise<{ size: number; isDirectory: boolean; isFile: boolean; mtimeMs: number }>;
134
+ __nfsExists: (p: string) => Promise<boolean>;
135
+ __nproc: (cmd: string, args: string[], cwd: string | undefined, env: Record<string, string> | undefined) => Promise<ProcResult>;
136
+ UglyNative: unknown;
137
+ };
138
+ const listeners: Record<string, Cb[]> = {};
139
+ let seq = 0;
140
+ const emit = (e: string, d: unknown): void => (listeners[e] ?? []).forEach((cb) => cb(d));
141
+ const s = (v: unknown): string =>
142
+ typeof v === 'string' ? v : typeof v === 'number' || typeof v === 'boolean' ? String(v) : '';
143
+ w.UglyNative = {
144
+ platform: platformArg,
145
+ subscribe: (e: string, cb: Cb) => {
146
+ (listeners[e] ??= []).push(cb);
147
+ return () => { listeners[e] = (listeners[e] ?? []).filter((f) => f !== cb); };
148
+ },
149
+ invoke: async (ch: string, payload: Record<string, unknown> = {}) => {
150
+ const p = payload;
151
+ switch (ch) {
152
+ case 'permissions.request':
153
+ case 'permissions.query':
154
+ return { granted: { fs: 'full', process: 'full' } };
155
+ case 'fs.readFile':
156
+ return { content: await w.__nfsReadFile(s(p.path)) };
157
+ case 'fs.writeFile':
158
+ await w.__nfsWriteFile(s(p.path), s(p.content));
159
+ return {};
160
+ case 'fs.readdir':
161
+ return { entries: await w.__nfsReaddir(s(p.path)) };
162
+ case 'fs.mkdir':
163
+ await w.__nfsMkdir(s(p.path));
164
+ return {};
165
+ case 'fs.rm':
166
+ await w.__nfsRm(s(p.path));
167
+ return {};
168
+ case 'fs.rename':
169
+ await w.__nfsRename(s(p.from ?? p.oldPath), s(p.to ?? p.newPath));
170
+ return {};
171
+ case 'fs.stat':
172
+ return await w.__nfsStat(s(p.path));
173
+ case 'fs.exists':
174
+ return { exists: await w.__nfsExists(s(p.path)) };
175
+ case 'fs.realpath':
176
+ return { path: s(p.path) };
177
+ case 'process.spawn': {
178
+ const id = `p${seq++}`;
179
+ const args = Array.isArray(p.args) ? p.args.map(s) : [];
180
+ const env = (p.env && typeof p.env === 'object' ? p.env : {}) as Record<string, string>;
181
+ void w
182
+ .__nproc(s(p.cmd), args, p.cwd ? s(p.cwd) : undefined, env)
183
+ .then((r: ProcResult) =>
184
+ setTimeout(() => {
185
+ if (r.stdout) emit('process.stdout:' + id, { chunk: r.stdout });
186
+ if (r.stderr) emit('process.stderr:' + id, { chunk: r.stderr });
187
+ emit('process.exit:' + id, { code: r.code });
188
+ }, 0),
189
+ );
190
+ return { id, pid: 1000 + seq };
191
+ }
192
+ case 'process.write':
193
+ case 'process.closeStdin':
194
+ case 'process.kill':
195
+ return {};
196
+ default:
197
+ return {};
198
+ }
199
+ },
200
+ };
201
+ }, platform);
202
+ }
203
+
204
+ /**
205
+ * Build a sandbox path resolver confined to `root`. Returns an absolute path inside `root`,
206
+ * or THROWS on any escape (`..` traversal, an absolute path outside `root`, or a NUL byte).
207
+ * Mirrors the production `resolveScopedPath` used by the `fs: 'scoped'` capability grant, so
208
+ * a test's confinement matches how a real scoped app is limited. Exported so callers can
209
+ * reuse the same check for their own bindings.
210
+ */
211
+ export function makePathResolver(root: string): (p: string) => string {
212
+ const base = resolve(root);
213
+ return (p: string): string => {
214
+ if (typeof p === 'string' && p.includes('\0')) throw new Error(`path contains NUL: ${p}`);
215
+ const r = resolve(isAbsolute(p) ? p : join(base, p));
216
+ if (r !== base && !r.startsWith(base + sep)) throw new Error(`path escapes project root: ${p}`);
217
+ return r;
218
+ };
219
+ }
@@ -26,6 +26,43 @@ describe('SqlitePipeline.translatePipeline', () => {
26
26
  expect(sql).not.toContain('GROUP BY');
27
27
  });
28
28
 
29
+ it('$project inclusion rebuilds data from only the listed keys (nested-safe) + keeps top-level cols', () => {
30
+ const { sql } = translatePipeline('c', [
31
+ { $project: { _id: 1, title: 1, tags: 1 } },
32
+ ]);
33
+ expect(sql).toContain('_id,');
34
+ expect(sql).toContain('created, updated, version');
35
+ expect(sql).toContain("json_each(data) je WHERE je.key IN ('title', 'tags')");
36
+ // nested objects/arrays preserved (not double-encoded)
37
+ expect(sql).toContain("CASE WHEN je.type IN ('object','array') THEN json(je.value) ELSE je.value END");
38
+ });
39
+
40
+ it('$project exclusion strips listed keys via json_remove', () => {
41
+ const { sql } = translatePipeline('c', [{ $project: { text: 0, markdown: 0 } }]);
42
+ expect(sql).toContain("json_remove(data, '$.text', '$.markdown') AS data");
43
+ });
44
+
45
+ it('$project then $sort on a projected field still sorts (reads base data)', () => {
46
+ const { sql } = translatePipeline('c', [
47
+ { $project: { title: 1, tags: 1 } },
48
+ { $sort: { title: 1 } },
49
+ ]);
50
+ expect(sql).toContain('json_each(data)');
51
+ expect(sql).toContain(`ORDER BY (CAST(json_extract(data,'$.title') AS TEXT)`);
52
+ });
53
+
54
+ it('$project rejects mixed inclusion/exclusion', () => {
55
+ expect(() => translatePipeline('c', [{ $project: { title: 1, text: 0 } }])).toThrow(
56
+ /cannot mix inclusion and exclusion/,
57
+ );
58
+ });
59
+
60
+ it('$project rejects an unsafe field name', () => {
61
+ expect(() =>
62
+ translatePipeline('c', [{ $project: { ["bad'name"]: 1 } as Record<string, number> }]),
63
+ ).toThrow(/safe name/);
64
+ });
65
+
29
66
  it('$count wraps the query in a COUNT(*) subquery', () => {
30
67
  const { sql } = translatePipeline('c', [
31
68
  { $match: { kind: 'a' } },
@@ -133,6 +133,48 @@ export function translatePipeline(
133
133
  parts.grouped = false;
134
134
  break;
135
135
  }
136
+ case '$project': {
137
+ // Reshape `data` to the projected field set, keeping the row shape the
138
+ // DocStore hydrates (`_id, data, created, updated, version`). Inclusion
139
+ // ({field:1}) rebuilds `data` from only the listed keys; exclusion
140
+ // ({field:0}) strips the listed keys. Nested objects/arrays are
141
+ // preserved (not double-encoded) via the json_each `type` guard. A
142
+ // following $sort/$match still reads the base `data`, so it may reference
143
+ // projected-away fields — an intentional, documented divergence from
144
+ // Mongo that matches how these pipelines are written (project then sort
145
+ // on a projected field).
146
+ if (parts.grouped) Object.assign(parts, wrapAsSubquery(parts));
147
+ const proj = value as Record<string, number | boolean>;
148
+ const entries = Object.entries(proj);
149
+ const include = entries.filter(([, v]) => v === 1 || v === true).map(([k]) => k);
150
+ const exclude = entries.filter(([, v]) => v === 0 || v === false).map(([k]) => k);
151
+ if (include.length > 0 && exclude.length > 0) {
152
+ throw new Error('[SqlitePipeline] $project cannot mix inclusion and exclusion');
153
+ }
154
+ const dataCol = parts.from.includes('_sub') ? '"data"' : 'data';
155
+ const asName = (k: string): string => {
156
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k)) {
157
+ throw new Error(`[SqlitePipeline] $project field not a safe name: ${k}`);
158
+ }
159
+ return k;
160
+ };
161
+ let dataExpr: string;
162
+ if (include.length > 0) {
163
+ const fields = include.filter((k) => !TOP_LEVEL_COLUMNS.has(k)).map(asName);
164
+ dataExpr = fields.length
165
+ ? `(SELECT json_group_object(je.key, ` +
166
+ `CASE WHEN je.type IN ('object','array') THEN json(je.value) ELSE je.value END) ` +
167
+ `FROM json_each(${dataCol}) je WHERE je.key IN (${fields.map((f) => `'${f}'`).join(', ')}))`
168
+ : `'{}'`;
169
+ } else {
170
+ const fields = exclude.filter((k) => !TOP_LEVEL_COLUMNS.has(k)).map(asName);
171
+ dataExpr = fields.length
172
+ ? `json_remove(${dataCol}, ${fields.map((f) => `'$.${f}'`).join(', ')})`
173
+ : dataCol;
174
+ }
175
+ parts.select = `_id, ${dataExpr} AS data, created, updated, version`;
176
+ break;
177
+ }
136
178
  case '$group': {
137
179
  const group = value as Record<string, unknown>;
138
180
  const groupId = group._id;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * menuChrome — the shared look for popup/context menus, as framework-agnostic
3
+ * HTML/CSS strings so it can be used by native shells (ugly-studio's Electron
4
+ * context menu + ugly.bot menu) as well as by web/React surfaces (which style
5
+ * their own menu markup from the SAME tokens below). One source of truth for
6
+ * menu typography, spacing, icons, and hover.
7
+ *
8
+ * Usage: drop MENU_FONTS in <head>, then MENU_TOKENS + MENU_CSS in a <style>.
9
+ * Build rows as `<div class="mi"><span class="ic">…icon…</span><span class="lbl">
10
+ * Label</span><span class="acc">⌘X</span></div>` and dividers as `<div class="sep">`.
11
+ */
12
+
13
+ /** Webfonts — IBM Plex Mono (rows) + Bricolage (display). */
14
+ export const MENU_FONTS =
15
+ '<link rel="preconnect" href="https://fonts.googleapis.com">' +
16
+ '<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">';
17
+
18
+ /** Industrial tokens (light + dark) shared by every menu. */
19
+ export const MENU_TOKENS = `:root{
20
+ --panel2:#ece6da;--panel:#f2ede3;--dock:#e0d9cb;--line:#d3cab9;--line2:#c0b6a3;
21
+ --ink:#23201a;--dim:#6c6457;--faint:#a09784;--orange:#e8590c;--orange-d:#c2430a;--orange-soft:#f7e7d6;
22
+ --shadow:0 24px 52px -20px rgba(20,12,4,.6),0 8px 20px -10px rgba(20,12,4,.4);
23
+ --disp:'Bricolage Grotesque',ui-sans-serif,system-ui,sans-serif;
24
+ --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,monospace;
25
+ }
26
+ @media (prefers-color-scheme:dark){:root{
27
+ --panel2:#241e18;--panel:#1e1915;--dock:#13100c;--line:#322a22;--line2:#473a2d;
28
+ --ink:#efe7da;--dim:#a99a86;--faint:#6f6453;--orange:#ff6a1f;--orange-d:#d44e0a;--orange-soft:rgba(255,106,31,.15);
29
+ --shadow:0 24px 52px -20px rgba(0,0,0,.62),0 8px 20px -10px rgba(0,0,0,.5);
30
+ }}`;
31
+
32
+ /**
33
+ * The shared card + row primitives. `.menu` is the card; `.mi` a row (icon +
34
+ * label + trailing accel/chevron); `.sep` a divider. Icons work either wrapped
35
+ * in `.ic` or as a direct `<svg>` child.
36
+ */
37
+ export const MENU_CSS = `.menu{background:var(--panel2);border:1px solid var(--line2);border-radius:13px;box-shadow:var(--shadow);padding:7px}
38
+ .mi{display:flex;align-items:center;gap:11px;width:100%;padding:8px 9px;border:none;background:none;border-radius:8px;color:var(--ink);font-family:var(--mono);font-size:11.5px;line-height:1.35;text-align:left;white-space:nowrap;cursor:pointer}
39
+ .mi:hover,.mi.active{background:var(--orange-soft);color:var(--orange)}
40
+ .mi .ic{width:15px;height:15px;flex:none;display:grid;place-items:center;color:var(--dim)}
41
+ .mi .ic svg{width:15px;height:15px}
42
+ .mi:hover .ic,.mi.active .ic{color:var(--orange)}
43
+ .mi>svg{width:15px;height:15px;flex:none;color:var(--dim)}
44
+ .mi:hover>svg,.mi.active>svg{color:var(--orange)}
45
+ .mi .lbl{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
46
+ .mi .acc,.mi .chev{margin-left:auto;flex:none;color:var(--faint);font-size:10px;letter-spacing:.02em;display:grid}
47
+ .mi .chev svg{width:14px;height:14px;display:block}
48
+ .mi:hover .acc,.mi.active .acc,.mi:hover .chev,.mi.active .chev{color:var(--orange-d)}
49
+ .mi.dis{color:var(--faint);cursor:default}.mi.dis:hover{background:none}.mi.dis .ic{color:var(--faint)}
50
+ .sep{height:1px;background:var(--line);margin:6px 5px}`;
@@ -91,6 +91,23 @@ Store `BASE_URL="$(npx ugly-app url)"` for the run. Everything the swarm reads a
91
91
  lives on **this local server** — the framework read CLIs (`ugly-app feedback`, `errors`,
92
92
  `logs`) query **prod**, not local, so don't use them to inspect the run.
93
93
 
94
+ ### Local vs. prod target
95
+
96
+ Default to **local** (above) — it isolates the run and tests your current checkout. But
97
+ target **prod** (`BASE_URL=https://<app-domain>`, skip step 5's dev server) when the app is
98
+ **consume-heavy and the data only exists in prod** — e.g. a corpus/search app whose local
99
+ DB is empty, where every consume-persona would fail Step 2. Only the user can OK a prod run
100
+ (real users' app, real feedback queue). When you do run against prod:
101
+ - **Verify auth first:** load `$BASE_URL` in Playwright with a bot's `auth_token` cookie and
102
+ confirm the app renders authed (not a login page) before dispatching — an SSO app may not
103
+ honor the cookie, which would waste the whole (billed) swarm.
104
+ - **Tag feedback:** `feedback:submit` writes to the **real** prod queue that `ugly-app
105
+ feedback` / `/fix-feedback` read. Have every persona prefix its message with `[eval-swarm]`
106
+ so synthetic findings are filterable from genuine user feedback. (Upside vs. local: the
107
+ fixer *can* read these — no in-context handoff needed.)
108
+ - You're testing the **deployed** build, not your checkout — deploy first if you want your
109
+ latest changes evaluated.
110
+
94
111
  ---
95
112
 
96
113
  ## Step 1 — Design the personas (grounded in THIS app)
@@ -205,10 +222,61 @@ rows. Don't hand off by telling a fixer to "go read the feedback" — it can't.)
205
222
  output, then UX friction, then wishes.
206
223
  4. Present one ranked list: for each item — feature, severity, how many personas hit it,
207
224
  the sharpest one-line evidence, and the persona verdicts (did the tool work?).
208
- 5. Hand off the ranked list **as content** for fixing either fix the items directly in
225
+ 5. **Rate the product 1–5 stars** (see rubric) with a one-line justification. Lead the
226
+ presentation with the rating — it is the headline the user wants; the ranked list is the
227
+ evidence behind it.
228
+ 6. Hand off the ranked list **as content** for fixing — either fix the items directly in
209
229
  this session, or paste the list into a `/fix-feedback`-style pass (using its ambition
210
230
  policy), since the fixer cannot pull these local rows itself.
211
231
 
232
+ **A persona reports a SYMPTOM; you diagnose the CAUSE before fixing.** "Exact search
233
+ returns only one book" is a symptom — the cause was an FTS index the D1 cutover never
234
+ backfilled (72k fragments, only the newest book indexed), found by querying prod D1
235
+ directly (`wrangler d1 execute … "SELECT count(*) FROM <c>_fts"`), not by reading the UI.
236
+ Root-cause at the data/infra layer (indexes, migrations, provisioning, backfills) before
237
+ touching surface code — several personas' separate complaints often trace to one cause,
238
+ and patching the surface leaves it broken.
239
+
240
+ ### Iterating to a target rating (fix → re-run)
241
+
242
+ Eval-swarm is a *loop*, not a one-shot: run → rate → fix → re-run → confirm the rating
243
+ moved. That's how a product climbs from ★★★ to ★★★★★.
244
+
245
+ - **Reuse the same bot accounts across rounds.** They don't delete and idle ones bill
246
+ nothing; a re-run just re-drives the app with fresh chats ("New chat"), so you don't
247
+ mint (and pay to warm) a new roster each round.
248
+ - **Keep the round-N+1 goals the same** (don't tell personas "we fixed X, confirm it") —
249
+ their existing goals naturally re-exercise the fixed areas, and a fresh, unbiased
250
+ verdict is the honest signal. The rigorous persona's re-verification is the one that
251
+ actually moves the rating.
252
+ - **Deploy before re-running a prod-target swarm** — personas test the *deployed* build,
253
+ so a fix that's only committed won't show. (Data/index fixes like a backfill are live
254
+ immediately; code fixes need a deploy.)
255
+ - **Track the trajectory**: report round N's rating, the ceiling finding, what you fixed,
256
+ and round N+1's rating. The ceiling finding is your fix list — clear it and the next
257
+ round's ceiling rises.
258
+ - **A billed re-run is the user's call.** After fixing, you may have *directly verified*
259
+ every ceiling item yourself (drove the flow, read the screenshot); say so and offer the
260
+ independent re-run rather than auto-spending another swarm's worth of tokens.
261
+
262
+ ### Rating rubric (1–5 stars)
263
+
264
+ Rate the product as a whole, computed from the **worst core-feature outcome**, **how many
265
+ personas completed their primary goal**, and the **persona verdicts**. A broken feature
266
+ caps the score — polish can't buy back a blocker.
267
+
268
+ | Stars | Meaning | Gate |
269
+ |-------|---------|------|
270
+ | ★☆☆☆☆ **1 — Broken** | A core feature fails; a real user cannot accomplish the primary job. | Any blocker on a *primary* feature, or most personas' verdict = "broken". |
271
+ | ★★☆☆☆ **2 — Rough** | Only happy paths work; a primary feature is broken-with-workaround, or several high-severity bugs. | A primary feature is unreliable, or ≥2 high-severity correctness/empty-output findings. |
272
+ | ★★★☆☆ **3 — Usable** | Core jobs work but with real friction, wrong/empty AI output, or a broken *secondary* feature. | Any blocker/high on a secondary feature, or mixed persona verdicts. |
273
+ | ★★★★☆ **4 — Solid** | All core jobs work; findings are UX polish, edge cases, or wishes. | All personas completed their goal; no blocker/high on a core path. |
274
+ | ★★★★★ **5 — Excellent** | Every persona completed their goal; outputs accurate and trustworthy; only cosmetic nits. | Every verdict "works"; no finding above `low`. |
275
+
276
+ Caps are hard: one blocker on a primary feature means **≤2 stars no matter how good the
277
+ rest is**; a broken secondary feature caps at **3**. Half-stars are fine (e.g. ★★★½).
278
+ State the single finding that set the ceiling.
279
+
212
280
  ---
213
281
 
214
282
  ## Screenshot recipe — always wait for hydration
@@ -231,6 +299,16 @@ If `WAIT_SEL` never appears **and you've confirmed it's the right selector for t
231
299
  the page genuinely failed to render — report that as a real bug. Never fall back to a
232
300
  no-wait capture.
233
301
 
302
+ **Writing your own Playwright script (multi-step journeys):** the CLI `pnpm exec playwright
303
+ screenshot` is one-shot; a persona driving a real journey (search → click → chat → wait)
304
+ writes a `.mjs` script instead. Two gotchas that will `ERR_MODULE_NOT_FOUND` you:
305
+ - **Import from the package that's actually installed.** Many apps ship `@playwright/test`,
306
+ not standalone `playwright` — `import { chromium } from '@playwright/test'`. Check with
307
+ `ls node_modules | grep playwright` first.
308
+ - **Put the script inside the project dir** (e.g. `./.eval-<slug>.mjs`), not `/tmp` — Node
309
+ resolves bare imports from the script's own folder upward, so a script in a scratchpad
310
+ can't find the app's `node_modules`. Delete it when done.
311
+
234
312
  ---
235
313
 
236
314
  ## Config (optional, per-app)
@@ -256,6 +334,54 @@ If no config exists, invent personas per Step 1 — that is the normal path.
256
334
 
257
335
  ---
258
336
 
337
+ ## Standard ugly-app capabilities (what's cheap to build)
338
+
339
+ Every ugly-app child app is built on the same framework, so a large feature set is
340
+ **standard and low-cost to wire** — it's a handler + a collection + a page, not new
341
+ infrastructure. Use this list two ways: (1) when a persona wishes for something the app
342
+ lacks, check here — if it's a standard capability, mark the `feature` finding **"low-cost
343
+ (framework-standard)"** so the fixer knows it's an easy win, not a moonshot; (2) when
344
+ designing personas, exercise the AI-bearing standard features hardest — they have the most
345
+ to get wrong.
346
+
347
+ **Data & search**
348
+ - Typed collections on D1 or Neon (`createTypedDB` / `defineCollections`, zod schemas); CRUD via `socket.request` handlers.
349
+ - Full-text search (FTS5, `meta.search`); semantic/vector search (Cloudflare Vectorize, `meta.vector`); RRF hybrid fusion of the two.
350
+ - Structured queries: filters (`$in`, ranges), sort, pagination, counts.
351
+ - `trackDocs` / collab — live-syncing documents with realtime updates (no manual polling).
352
+
353
+ **AI (all user-billed or owner-proxied)**
354
+ - Text generation with multiple providers (Claude, DeepSeek, GLM…), streaming via SSE, `reasoningEffort`.
355
+ - Client-driven **agent loop** (`agentTurn` + tools + `sessionStore` memory) — a tool-using chat agent with durable per-user memory.
356
+ - Image generation (`createImageGen`), TTS (with viseme + 3D-avatar pipeline), embeddings (`createEmbeddingClient`), web search (`createWebSearchClient`).
357
+ - Grounded RAG: retrieval tool + citations that open the source (as in andalib's research chat).
358
+
359
+ **Auth & users**
360
+ - ugly.bot SSO (silent login, magic-link), `getUserId`/`getUserToken`/`userContextStore`; per-user preferences persisted cross-device.
361
+ - Bot/test accounts (`auth:create-bot`); user profiles (`userGet`).
362
+
363
+ **Comms & sharing**
364
+ - Push notifications (`pushSendTyped`), email (`emailSend` + templates), public login-free **share links** (`shareLink`), cross-app chat hub.
365
+
366
+ **Files, media & content**
367
+ - Uploads/storage via `ctx.storage` (R2), blob upload, public-prefix serving.
368
+ - Markdown editor + viewer (rich editing, streaming-markdown repair, annotations, heading labels).
369
+ - Conversations engine (threads, messages, sharing).
370
+
371
+ **App structure & ops**
372
+ - Pages & routing (`definePage`/`definePages`, typed params, deep-link + push routing, popups), SSR.
373
+ - Cron / scheduled tasks; A/B experiments; owner alerts.
374
+ - Telemetry to D1 — error logs, feedback reports, perf snapshots (queryable via `ugly-app errors`/`feedback`/`perf`).
375
+
376
+ **Trivially-assembled app features** (primitives already exist, ~a collection + a list/page):
377
+ bookmarks/favorites, search history/recents, calendar/events, comments/annotations,
378
+ tags/filters, a settings page, an admin gate.
379
+
380
+ If a wished-for feature is **not** on this list (needs a new provider, a novel algorithm,
381
+ native/mobile work, or cross-app infra), say so — that's a real-cost item, not an easy win.
382
+
383
+ ---
384
+
259
385
  ## Red flags — STOP
260
386
 
261
387
  | Thought | Reality |
@@ -269,8 +395,8 @@ If no config exists, invent personas per Step 1 — that is the normal path.
269
395
 
270
396
  ## Completion
271
397
 
272
- One-line summary:
398
+ One-line summary — lead with the star rating:
273
399
 
274
400
  ```
275
- Eval swarm complete. Personas: {N}/{M} succeeded. Findings: {total} ({dedup} unique). Top issue: {…}. Verdict: {works / broken}.
401
+ Eval swarm complete. Rating: {★★★☆☆ 3/5} ({ceiling finding}). Personas: {N}/{M} succeeded. Findings: {total} ({dedup} unique). Top issue: {…}.
276
402
  ```