gestalt-mobile 0.37.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/server/server/app.js +3 -0
- package/dist/server/server/cli.js +11 -0
- package/dist/server/server/composition.js +1 -0
- package/dist/server/server/config.js +7 -0
- package/dist/server/server/features/pwa/register-routes.js +36 -0
- package/dist/server/server/platform/pwa/load-pwa-icon.js +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,17 @@ The command prints the loopback URL when it is ready. Open that URL in a
|
|
|
32
32
|
browser. Press Ctrl-C, or send SIGINT or SIGTERM, to stop the HTTP server,
|
|
33
33
|
active Codex subprocesses, and database cleanly.
|
|
34
34
|
|
|
35
|
+
Use `--icon <path>` to replace the installed PWA application icon with a local
|
|
36
|
+
SVG or PNG. Relative paths resolve from the directory where Mobile is started:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
gestalt-mobile --cwd . --icon ./branding/project-icon.svg
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Without `--icon`, Mobile keeps its packaged Gestalt icons. SVG icons are
|
|
43
|
+
declared scalable; PNG dimensions are read from the file. A square PNG of at
|
|
44
|
+
least 512×512 is recommended for consistent launcher presentation.
|
|
45
|
+
|
|
35
46
|
### Run this checkout
|
|
36
47
|
|
|
37
48
|
`npm run start` builds and runs the current checkout with the same managed
|
|
@@ -149,6 +160,7 @@ checks with `npm run format:check`, `npm run license:check`, `npm run check`,
|
|
|
149
160
|
| `--public-origin <origin>` | Loopback Vite origin | Exact browser origin for passkeys; required for non-loopback hosts |
|
|
150
161
|
| `--disable-passkey-auth` | Off | Disable passkey access control and serve every client directly |
|
|
151
162
|
| `--data-dir <path>` | XDG state directory | Directory containing `relay.sqlite` |
|
|
163
|
+
| `--icon <path>` | Gestalt icons | SVG or PNG used when installing the PWA |
|
|
152
164
|
| `--skills <profile>` | | Apply a saved global profile to every session |
|
|
153
165
|
| `--skills list` | | List global profiles without starting the server |
|
|
154
166
|
| `--help` | | Print usage without starting the application |
|
|
@@ -19,6 +19,7 @@ import { registerOrgPlanAttentionRoutes } from './features/org-plan-attention/re
|
|
|
19
19
|
import { registerAutopilotRoutes } from './features/autopilot/register-routes.js';
|
|
20
20
|
import { registerFileRoutes } from './features/files/register-routes.js';
|
|
21
21
|
import { registerMaintenanceRoutes } from './features/maintenance/register-routes.js';
|
|
22
|
+
import { registerPwaRoutes } from './features/pwa/register-routes.js';
|
|
22
23
|
export async function buildApp(deps) {
|
|
23
24
|
// Keep the default boundary finite even when an endpoint forgot a narrower schema.
|
|
24
25
|
const app = fastify({ logger: false, bodyLimit: 1024 * 1024 });
|
|
@@ -32,6 +33,8 @@ export async function buildApp(deps) {
|
|
|
32
33
|
return payload;
|
|
33
34
|
});
|
|
34
35
|
await app.register(fastifyCookie);
|
|
36
|
+
if (deps.pwaIcon)
|
|
37
|
+
registerPwaRoutes(app, deps.pwaIcon);
|
|
35
38
|
if (deps.staticDir)
|
|
36
39
|
await app.register(fastifyStatic, { root: deps.staticDir });
|
|
37
40
|
if (deps.auth)
|
|
@@ -14,6 +14,7 @@ import { composeRelayApp } from './composition.js';
|
|
|
14
14
|
import { CliUsageError, parseConfig } from './config.js';
|
|
15
15
|
import { LauncherProfileCatalog } from './platform/catalog/launcher-profile-catalog.js';
|
|
16
16
|
import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
|
|
17
|
+
import { loadPwaIcon } from './platform/pwa/load-pwa-icon.js';
|
|
17
18
|
const runFile = promisify(execFile);
|
|
18
19
|
export const usage = `Usage: gestalt-mobile [options]
|
|
19
20
|
|
|
@@ -24,6 +25,7 @@ Options:
|
|
|
24
25
|
--public-origin <origin> Exact HTTPS browser origin for passkeys (localhost may use HTTP)
|
|
25
26
|
--disable-passkey-auth Disable passkey access control (unsafe on shared or network hosts)
|
|
26
27
|
--data-dir <path> Directory for persistent application data
|
|
28
|
+
--icon <path> SVG or PNG used as the installed PWA application icon
|
|
27
29
|
--skills <profile> Use a global skill profile for every session
|
|
28
30
|
--skills list List saved global skill profiles without starting the server
|
|
29
31
|
--help Show this help
|
|
@@ -205,6 +207,14 @@ export async function runCli(dependencies = {}) {
|
|
|
205
207
|
stderr.write(`Unknown skill profile: ${config.skillsProfile}\n\n${usage}\n`);
|
|
206
208
|
return 2;
|
|
207
209
|
}
|
|
210
|
+
let pwaIcon;
|
|
211
|
+
try {
|
|
212
|
+
pwaIcon = config.iconPath ? await loadPwaIcon(config.iconPath) : undefined;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
stderr.write(`${error instanceof Error ? error.message : 'Invalid --icon file'}\n\n${usage}\n`);
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
208
218
|
const diagnosis = await (dependencies.runStartupDoctor ?? runStartupDoctor)(cwd);
|
|
209
219
|
if (diagnosis.report)
|
|
210
220
|
stderr.write(diagnosis.report);
|
|
@@ -219,6 +229,7 @@ export async function runCli(dependencies = {}) {
|
|
|
219
229
|
relyingParty: config.relyingParty,
|
|
220
230
|
passkeyAuthEnabled: config.passkeyAuthEnabled,
|
|
221
231
|
staticDir: packagedClientDir(moduleUrl),
|
|
232
|
+
pwaIcon,
|
|
222
233
|
profiles: new LauncherProfileCatalog(),
|
|
223
234
|
installedCodexVersion,
|
|
224
235
|
installedKimiVersion,
|
|
@@ -15,6 +15,7 @@ const optionNames = new Set([
|
|
|
15
15
|
'--data-dir',
|
|
16
16
|
'--public-origin',
|
|
17
17
|
'--skills',
|
|
18
|
+
'--icon',
|
|
18
19
|
]);
|
|
19
20
|
const flagNames = new Set(['--disable-passkey-auth']);
|
|
20
21
|
export function parseConfig(args, cwd = process.cwd()) {
|
|
@@ -57,8 +58,14 @@ export function parseConfig(args, cwd = process.cwd()) {
|
|
|
57
58
|
root: resolve(cwd, values.get('--cwd') ?? '.'),
|
|
58
59
|
dataDir: values.get('--data-dir'),
|
|
59
60
|
skillsProfile: values.get('--skills'),
|
|
61
|
+
iconPath: values.has('--icon') ? resolveIconPath(cwd, values.get('--icon')) : undefined,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
64
|
+
function resolveIconPath(cwd, value) {
|
|
65
|
+
if (!/\.(?:png|svg)$/i.test(value))
|
|
66
|
+
throw new CliUsageError('--icon must reference an SVG or PNG file');
|
|
67
|
+
return resolve(cwd, value);
|
|
68
|
+
}
|
|
62
69
|
export function normalizePublicOrigin(value) {
|
|
63
70
|
if (!/^[a-z][a-z\d+.-]*:\/\/[^/?#\s\\\\]+\/?$/i.test(value))
|
|
64
71
|
throw new CliUsageError('--public-origin must be a bare origin without a path, query, or fragment');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
const manifestBase = {
|
|
7
|
+
id: '/',
|
|
8
|
+
name: 'Gestalt Mobile',
|
|
9
|
+
short_name: 'Gestalt',
|
|
10
|
+
description: 'A mobile interface for durable Codex development sessions.',
|
|
11
|
+
start_url: '/',
|
|
12
|
+
scope: '/',
|
|
13
|
+
display: 'standalone',
|
|
14
|
+
background_color: '#141414',
|
|
15
|
+
theme_color: '#141414',
|
|
16
|
+
categories: ['productivity', 'utilities'],
|
|
17
|
+
};
|
|
18
|
+
/** Overrides only install metadata; ordinary client assets stay package-static. */
|
|
19
|
+
export function registerPwaRoutes(app, icon) {
|
|
20
|
+
const iconPath = `/install-icon.${icon.extension}`;
|
|
21
|
+
app.get('/manifest.webmanifest', async (_request, reply) => reply
|
|
22
|
+
.header('Cache-Control', 'no-cache')
|
|
23
|
+
.type('application/manifest+json')
|
|
24
|
+
.send({
|
|
25
|
+
...manifestBase,
|
|
26
|
+
icons: [
|
|
27
|
+
{
|
|
28
|
+
src: iconPath,
|
|
29
|
+
sizes: icon.sizes,
|
|
30
|
+
type: icon.contentType,
|
|
31
|
+
purpose: 'any',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
}));
|
|
35
|
+
app.get(iconPath, async (_request, reply) => reply.header('Cache-Control', 'no-cache').type(icon.contentType).send(icon.bytes));
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { extname } from 'node:path';
|
|
8
|
+
const maxIconBytes = 5 * 1024 * 1024;
|
|
9
|
+
const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
10
|
+
export async function loadPwaIcon(path) {
|
|
11
|
+
let bytes;
|
|
12
|
+
try {
|
|
13
|
+
bytes = await readFile(path);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`Unable to read --icon file: ${path}`);
|
|
17
|
+
}
|
|
18
|
+
if (bytes.length === 0 || bytes.length > maxIconBytes)
|
|
19
|
+
throw new Error('--icon must be a non-empty SVG or PNG no larger than 5 MiB');
|
|
20
|
+
const extension = extname(path).toLowerCase();
|
|
21
|
+
if (extension === '.svg') {
|
|
22
|
+
if (!/<svg(?:\s|>)/i.test(bytes.toString('utf8')))
|
|
23
|
+
throw new Error('--icon SVG does not contain an <svg> root');
|
|
24
|
+
return { bytes, contentType: 'image/svg+xml', extension: 'svg', sizes: 'any' };
|
|
25
|
+
}
|
|
26
|
+
if (extension !== '.png' ||
|
|
27
|
+
bytes.length < 24 ||
|
|
28
|
+
!bytes.subarray(0, 8).equals(pngSignature) ||
|
|
29
|
+
bytes.toString('ascii', 12, 16) !== 'IHDR')
|
|
30
|
+
throw new Error('--icon PNG has an invalid header');
|
|
31
|
+
const width = bytes.readUInt32BE(16);
|
|
32
|
+
const height = bytes.readUInt32BE(20);
|
|
33
|
+
if (width === 0 || height === 0)
|
|
34
|
+
throw new Error('--icon PNG has invalid dimensions');
|
|
35
|
+
return { bytes, contentType: 'image/png', extension: 'png', sizes: `${width}x${height}` };
|
|
36
|
+
}
|