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.
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/playwright/dev-auth.d.ts +29 -0
- package/dist/playwright/dev-auth.d.ts.map +1 -0
- package/dist/playwright/dev-auth.js +35 -0
- package/dist/playwright/dev-auth.js.map +1 -0
- package/dist/playwright/index.d.ts +4 -0
- package/dist/playwright/index.d.ts.map +1 -1
- package/dist/playwright/index.js +2 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/real-native.d.ts +60 -0
- package/dist/playwright/real-native.d.ts.map +1 -0
- package/dist/playwright/real-native.js +178 -0
- package/dist/playwright/real-native.js.map +1 -0
- package/dist/server/sqlite/SqlitePipeline.d.ts.map +1 -1
- package/dist/server/sqlite/SqlitePipeline.js +44 -0
- package/dist/server/sqlite/SqlitePipeline.js.map +1 -1
- package/dist/shared/menuChrome.d.ts +22 -0
- package/dist/shared/menuChrome.d.ts.map +1 -0
- package/dist/shared/menuChrome.js +47 -0
- package/dist/shared/menuChrome.js.map +1 -0
- package/package.json +2 -1
- package/src/cli/version.ts +1 -1
- package/src/playwright/dev-auth.ts +46 -0
- package/src/playwright/index.ts +4 -0
- package/src/playwright/real-native.ts +219 -0
- package/src/server/sqlite/SqlitePipeline.test.ts +37 -0
- package/src/server/sqlite/SqlitePipeline.ts +42 -0
- package/src/shared/menuChrome.ts +50 -0
- package/templates/.claude/skills/eval-swarm/SKILL.md +129 -3
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.861";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -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"}
|
package/dist/playwright/index.js
CHANGED
|
@@ -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":"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"}
|
|
@@ -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,
|
|
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.
|
|
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",
|
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.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
|
+
}
|
package/src/playwright/index.ts
CHANGED
|
@@ -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.
|
|
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: {…}.
|
|
401
|
+
Eval swarm complete. Rating: {★★★☆☆ 3/5} ({ceiling finding}). Personas: {N}/{M} succeeded. Findings: {total} ({dedup} unique). Top issue: {…}.
|
|
276
402
|
```
|