@materialwebunofficial/antigravity-themes 0.0.0-stage → 1.0.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/bin/cli.mjs ADDED
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ // antigravity-themes: installs, runs and removes the themes. See README.md.
3
+ import { spawn } from 'node:child_process';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { closeSync, cpSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, watch as watchFiles, writeFileSync } from 'node:fs';
6
+ import http from 'node:http';
7
+ import path from 'node:path';
8
+ import { ROOT, pageScript, REMOVE_SCRIPT, PROBE_SCRIPT, selectScript } from '../lib/bundle.mjs';
9
+ import { readPort, listTargets, evaluateEverywhere, watch } from '../lib/injector.mjs';
10
+ import { installDir, stateFile, logFile, portFile } from '../lib/paths.mjs';
11
+ import { writeAutostart, removeAutostart, hasAutostart, autostartFile } from '../lib/autostart.mjs';
12
+
13
+ const NAME = 'antigravity-themes';
14
+ const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
15
+ const NPX = `npx ${pkg.name}`;
16
+ const [command = 'help', ...args] = process.argv.slice(2);
17
+ const has = (flag) => args.includes(flag);
18
+ const say = (line = '') => console.log(line);
19
+ function fail(message) {
20
+ console.error(`${NAME}: ${message}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ if (typeof WebSocket !== 'function') fail(`Node.js 22 or newer is needed (this is ${process.version}).`);
25
+
26
+ // ---- Background process ------------------------------------------------------
27
+ // It answers on 127.0.0.1 with a random token, written to run.json: other
28
+ // commands find and stop it through that, never by process id (an id can be
29
+ // reused by an unrelated process after a reboot).
30
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
+ function readState() {
32
+ try { return JSON.parse(readFileSync(stateFile(), 'utf8')); } catch { return null; }
33
+ }
34
+ async function running() {
35
+ const state = readState();
36
+ if (!state) return null;
37
+ try {
38
+ const res = await fetch(`http://127.0.0.1:${state.port}/ping?token=${state.token}`, { signal: AbortSignal.timeout(1000) });
39
+ if (res.ok) return state;
40
+ } catch { /* not answering */ }
41
+ return null;
42
+ }
43
+
44
+ async function stopBackground() {
45
+ const state = await running();
46
+ if (!state) return false;
47
+ try {
48
+ await fetch(`http://127.0.0.1:${state.port}/stop?token=${state.token}`, { method: 'POST', signal: AbortSignal.timeout(2000) });
49
+ } catch { /* it exits while answering */ }
50
+ for (let i = 0; i < 30 && await running(); i++) await sleep(100);
51
+ return true;
52
+ }
53
+
54
+ async function startBackground(cli) {
55
+ mkdirSync(installDir(), { recursive: true });
56
+ // keep the log short: it is only for troubleshooting
57
+ try { if (statSync(logFile()).size > 512 * 1024) rmSync(logFile()); } catch { /* none yet */ }
58
+ const log = openSync(logFile(), 'a');
59
+ spawn(process.execPath, [cli, 'run'], { detached: true, stdio: ['ignore', log, log], windowsHide: true }).unref();
60
+ closeSync(log);
61
+ for (let i = 0; i < 100; i++) {
62
+ const state = await running();
63
+ if (state) return state;
64
+ await sleep(100);
65
+ }
66
+ return null;
67
+ }
68
+
69
+ async function run() {
70
+ const other = await running();
71
+ if (other) {
72
+ say(`Already running (process ${other.pid}).`);
73
+ return;
74
+ }
75
+ const token = randomUUID();
76
+ const control = http.createServer((req, res) => {
77
+ const url = new URL(req.url, 'http://127.0.0.1');
78
+ if (url.searchParams.get('token') !== token) { res.writeHead(403).end(); return; }
79
+ if (url.pathname === '/ping') { res.end(pkg.version); return; }
80
+ if (url.pathname === '/stop' && req.method === 'POST') { res.end('stopping'); setTimeout(() => process.exit(0), 50); return; }
81
+ res.writeHead(404).end();
82
+ });
83
+ await new Promise((resolve) => control.listen(0, '127.0.0.1', resolve));
84
+ mkdirSync(installDir(), { recursive: true });
85
+ writeFileSync(stateFile(), JSON.stringify({ pid: process.pid, port: control.address().port, token, version: pkg.version }));
86
+ process.on('exit', () => {
87
+ if (readState()?.token === token) rmSync(stateFile(), { force: true });
88
+ });
89
+ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => process.exit(0));
90
+ // Never two processes theming the same windows: when another one has
91
+ // written run.json since (the last one started wins), or the file is gone
92
+ // (uninstall), this one ends.
93
+ setInterval(() => {
94
+ if (readState()?.token !== token) process.exit(0);
95
+ }, 2000);
96
+ const log = (line) => say(`${new Date().toISOString()} ${line}`);
97
+ log(`${NAME} ${pkg.version} running from ${ROOT}`);
98
+ const watcher = watch({ getSource: () => pageScript(ROOT), log });
99
+ if (has('--dev')) {
100
+ let timer;
101
+ for (const dir of ['src', 'themes', 'vendor']) {
102
+ watchFiles(path.join(ROOT, dir), { recursive: true }, () => {
103
+ clearTimeout(timer);
104
+ timer = setTimeout(() => watcher.reload().catch((err) => log(err.message)), 150);
105
+ });
106
+ }
107
+ log('--dev: reloading on changes to src/, themes/ and vendor/');
108
+ }
109
+ }
110
+
111
+ // ---- Commands -----------------------------------------------------------------
112
+ const RUNTIME = ['bin', 'lib', 'src', 'themes', 'vendor', 'package.json', 'README.md', 'LICENSE', 'NOTICE'];
113
+
114
+ async function install() {
115
+ const dir = installDir();
116
+ const same = path.resolve(dir) === path.resolve(ROOT);
117
+ const wasRunning = await stopBackground();
118
+ if (!same) {
119
+ mkdirSync(dir, { recursive: true });
120
+ for (const entry of RUNTIME) {
121
+ const from = path.join(ROOT, entry);
122
+ if (!existsSync(from)) continue;
123
+ rmSync(path.join(dir, entry), { recursive: true, force: true });
124
+ cpSync(from, path.join(dir, entry), { recursive: true });
125
+ }
126
+ }
127
+ const cli = path.join(dir, 'bin', 'cli.mjs');
128
+ say(`Installed ${NAME} ${pkg.version} in ${dir}`);
129
+ if (has('--no-autostart')) {
130
+ removeAutostart();
131
+ say('Not started at logon (--no-autostart).');
132
+ } else {
133
+ say(`Starts at logon: ${writeAutostart(process.execPath, cli)}`);
134
+ }
135
+ const state = await startBackground(cli);
136
+ say(state ? `${wasRunning ? 'Restarted' : 'Started'} in the background (process ${state.pid}).` : `Could not start the background process; see ${logFile()}`);
137
+ try {
138
+ const windows = await listTargets(readPort());
139
+ say(`Antigravity is running: ${windows.length} window(s) will get the theme within a few seconds.`);
140
+ } catch {
141
+ say('Antigravity is not running: the theme appears when it starts.');
142
+ }
143
+ say();
144
+ say('Choose a theme in Antigravity: the "Theme" menu in the title bar (Matte), or the menu button at the');
145
+ say('top left, then Theme (Material 3 Expressive). Ctrl+Shift+F12 cycles through the themes.');
146
+ say(`Remove everything with: ${NPX} uninstall`);
147
+ }
148
+
149
+ async function uninstall() {
150
+ if (await stopBackground()) say('Stopped the background process.');
151
+ try {
152
+ const results = await evaluateEverywhere(REMOVE_SCRIPT);
153
+ say(`Removed the theme from ${results.filter((r) => !r.error).length} open window(s).`);
154
+ } catch {
155
+ say('Antigravity is not running: nothing to remove from its windows.');
156
+ }
157
+ const entry = removeAutostart();
158
+ if (entry) say(`Removed the logon entry: ${entry}`);
159
+ const dir = installDir();
160
+ if (path.basename(dir) === NAME && existsSync(dir)) {
161
+ rmSync(dir, { recursive: true, force: true });
162
+ say(`Deleted ${dir}`);
163
+ }
164
+ say('Done. Antigravity keeps your last theme choice in its own storage; it has no effect without this tool.');
165
+ }
166
+
167
+ async function on() {
168
+ const results = await evaluateEverywhere(pageScript(ROOT));
169
+ const ok = results.filter((r) => !r.error).length;
170
+ say(`Applied to ${ok} window(s).${await running() ? '' : ' Without the background process it lasts until a window reloads; run "start" to keep it.'}`);
171
+ }
172
+
173
+ async function off() {
174
+ if (await stopBackground()) say('Stopped the background process.');
175
+ const results = await evaluateEverywhere(REMOVE_SCRIPT);
176
+ say(`Removed from ${results.filter((r) => !r.error).length} window(s).${hasAutostart() ? ' It comes back at the next logon; "uninstall" removes it for good.' : ''}`);
177
+ }
178
+
179
+ async function theme() {
180
+ const index = JSON.parse(readFileSync(path.join(ROOT, 'themes', 'index.json'), 'utf8'));
181
+ const key = args.find((a) => !a.startsWith('-'));
182
+ if (!key) {
183
+ say('Themes:');
184
+ for (const o of index.options) say(` ${o.key.padEnd(14)} ${o.name}`);
185
+ say(`\nSwitch with: ${NPX} theme <key>`);
186
+ return;
187
+ }
188
+ if (!index.options.some((o) => o.key === key)) fail(`no theme "${key}". Run "theme" to list them.`);
189
+ const results = await evaluateEverywhere(selectScript(key));
190
+ const ok = results.filter((r) => r.value === 'ok').length;
191
+ if (!ok) fail('no themed window found. Is the background process running? Try "status".');
192
+ say(`Switched ${ok} window(s) to ${key}.`);
193
+ }
194
+
195
+ async function status() {
196
+ const dir = installDir();
197
+ let installed = null;
198
+ try { installed = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8')).version; } catch { /* not installed */ }
199
+ say(`Installed: ${installed ? `${installed} in ${dir}` : 'no'}`);
200
+ say(`At logon: ${hasAutostart() ? autostartFile() : 'no'}`);
201
+ const state = await running();
202
+ say(`Background: ${state ? `running (process ${state.pid}, version ${state.version})` : 'not running'}`);
203
+ let port;
204
+ try { port = readPort(); } catch { say(`Antigravity: not running (no ${portFile()})`); return; }
205
+ try {
206
+ const results = await evaluateEverywhere(PROBE_SCRIPT, port);
207
+ say(`Antigravity: running, port ${port}, ${results.length} window(s)`);
208
+ for (const r of results) {
209
+ const host = r.value ? JSON.parse(r.value) : null;
210
+ say(` ${host ? `themed (${host.theme})` : 'not themed'} ${r.url}`);
211
+ }
212
+ } catch (err) {
213
+ say(`Antigravity: ${err.message}`);
214
+ }
215
+ }
216
+
217
+ function help() {
218
+ say(`${NAME} ${pkg.version}: Matte and Material 3 Expressive themes for Antigravity Desktop
219
+
220
+ Usage: ${NPX} <command>
221
+
222
+ install [--no-autostart] Copy the themes to ${installDir()},
223
+ start them at logon, and start them now
224
+ uninstall Remove the themes from Antigravity, the logon entry and that folder
225
+ start Start the background process that themes every window
226
+ stop Stop it (open windows keep the theme until they reload)
227
+ on Apply the themes to the open windows once
228
+ off Stop the background process and remove the themes from the open windows
229
+ theme [key] List the themes, or switch every window to one
230
+ status Show what is installed and running
231
+ run [--dev] Run the background process in this terminal (--dev reloads on file changes)`);
232
+ }
233
+
234
+ const commands = {
235
+ install, uninstall, on, off, theme, status, run, help,
236
+ start: async () => {
237
+ const state = await running() ?? await startBackground(path.join(ROOT, 'bin', 'cli.mjs'));
238
+ say(state ? `Running in the background (process ${state.pid}).` : `Could not start; see ${logFile()}`);
239
+ },
240
+ stop: async () => say(await stopBackground() ? 'Stopped.' : 'Not running.'),
241
+ '--version': () => say(pkg.version),
242
+ '-v': () => say(pkg.version),
243
+ '--help': help,
244
+ '-h': help,
245
+ };
246
+ const handler = commands[command];
247
+ if (!handler) fail(`unknown command "${command}". Run "help" for the list.`);
248
+ try {
249
+ await handler();
250
+ } catch (err) {
251
+ fail(err.message);
252
+ }
@@ -0,0 +1,70 @@
1
+ // Starts the background process when the user logs in, with no window:
2
+ // Windows a .vbs launcher in the Startup folder (no admin rights needed)
3
+ // macOS a LaunchAgent (untested)
4
+ // Linux an XDG autostart entry (untested)
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { ROOT } from './bundle.mjs';
9
+
10
+ const NAME = 'antigravity-themes';
11
+ const PACKAGE = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8')).name;
12
+
13
+ export function autostartFile() {
14
+ const home = os.homedir();
15
+ if (process.platform === 'win32') {
16
+ const roaming = process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming');
17
+ return path.join(roaming, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${NAME}.vbs`);
18
+ }
19
+ if (process.platform === 'darwin') return path.join(home, 'Library', 'LaunchAgents', `dev.${NAME}.plist`);
20
+ return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, '.config'), 'autostart', `${NAME}.desktop`);
21
+ }
22
+
23
+ // node and the installed cli.mjs, as absolute paths: the entry keeps working
24
+ // after the npx cache is gone (and stops if Node moves: install again then)
25
+ export function writeAutostart(node, cli) {
26
+ const file = autostartFile();
27
+ mkdirSync(path.dirname(file), { recursive: true });
28
+ if (process.platform === 'win32') {
29
+ const q = (s) => s.replace(/"/g, '""');
30
+ writeFileSync(file, [
31
+ `' ${NAME}: applies the themes to Antigravity at logon, with no window.`,
32
+ `' Remove it with: npx ${PACKAGE} uninstall`,
33
+ `CreateObject("WScript.Shell").Run """${q(node)}"" ""${q(cli)}"" run", 0, False`,
34
+ '',
35
+ ].join('\r\n'));
36
+ } else if (process.platform === 'darwin') {
37
+ const x = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
38
+ writeFileSync(file, `<?xml version="1.0" encoding="UTF-8"?>
39
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
40
+ <plist version="1.0">
41
+ <dict>
42
+ <key>Label</key><string>dev.${NAME}</string>
43
+ <key>ProgramArguments</key>
44
+ <array><string>${x(node)}</string><string>${x(cli)}</string><string>run</string></array>
45
+ <key>RunAtLoad</key><true/>
46
+ </dict>
47
+ </plist>
48
+ `);
49
+ } else {
50
+ const q = (s) => `"${s.replace(/(["\\`$])/g, '\\$1')}"`;
51
+ writeFileSync(file, `[Desktop Entry]
52
+ Type=Application
53
+ Name=${NAME}
54
+ Comment=Applies the themes to Antigravity
55
+ Exec=${q(node)} ${q(cli)} run
56
+ X-GNOME-Autostart-enabled=true
57
+ NoDisplay=true
58
+ `);
59
+ }
60
+ return file;
61
+ }
62
+
63
+ export function removeAutostart() {
64
+ const file = autostartFile();
65
+ if (!existsSync(file)) return null;
66
+ rmSync(file);
67
+ return file;
68
+ }
69
+
70
+ export const hasAutostart = () => existsSync(autostartFile());
package/lib/bundle.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // Builds the script that runs inside Antigravity: the vendored md3e-web
2
+ // components, the theme modules and the theme host, started with a bundle
3
+ // read from themes/index.json (each theme's CSS, its fonts as base64, and
4
+ // the choices in the Theme menu).
5
+ import { readFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+
11
+ // The components come first: the indicator modules need them defined.
12
+ const MODULES = [
13
+ 'vendor/md3e/components.js',
14
+ 'src/context-card.js',
15
+ 'src/turn-tokens.js',
16
+ 'src/loading-indicator.js',
17
+ 'src/progress-indicators.js',
18
+ 'src/code-tools.js',
19
+ 'src/theme-host.js',
20
+ ];
21
+
22
+ export function loadBundle(root = ROOT) {
23
+ const themesDir = path.join(root, 'themes');
24
+ const index = JSON.parse(readFileSync(path.join(themesDir, 'index.json'), 'utf8'));
25
+ const themes = {};
26
+ for (const [id, files] of Object.entries(index.themes)) {
27
+ themes[id] = [].concat(files).map((file) => readFileSync(path.join(themesDir, file), 'utf8')).join('\n');
28
+ }
29
+ const fonts = {};
30
+ for (const [id, list] of Object.entries(index.fonts ?? {})) {
31
+ fonts[id] = list.map(({ file, ...descriptors }) => ({
32
+ ...descriptors,
33
+ data: readFileSync(path.join(themesDir, file)).toString('base64'),
34
+ }));
35
+ }
36
+ return { default: index.default, themes, fonts, ripple: index.ripple ?? [], options: index.options };
37
+ }
38
+
39
+ // On a new document the script runs before the parser has created <html>,
40
+ // so it waits for that element; the host then waits for the rest itself.
41
+ export function pageScript(root = ROOT) {
42
+ const parts = MODULES.map((file) => readFileSync(path.join(root, file), 'utf8'));
43
+ return `(() => {
44
+ const start = () => {
45
+ ${parts.join('\n;')}
46
+ ;agyThemeHost(${JSON.stringify(loadBundle(root))});
47
+ };
48
+ if (document.documentElement) start();
49
+ else new MutationObserver((_, observer) => {
50
+ if (!document.documentElement) return;
51
+ observer.disconnect();
52
+ start();
53
+ }).observe(document, { childList: true });
54
+ })();`;
55
+ }
56
+
57
+ // Undoes everything the page script did in that window
58
+ export const REMOVE_SCRIPT = `(() => {
59
+ window.__agyThemeHost?.dispose();
60
+ document.getElementById('agy-theme-css')?.remove();
61
+ document.documentElement?.removeAttribute('data-agy-theme');
62
+ })();`;
63
+
64
+ // The theme host's version and theme in a window, or null without one
65
+ export const PROBE_SCRIPT = `JSON.stringify(window.__agyThemeHost ? { version: window.__agyThemeHost.version, theme: window.__agyThemeHost.get() } : null)`;
66
+
67
+ export const selectScript = (key) => `(() => {
68
+ const host = window.__agyThemeHost;
69
+ if (!host) return 'no-host';
70
+ if (!host.options().some((o) => o.key === ${JSON.stringify(key)})) return 'unknown';
71
+ host.select(${JSON.stringify(key)});
72
+ return 'ok';
73
+ })()`;
@@ -0,0 +1,176 @@
1
+ // Talks to a running Antigravity over the Chrome DevTools Protocol on
2
+ // 127.0.0.1: Antigravity opens that port itself on every launch. Nothing is
3
+ // sent anywhere else, and no app file is changed: the theme lives in the
4
+ // open windows, and a script registered for each window's next document
5
+ // brings it back after a reload while the connection lasts.
6
+ import { readFileSync } from 'node:fs';
7
+ import { portFile } from './paths.mjs';
8
+
9
+ const TARGET_TYPES = new Set(['page', 'webview', 'iframe']);
10
+
11
+ export function readPort() {
12
+ const file = portFile();
13
+ let text;
14
+ try {
15
+ text = readFileSync(file, 'utf8');
16
+ } catch {
17
+ throw new Error(`${file} was not found. Is Antigravity running?`);
18
+ }
19
+ const port = Number(text.split(/\r?\n/)[0]);
20
+ if (!Number.isInteger(port) || port <= 0) throw new Error(`${file} holds no port.`);
21
+ return port;
22
+ }
23
+
24
+ export async function listTargets(port) {
25
+ let res;
26
+ try {
27
+ res = await fetch(`http://127.0.0.1:${port}/json/list`);
28
+ } catch {
29
+ throw new Error(`Antigravity does not answer on port ${port}. Is it running?`);
30
+ }
31
+ const targets = await res.json();
32
+ return targets.filter((t) => TARGET_TYPES.has(t.type) && t.webSocketDebuggerUrl && !t.url.startsWith('devtools://'));
33
+ }
34
+
35
+ export class Session {
36
+ constructor(target) {
37
+ this.target = target;
38
+ this.nextId = 0;
39
+ this.pending = new Map();
40
+ this.scriptId = null;
41
+ this.pageEnabled = false;
42
+ }
43
+
44
+ open() {
45
+ return new Promise((resolve, reject) => {
46
+ this.ws = new WebSocket(this.target.webSocketDebuggerUrl);
47
+ this.ws.onopen = () => resolve(this);
48
+ this.ws.onerror = () => reject(new Error(`cannot connect to ${this.target.url}`));
49
+ this.ws.onclose = () => {
50
+ for (const { reject: fail } of this.pending.values()) fail(new Error('connection closed'));
51
+ this.pending.clear();
52
+ this.onclose?.();
53
+ };
54
+ this.ws.onmessage = (event) => {
55
+ const msg = JSON.parse(event.data);
56
+ const call = msg.id && this.pending.get(msg.id);
57
+ if (!call) return;
58
+ this.pending.delete(msg.id);
59
+ if (msg.error) call.reject(new Error(msg.error.message));
60
+ else call.resolve(msg.result);
61
+ };
62
+ });
63
+ }
64
+
65
+ send(method, params = {}) {
66
+ const id = ++this.nextId;
67
+ return new Promise((resolve, reject) => {
68
+ this.pending.set(id, { resolve, reject });
69
+ this.ws.send(JSON.stringify({ id, method, params }));
70
+ });
71
+ }
72
+
73
+ // Now, and on every new document in this window while connected. Chromium
74
+ // runs scripts registered for new documents only once the Page domain is
75
+ // enabled on this connection.
76
+ async apply(source) {
77
+ if (!this.pageEnabled) {
78
+ await this.send('Page.enable');
79
+ this.pageEnabled = true;
80
+ }
81
+ if (this.scriptId) await this.send('Page.removeScriptToEvaluateOnNewDocument', { identifier: this.scriptId });
82
+ ({ identifier: this.scriptId } = await this.send('Page.addScriptToEvaluateOnNewDocument', { source }));
83
+ await this.evaluate(source);
84
+ }
85
+
86
+ async evaluate(expression) {
87
+ const { result, exceptionDetails } = await this.send('Runtime.evaluate', { expression, returnByValue: true });
88
+ if (exceptionDetails) throw new Error(exceptionDetails.exception?.description ?? exceptionDetails.text);
89
+ return result?.value;
90
+ }
91
+
92
+ close() {
93
+ this.ws.close();
94
+ }
95
+ }
96
+
97
+ // Runs one script in every open window and closes again
98
+ export async function evaluateEverywhere(script, port = readPort()) {
99
+ const results = [];
100
+ for (const target of await listTargets(port)) {
101
+ const session = new Session(target);
102
+ try {
103
+ await session.open();
104
+ results.push({ url: target.url, value: await session.evaluate(script) });
105
+ } catch (err) {
106
+ results.push({ url: target.url, error: err.message });
107
+ } finally {
108
+ session.close();
109
+ }
110
+ }
111
+ return results;
112
+ }
113
+
114
+ // The long-running part: themes every window Antigravity opens, keeps the
115
+ // theme across reloads, follows the port when the app restarts, and waits
116
+ // while it is closed. getSource() is read again when reload() is called.
117
+ export function watch({ getSource, log = () => {} }) {
118
+ const sessions = new Map(); // target id -> Session
119
+ let port = null;
120
+ let source = getSource();
121
+ let waiting = false;
122
+
123
+ function followPort() {
124
+ const next = readPort();
125
+ if (next === port) return;
126
+ for (const session of sessions.values()) session.close();
127
+ sessions.clear();
128
+ if (port !== null) log(`Antigravity restarted (port ${next})`);
129
+ port = next;
130
+ }
131
+
132
+ async function attachNew() {
133
+ for (const target of await listTargets(port)) {
134
+ if (sessions.has(target.id)) continue;
135
+ const session = new Session(target);
136
+ sessions.set(target.id, session);
137
+ session.onclose = () => sessions.delete(target.id);
138
+ try {
139
+ await session.open();
140
+ await session.apply(source);
141
+ log(`themed ${target.type} ${target.url}`);
142
+ } catch (err) {
143
+ log(`${target.url}: ${err.message}`);
144
+ sessions.delete(target.id);
145
+ }
146
+ }
147
+ }
148
+
149
+ async function tick() {
150
+ try {
151
+ followPort();
152
+ await attachNew();
153
+ if (waiting) log(`Antigravity found on port ${port}`);
154
+ waiting = false;
155
+ } catch (err) {
156
+ if (!waiting) log(`waiting for Antigravity (${err.message})`);
157
+ waiting = true;
158
+ }
159
+ }
160
+
161
+ const timer = setInterval(tick, 2000);
162
+ tick();
163
+ return {
164
+ async reload() {
165
+ source = getSource();
166
+ for (const session of sessions.values()) {
167
+ try { await session.apply(source); } catch (err) { log(`reload failed for ${session.target.url}: ${err.message}`); }
168
+ }
169
+ log(`reloaded into ${sessions.size} window(s)`);
170
+ },
171
+ stop() {
172
+ clearInterval(timer);
173
+ for (const session of sessions.values()) session.close();
174
+ },
175
+ };
176
+ }
package/lib/paths.mjs ADDED
@@ -0,0 +1,28 @@
1
+ // Where Antigravity writes its debug port, and where this tool installs
2
+ // itself, per platform. Windows is tested; macOS and Linux follow Electron's
3
+ // conventions for an app named "Antigravity".
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const home = os.homedir();
8
+
9
+ // Electron's userData folder for Antigravity
10
+ export function appDataDir() {
11
+ if (process.platform === 'win32') return path.join(process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), 'Antigravity');
12
+ if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Antigravity');
13
+ return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, '.config'), 'Antigravity');
14
+ }
15
+
16
+ // Antigravity starts with --remote-debugging-port=0 and writes the port it
17
+ // got here (first line), anew on every launch
18
+ export const portFile = () => path.join(appDataDir(), 'DevToolsActivePort');
19
+
20
+ export function installDir() {
21
+ if (process.platform === 'win32') return path.join(process.env.LOCALAPPDATA ?? path.join(home, 'AppData', 'Local'), 'antigravity-themes');
22
+ if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'antigravity-themes');
23
+ return path.join(process.env.XDG_DATA_HOME ?? path.join(home, '.local', 'share'), 'antigravity-themes');
24
+ }
25
+
26
+ // The background process writes how to reach it here while it runs
27
+ export const stateFile = () => path.join(installDir(), 'run.json');
28
+ export const logFile = () => path.join(installDir(), 'run.log');
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@materialwebunofficial/antigravity-themes",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Matte and Material 3 Expressive themes for Antigravity Desktop, applied without changing the app's files.",
5
+ "homepage": "https://github.com/materialwebunofficial/antigravity-themes#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/materialwebunofficial/antigravity-themes.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/materialwebunofficial/antigravity-themes/issues"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "antigravity-themes": "bin/cli.mjs"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "lib/",
20
+ "src/",
21
+ "themes/",
22
+ "vendor/",
23
+ "LICENSE",
24
+ "NOTICE",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "keywords": [
31
+ "antigravity",
32
+ "theme",
33
+ "material-design",
34
+ "material-3",
35
+ "material-3-expressive",
36
+ "md3e"
37
+ ],
38
+ "license": "Apache-2.0",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }