@materialwebunofficial/antigravity-themes 1.0.4 → 1.0.13

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 CHANGED
@@ -1,7 +1,9 @@
1
1
  # antigravity-themes
2
2
 
3
- Two themes for Antigravity Desktop: **Matte** and **Material 3 Expressive**
4
- (in five colors). They are applied to the running app; no app file is changed.
3
+ Four themes for Antigravity Desktop: **Matte**, **Nordic Frost**, **E-Ink /
4
+ Paper** and **Material 3 Expressive** (in five colors). Each one restyles the
5
+ whole app in its own structure, Settings, menus, dialogs and sidecar panes
6
+ such as Swarm included, rather than recoloring Antigravity's default look.
5
7
 
6
8
  This is an unofficial project. It is not affiliated with, endorsed by or
7
9
  sponsored by Google.
@@ -9,12 +11,18 @@ sponsored by Google.
9
11
  ## Install
10
12
 
11
13
  ```sh
12
- npx -y @materialwebunofficial/antigravity-themes install
14
+ npx -y @materialwebunofficial/antigravity-themes install --native
13
15
  ```
14
16
 
15
17
  That is all. It takes a few seconds, needs no administrator rights, and it is
16
- permanent: the themes return at every logon and every Antigravity start, until
17
- you uninstall them. It needs [Node.js](https://nodejs.org) 22 or newer.
18
+ permanent: the themes stay after closing Antigravity, after restarting the
19
+ computer and after Antigravity updates itself, with nothing running in the
20
+ background and nothing to reinstall. It needs [Node.js](https://nodejs.org) 22
21
+ or newer (only to run the command; the themes do not need it afterwards).
22
+
23
+ Without `--native`, `install` leaves Antigravity's files untouched and applies
24
+ the themes from a small background process instead (see
25
+ [What install does](#what-install-does)).
18
26
 
19
27
  On Windows, if PowerShell says running scripts is disabled, use `npx.cmd` in
20
28
  place of `npx`.
@@ -50,12 +58,44 @@ code 0 means installed and permanent.
50
58
 
51
59
  ## Requirements
52
60
 
53
- - Antigravity Desktop. Tested with version 2.28 on Windows 11.
61
+ - Antigravity Desktop. Tested with versions 2.31.0 and 2.31.2 on Windows 11
62
+ (the keeper carried the themes across that update on its own).
54
63
  macOS and Linux are supported in the code but untested.
55
64
  - [Node.js](https://nodejs.org) 22 or newer. Check with `node --version`.
56
65
 
57
66
  ## What install does
58
67
 
68
+ ### `install --native` (recommended)
69
+
70
+ 1. Stops a background process from an earlier `install`, if any, and removes
71
+ its logon entry and its folder, so an older version cannot replace the
72
+ themes.
73
+ 2. Writes the themes into Antigravity's own files. Antigravity loads
74
+ `resources/app` before its `app.asar` archive, so the app is extracted
75
+ there once and two of its files are patched: the window's preload
76
+ (`dist/preload.js`) runs the theme script, and the main process
77
+ (`dist/main.js`) runs the frame script in each sidecar pane (Swarm), which
78
+ a preload cannot reach. A stamp, `resources/app/agy-themes.json`, records
79
+ which build of the themes is in place.
80
+ 3. Adds a small Antigravity plugin, `~/.gemini/config/plugins/antigravity-themes`,
81
+ holding the **keeper**. Antigravity itself starts the keeper (as a plugin
82
+ sidecar, with its own built-in Node) every time it launches:
83
+ - when the themes are in place, the keeper exits at once;
84
+ - after an Antigravity update, which replaces `resources`, it extracts and
85
+ patches the app again (about a second) and themes the open windows right
86
+ away, so the update shows the themes on its first start.
87
+ 4. Applies the themes to the open windows and panes now.
88
+
89
+ While Antigravity runs, Windows keeps its `app.asar` locked; if an earlier
90
+ version of this tool had patched that archive, `install --native` says so.
91
+ It is harmless (Antigravity loads `resources/app`); run the command again with
92
+ Antigravity closed to restore the original archive.
93
+
94
+ Turning the plugin off in Antigravity stops the keeper: the themes then last
95
+ until the next Antigravity update.
96
+
97
+ ### `install` (background process)
98
+
59
99
  1. Copies this package to a folder in your user profile:
60
100
 
61
101
  | System | Folder |
@@ -79,9 +119,6 @@ code 0 means installed and permanent.
79
119
  To skip this, use `npx -y @materialwebunofficial/antigravity-themes install --no-autostart` and
80
120
  run `npx -y @materialwebunofficial/antigravity-themes start` when you want the themes.
81
121
 
82
- **Direct / Native Install (Zero background processes):**
83
- Use `npx -y @materialwebunofficial/antigravity-themes install --native` to inject the themes directly into Antigravity's Electron resources. This runs with ZERO background processes, no listening ports, and no startup entries.
84
-
85
122
  3. Starts a small background Node.js process. It applies the theme to every
86
123
  Antigravity window, keeps it applied when a window reloads, and waits
87
124
  quietly while Antigravity is closed.
@@ -91,7 +128,8 @@ code 0 means installed and permanent.
91
128
 
92
129
  ## Choosing a theme
93
130
 
94
- - **Matte**: open the **Theme** menu in the title bar.
131
+ - **Matte**, **Nordic Frost**, **E-Ink / Paper**: open the **Theme** menu in
132
+ the title bar.
95
133
  - **Material 3 Expressive**: the title bar menus (Antigravity, Workspace,
96
134
  View, Window, Theme) are gathered into one menu button at the top left.
97
135
  Open it, then **Theme**.
@@ -102,7 +140,10 @@ code 0 means installed and permanent.
102
140
 
103
141
  | Key | Theme |
104
142
  | ------------- | ----- |
143
+ | `antigravity` | Antigravity's own look (the themes off) |
105
144
  | `matte` | Matte (the default) |
145
+ | `nordic` | Nordic Frost |
146
+ | `paper` | E-Ink / Paper |
106
147
  | `md3e-teal` | Material 3 Expressive, Teal |
107
148
  | `md3e-ocean` | Material 3 Expressive, Ocean |
108
149
  | `md3e-forest` | Material 3 Expressive, Forest |
@@ -110,42 +151,102 @@ code 0 means installed and permanent.
110
151
  | `md3e-coral` | Material 3 Expressive, Coral |
111
152
 
112
153
  Your choice is remembered. Light and dark follow Antigravity's own
113
- appearance setting; both themes have a light and a dark version.
154
+ appearance setting; every theme has a light and a dark version.
114
155
 
115
156
  ## What the themes change
116
157
 
158
+ Each theme has its own layout language, carried through the Settings dialog
159
+ (navigation, page titles, groups, rows, switches, selects, buttons, segmented
160
+ controls, sliders, fields, option cards, key caps, usage rings and token
161
+ bars), dialogs, menus, the "Restart to Update" button and the Swarm pane.
162
+ None of them loops an animation (the app's own pulsing update button is
163
+ stilled), and hovers keep text at WCAG AA contrast or better.
164
+
117
165
  **Matte** is quiet and fast: a near-black window holding one rounded panel,
118
166
  small neutral steps between surfaces, 1px lines for structure, and no
119
- gradients, blur, glow or looping animation.
167
+ gradients, blur or glow. Settings reads like an inspector: a darker index
168
+ column with a bright bar on the current page, plain rows ruled by hairlines
169
+ under small uppercase labels, compact 28px controls in raised gray, mono key
170
+ caps, and a switch that is the one bright thing when on.
171
+
172
+ **Nordic Frost** uses the [Nord](https://www.nordtheme.com) palette: Polar
173
+ Night surfaces, Snow Storm text, Frost accents, Aurora for status. Settings
174
+ is a preferences window: a raised index column with Frost group headings,
175
+ every group a solid tile with grooves of the base color between its rows,
176
+ flat filled controls with no strokes, and Frost marking what is on or chosen.
177
+
178
+ **E-Ink / Paper** is strict monochrome for reading: ink on paper (or paper
179
+ on ink), a serif for headings and prose, square corners, rules instead of
180
+ shadows, and no in-between animation frames. Settings is a printed form: a
181
+ serif index with the current page printed in reverse, small-capital group
182
+ titles under a heavy rule, ruled boxes with dashed lines between entries,
183
+ square ink controls that print in reverse when chosen, and fields drawn as
184
+ lines to write on.
120
185
 
121
186
  **Material 3 Expressive** maps the Material 3 Expressive design system onto
122
187
  the app: its color schemes, the Roboto type scale, shapes, state layers,
123
188
  ripples and spring motion; Material Symbols Rounded icons; a navigation
124
- drawer style sidebar; the app menu button; a shape-morphing loading
189
+ drawer style sidebar (title-small group headlines, dividers between the
190
+ destinations and every group); the app menu button; a shape-morphing loading
125
191
  indicator while the agent works; and wavy progress bars for running
126
192
  background commands and tasks. The menu and the indicators are the
127
- [md3e-web](https://materialwebunofficial.github.io) components.
128
-
129
- Both themes also add:
193
+ [md3e-web](https://materialwebunofficial.github.io) components. In detail:
194
+
195
+ - **Settings** follows M3's list-detail layout: a navigation drawer beside
196
+ the page, which is a pane with large corners; settings are M3 Expressive
197
+ segmented lists (items 2px apart, 4px corners inside a group, 20px
198
+ outside) under title-small primary category headers; M3 switches (the
199
+ handle springs on the compositor), sliders, connected button groups (the
200
+ selection fades and morphs, the pressed button widens), filled text
201
+ fields, outlined selects whose lists are M3 menus, and filled or tonal
202
+ buttons whose corners close when pressed. Shape morphs run between real
203
+ radii, never from the 9999px "full" token, so the spring's overshoot
204
+ cannot flash square corners, and a ripple follows its button's box and
205
+ corners while it plays.
206
+ - **Usage** (the model menu's View Usage, and Settings > Models & Usage):
207
+ M3 circular progress indicators, with the gap between the arc and its
208
+ track; the menu's usage view is a list of two-line menu items.
209
+ - **Dialogs** are M3 dialogs, **artifact cards** (plans, reports) are filled
210
+ cards with a filled Proceed and a tonal split Open button, and **Restart to
211
+ Update** is an extra-small filled button with a refresh icon.
212
+
213
+ Every theme also adds:
130
214
 
131
215
  - **Context** at the top of the Overview panel: how full the conversation's
132
216
  context window is and what takes it up (system prompt, tools, rules, MCP
133
217
  tools, skills and so on), with a warning at 80% and at 95%. The app does
134
218
  not report context window sizes, so they are assumed by model family:
135
- Gemini 1M tokens, Claude 200k, GPT-OSS 128k.
219
+ Gemini 256k tokens, Claude 200k, GPT-OSS 128k.
136
220
  - **Token counts** under each finished agent turn, for example
137
221
  `2.5k in · 358 out · 142k cache`.
138
222
  - **Wrap lines** and **Line numbers** buttons beside "Copy code" in chat
139
223
  code blocks. Line numbers are not copied.
140
224
 
225
+ Sidecar panes such as **Swarm** follow the theme too. Each is a separate page
226
+ (a `plugin://` frame) with its own styles, so every theme has a frame style
227
+ sheet for it (`themes/frames/`), loaded inside the frame by a small frame
228
+ script. In Material 3 Expressive the Swarm board is a list of tonal rows with
229
+ primary tabs, a search bar, filter chips and an M3 switch and slider, and its
230
+ Live status is the md3e-web loading indicator: still while the board is
231
+ connected, morphing when new posts arrive, running while it reconnects.
232
+ Swarm's dropdowns use Chromium's customizable select (`appearance:
233
+ base-select`), so their lists are drawn in the page and follow each theme: an
234
+ M3 menu in Material 3 Expressive, a hairline panel in Matte, a Polar Night
235
+ tile in Nordic Frost, a ruled box in E-Ink / Paper.
236
+
237
+ Chips inside a sent message (quotes, files, terminal output) take the
238
+ message bubble's own ink in every theme, so a quote stays readable on any
239
+ bubble color.
240
+
141
241
  ## Commands
142
242
 
143
243
  Run each as `npx -y @materialwebunofficial/antigravity-themes <command>`.
144
244
 
145
245
  | Command | What it does |
146
246
  | ------- | ------------ |
247
+ | `install --native` | Write the themes into Antigravity and add the keeper plugin: no background process, kept across restarts and updates (see above) |
147
248
  | `install [--no-autostart]` | Install, add the logon entry, and start the background process (see above) |
148
- | `uninstall` | Remove everything `install` added (see below) |
249
+ | `uninstall` | Remove everything either install added (see below) |
149
250
  | `start` | Start the background process |
150
251
  | `stop` | Stop it. Open windows keep the theme until they reload |
151
252
  | `on` | Apply the themes to the open windows once, without the background process |
@@ -157,11 +258,18 @@ Run each as `npx -y @materialwebunofficial/antigravity-themes <command>`.
157
258
 
158
259
  ## Update
159
260
 
261
+ Antigravity updates need nothing: with the native install, the keeper puts
262
+ the themes back at the first start after the update.
263
+
264
+ To get a newer version of the themes:
265
+
160
266
  ```sh
161
- npx -y @materialwebunofficial/antigravity-themes@latest install
267
+ npx -y @materialwebunofficial/antigravity-themes@latest install --native
162
268
  ```
163
269
 
164
- This replaces the installed copy and restarts the background process.
270
+ This patches the app with the new build, replaces the keeper's copy and
271
+ applies the new themes to the open windows. (With the background install,
272
+ `install` without `--native` replaces its copy and restarts the process.)
165
273
 
166
274
  ## Uninstall
167
275
 
@@ -169,15 +277,34 @@ This replaces the installed copy and restarts the background process.
169
277
  npx -y @materialwebunofficial/antigravity-themes uninstall
170
278
  ```
171
279
 
172
- This stops the background process, restores the app's own look in the open
173
- windows, and deletes the logon entry and the install folder.
280
+ This removes the keeper plugin, takes the themes out of Antigravity's files
281
+ (and, with Antigravity closed, deletes the extracted `resources/app` so the
282
+ app runs from its own `app.asar` again), restores the app's own look in the
283
+ open windows, and stops a background process and deletes its logon entry and
284
+ install folder if there are any.
174
285
 
175
286
  Three small cookies stay in Antigravity's own storage (see Privacy). They
176
287
  have no effect without this tool.
177
288
 
178
289
  ## How it works
179
290
 
180
- Antigravity starts with the Chrome DevTools Protocol enabled on a local
291
+ **Native install.** Electron loads an app folder, `resources/app`, before the
292
+ `app.asar` archive. The installer extracts the archive there and adds the
293
+ scripts to two of its files: the window's preload runs the theme script in
294
+ every window as it loads, and the main process runs the frame script in each
295
+ sidecar frame (`plugin://`, such as Swarm) as it navigates. Nothing listens
296
+ on a port and nothing stays running.
297
+
298
+ An update replaces `resources`, so the themes would be gone after it. The
299
+ keeper plugin covers that: Antigravity discovers plugins in
300
+ `~/.gemini/config/plugins/`, outside its install folder, and starts their
301
+ sidecars itself at every launch. The keeper compares the stamp in
302
+ `resources/app` with its own build; when they differ (or the folder is gone)
303
+ it extracts and patches the app from its own copy of this package, then
304
+ applies the themes to the windows that are already open over the app's local
305
+ debugging port, and exits.
306
+
307
+ **Background install.** Antigravity starts with the Chrome DevTools Protocol enabled on a local
181
308
  port, on every launch, and writes that port number to a file named
182
309
  `DevToolsActivePort` in its user data folder (`%APPDATA%\Antigravity` on
183
310
  Windows, `~/Library/Application Support/Antigravity` on macOS,
@@ -191,6 +318,11 @@ each window:
191
318
  (the menu, the code block buttons, the indicators), and
192
319
  - registers the same script to run again whenever the window reloads.
193
320
 
321
+ In each sidecar frame (`plugin://`, such as Swarm) it runs the frame script
322
+ instead: only the frame host and the themes' frame style sheets. The frame
323
+ asks the window which theme, seed and light or dark mode is on, and the window
324
+ tells every frame when that changes. No other frame is touched.
325
+
194
326
  To find and stop the background process, the other commands talk to it on
195
327
  `127.0.0.1` through a random port and a random token that it writes to
196
328
  `run.json` in the install folder while it runs.
@@ -219,17 +351,23 @@ Things to know:
219
351
  | `agy-code` | The Wrap lines and Line numbers settings |
220
352
  | `agy-usage` | Running token totals for your last 20 conversations, by conversation id |
221
353
 
222
- - Stored in the install folder: `run.json` (process id, control port, token
223
- and version, while running), `run.log` (when windows were found and themed,
224
- with their internal addresses; started anew when it reaches 512 KB) and, on
225
- Windows, `start.vbs` (starts the background process with no window).
354
+ - Native install: `resources/app/agy-themes.json` (the build in place and
355
+ the Antigravity version), and `keeper.log` in the data folder Antigravity
356
+ gives the keeper (what the keeper did at each start; started anew at
357
+ 256 KB).
358
+ - Background install, in the install folder: `run.json` (process id, control
359
+ port, token and version, while running), `run.log` (when windows were found
360
+ and themed, with their internal addresses; started anew when it reaches
361
+ 512 KB) and, on Windows, `start.vbs` (starts the background process with no
362
+ window).
226
363
 
227
364
  ## Compatibility and known limits
228
365
 
229
- - The themes are written against Antigravity 2.28's interface (its class
366
+ - The themes are written against Antigravity 2.31's interface (its class
230
367
  names, element ids and component data). An app update can break parts of
231
368
  them until this package is updated. If that happens,
232
- `npx -y @materialwebunofficial/antigravity-themes off` restores the app's own look at once.
369
+ `npx -y @materialwebunofficial/antigravity-themes theme antigravity`
370
+ restores the app's own look at once, and `uninstall` removes everything.
233
371
  - Windows 11 is tested. macOS and Linux follow the platforms' usual folders
234
372
  and logon entries but have not been tested.
235
373
  - The context window sizes are assumptions (see above).
@@ -241,8 +379,9 @@ Things to know:
241
379
  - `npx -y @materialwebunofficial/antigravity-themes status` shows whether the themes are installed,
242
380
  whether the background process runs, and which Antigravity windows are
243
381
  themed.
244
- - The log is `run.log` in the install folder.
245
- - Antigravity looks wrong after an update: run `npx -y @materialwebunofficial/antigravity-themes off`
382
+ - The logs are `keeper.log` (native install) and `run.log` in the install
383
+ folder (background install).
384
+ - Antigravity looks wrong after an update: run `npx -y @materialwebunofficial/antigravity-themes theme antigravity`
246
385
  and [open an issue](https://github.com/materialwebunofficial/antigravity-themes/issues) with your Antigravity version.
247
386
 
248
387
  ## Development
@@ -262,9 +401,9 @@ at a time.
262
401
  | Path | Contents |
263
402
  | ---- | -------- |
264
403
  | `bin/cli.mjs` | The commands |
265
- | `lib/` | Finding Antigravity, the DevTools connection, logon entries |
266
- | `src/` | The scripts that run in the app: the theme host (menus, theme switching, ripples) and the context, token, code block and indicator features |
267
- | `themes/` | `index.json` (the theme list) and the CSS of each theme |
404
+ | `lib/` | Finding Antigravity, the DevTools connection, logon entries, the native install (`native.mjs`, with `asar.mjs` for the archive), the keeper (`keeper.mjs`) and its plugin (`plugin.mjs`) |
405
+ | `src/` | The scripts that run in the app: the theme host (menus, theme switching, ripples), the frame host for sidecar panes, and the context, token, code block and indicator features |
406
+ | `themes/` | `index.json` (the theme list), each theme's tokens and CSS, and `frames/`: each theme's style sheet for sidecar panes |
268
407
  | `vendor/md3e/` | The md3e-web components, the Roboto font, and their licenses |
269
408
  | `tools/` | Generators for files in `themes/` and `vendor/` (below) |
270
409
 
package/bin/cli.mjs CHANGED
@@ -4,10 +4,13 @@ import { randomUUID } from 'node:crypto';
4
4
  import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, watch as watchFiles, writeFileSync } from 'node:fs';
5
5
  import http from 'node:http';
6
6
  import path from 'node:path';
7
- import { ROOT, pageScript, REMOVE_SCRIPT, PROBE_SCRIPT, selectScript } from '../lib/bundle.mjs';
8
- import { readPort, evaluateEverywhere, watch } from '../lib/injector.mjs';
7
+ import { ROOT, pageScript, frameScript, REMOVE_SCRIPT, PROBE_SCRIPT, selectScript } from '../lib/bundle.mjs';
8
+ import { readPort, evaluateEverywhere, watch, scriptFor } from '../lib/injector.mjs';
9
9
  import { installDir, stateFile, logFile, portFile, appResourcesDir } from '../lib/paths.mjs';
10
10
  import { launch, writeAutostart, removeAutostart, hasAutostart, autostartFile } from '../lib/autostart.mjs';
11
+ import { appState, buildId, ensureApp, patchApp, removeLegacyArchive, restoreArchive, unpatchApp } from '../lib/native.mjs';
12
+ import { installPlugin, pluginDir, removePlugin } from '../lib/plugin.mjs';
13
+ import { removeTree } from '../lib/asar.mjs';
11
14
 
12
15
  const NAME = 'antigravity-themes';
13
16
  const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
@@ -101,7 +104,7 @@ async function run() {
101
104
  process.exit(1);
102
105
  });
103
106
  log(`${NAME} ${pkg.version} running from ${ROOT}`);
104
- const watcher = watch({ getSource: () => pageScript(ROOT), log });
107
+ const watcher = watch({ getSource: () => scriptFor({ page: pageScript(ROOT), frame: frameScript(ROOT) }), log });
105
108
  if (has('--dev')) {
106
109
  let timer;
107
110
  for (const dir of ['src', 'themes', 'vendor']) {
@@ -118,50 +121,66 @@ async function run() {
118
121
  const RUNTIME = ['bin', 'lib', 'src', 'themes', 'vendor', 'package.json', 'README.md', 'LICENSE', 'NOTICE'];
119
122
 
120
123
  const isThemed = (result) => Boolean(result.value && JSON.parse(result.value));
124
+ // Only the targets the themes go to: windows and sidecar frames
125
+ const PROBE = scriptFor({ page: PROBE_SCRIPT, frame: PROBE_SCRIPT });
121
126
 
122
127
  // Waits for the background process to reach the open windows, so that install
123
128
  // can report the result itself. Throws when Antigravity is not open.
124
129
  async function themedWindows() {
125
130
  let results = [];
126
131
  for (let i = 0; i < 40; i++) {
127
- results = await evaluateEverywhere(PROBE_SCRIPT);
132
+ results = await evaluateEverywhere(PROBE);
128
133
  if (results.length && results.every(isThemed)) break;
129
134
  await sleep(200);
130
135
  }
131
136
  return { themed: results.filter(isThemed).length, total: results.length };
132
137
  }
133
138
 
139
+ // The permanent install: the scripts go into Antigravity's own files
140
+ // (lib/native.mjs), and the keeper plugin (lib/plugin.mjs, lib/keeper.mjs)
141
+ // puts them back after an Antigravity update. Nothing of ours keeps running.
134
142
  async function installNative() {
135
- const { execSync } = await import('node:child_process');
136
- const resDir = appResourcesDir();
137
- const asarPath = path.join(resDir, 'app.asar');
138
- const appDir = path.join(resDir, 'app');
139
- if (!existsSync(asarPath) && !existsSync(appDir)) {
140
- fail(`Could not find Antigravity resources at ${resDir}. Is Antigravity installed?`);
143
+ const resources = appResourcesDir();
144
+ if (!existsSync(path.join(resources, 'app.asar')) && !existsSync(path.join(resources, 'app'))) {
145
+ fail(`Could not find Antigravity resources at ${resources}. Is Antigravity installed?`);
141
146
  }
142
- await stopBackground();
143
- removeAutostart();
144
-
145
- if (!existsSync(appDir)) {
146
- say('Extracting Antigravity app.asar into resources/app...');
147
- execSync(`npx -y @electron/asar extract "${asarPath}" "${appDir}"`, { stdio: 'inherit' });
147
+ if (await stopBackground()) say('Stopped the background process of an earlier install.');
148
+ const entry = removeAutostart();
149
+ if (entry) say(`Removed its logon entry: ${entry}`);
150
+ // the background install's own copy of the package is no longer used
151
+ const stale = installDir();
152
+ const background = ['bin', 'start.vbs', 'run.json'].some((f) => existsSync(path.join(stale, f)));
153
+ if (path.resolve(stale) !== path.resolve(ROOT) && background) {
154
+ try {
155
+ removeTree(stale);
156
+ say(`Removed the earlier background install: ${stale}`);
157
+ } catch (err) {
158
+ say(`Could not remove the earlier background install at ${stale} (${err.code || err.message}); it is unused.`);
159
+ }
148
160
  }
149
161
 
150
- const bundleCode = pageScript(ROOT);
151
- const bundleDest = path.join(appDir, 'dist', 'theme-bundle.js');
152
- writeFileSync(bundleDest, bundleCode, 'utf8');
153
-
154
- const preloadPath = path.join(appDir, 'dist', 'preload.js');
155
- let preloadContent = readFileSync(preloadPath, 'utf8');
156
- const marker = '// Antigravity Native Theme Injection (Zero background processes)';
157
- if (!preloadContent.includes(marker)) {
158
- const patch = `\n\n${marker}\n(function() {\n try {\n const electron = require('electron');\n const path = require('path');\n const fs = require('fs');\n const b = path.join(__dirname, 'theme-bundle.js');\n if (fs.existsSync(b)) {\n const c = fs.readFileSync(b, 'utf8');\n if (electron.webFrame) electron.webFrame.executeJavaScript(c);\n }\n } catch (e) {}\n})();\n`;
159
- appendFileSync(preloadPath, patch, 'utf8');
162
+ const page = pageScript(ROOT), frame = frameScript(ROOT), build = buildId(page, frame);
163
+ try {
164
+ if (restoreArchive(resources)) say("Restored the app's own app.asar (an earlier version had patched it).");
165
+ } catch (err) {
166
+ say(`app.asar still holds an earlier patch: it is in use while Antigravity runs (${err.code || err.message}).`);
167
+ say(' Harmless, Antigravity loads resources/app; run install again with Antigravity closed to restore it.');
160
168
  }
169
+ if (removeLegacyArchive(resources)) say('Removed app.asar.patched, left by an earlier build.');
170
+ if (ensureApp(resources)) say(`Extracted the app to ${path.join(resources, 'app')}.`);
171
+ const { appVersion } = patchApp(path.join(resources, 'app'), { page, frame, build });
172
+ say(`Patched Antigravity ${appVersion}: the themes load with every start.`);
173
+ say(`Keeper plugin: ${installPlugin(ROOT, { build, version: pkg.version })}`);
174
+ say(' Antigravity runs it at each start; after an update it puts the themes back.');
161
175
 
162
- say(`Installed natively into Antigravity (${resDir}).`);
163
- say('No background process, no host server, and no startup entry.');
164
- say('Done. The themes load permanently whenever Antigravity starts.');
176
+ try {
177
+ const results = await evaluateEverywhere(scriptFor({ page, frame }));
178
+ const applied = results.filter((r) => !r.error).length;
179
+ say(applied ? `Applied to ${applied} open window(s) and pane(s) now.` : 'Antigravity is open: the themes appear in new windows.');
180
+ } catch {
181
+ say('Antigravity is not open: the themes appear when it starts.');
182
+ }
183
+ say('Done. Permanent, across restarts and updates, with no background process and no logon entry.');
165
184
  }
166
185
 
167
186
  async function install() {
@@ -206,9 +225,13 @@ async function install() {
206
225
  }
207
226
 
208
227
  async function uninstall() {
228
+ const plugin = removePlugin();
229
+ if (plugin) say(`Removed the keeper plugin: ${plugin}`);
209
230
  if (await stopBackground()) say('Stopped the background process.');
231
+ let open = false;
210
232
  try {
211
233
  const results = await evaluateEverywhere(REMOVE_SCRIPT);
234
+ open = true;
212
235
  say(`Removed the theme from ${results.filter((r) => !r.error).length} open window(s).`);
213
236
  } catch {
214
237
  say('Antigravity is not running: nothing to remove from its windows.');
@@ -217,19 +240,32 @@ async function uninstall() {
217
240
  if (entry) say(`Removed the logon entry: ${entry}`);
218
241
  const dir = installDir();
219
242
  if (path.basename(dir) === NAME && existsSync(dir)) {
220
- rmSync(dir, { recursive: true, force: true });
243
+ removeTree(dir);
221
244
  say(`Deleted ${dir}`);
222
245
  }
223
- const appDir = path.join(appResourcesDir(), 'app');
246
+ const resources = appResourcesDir();
247
+ try {
248
+ if (restoreArchive(resources)) say("Restored the app's own app.asar.");
249
+ } catch (err) {
250
+ say(`Could not restore app.asar now (${err.code || err.message}): close Antigravity and run uninstall again.`);
251
+ }
252
+ removeLegacyArchive(resources);
253
+ const appDir = path.join(resources, 'app');
224
254
  if (existsSync(appDir)) {
225
- rmSync(appDir, { recursive: true, force: true });
226
- say(`Removed native installation at ${appDir}. Restored stock app.asar.`);
255
+ if (open) {
256
+ // the running app loads its windows from this folder: leave it, unpatched
257
+ unpatchApp(appDir);
258
+ say(`Took the themes out of ${appDir}. Close Antigravity and run uninstall again to delete that copy of the app.`);
259
+ } else {
260
+ removeTree(appDir);
261
+ say(`Removed ${appDir}: Antigravity runs from its own app.asar again.`);
262
+ }
227
263
  }
228
264
  say('Done. Antigravity keeps your last theme choice in its own storage; it has no effect without this tool.');
229
265
  }
230
266
 
231
267
  async function on() {
232
- const results = await evaluateEverywhere(pageScript(ROOT));
268
+ const results = await evaluateEverywhere(scriptFor({ page: pageScript(ROOT), frame: frameScript(ROOT) }));
233
269
  const ok = results.filter((r) => !r.error).length;
234
270
  say(`Applied to ${ok} window(s).${await running() ? '' : ' Without the background process it lasts until a window reloads; "install" makes it permanent.'}`);
235
271
  }
@@ -257,17 +293,22 @@ async function theme() {
257
293
  }
258
294
 
259
295
  async function status() {
296
+ const resources = appResourcesDir();
297
+ const app = appState(resources);
298
+ const native = app.stamp && app.extractedVersion === app.appVersion;
299
+ const keeper = existsSync(path.join(pluginDir(), 'sidecars', 'keeper', 'sidecar.json'));
300
+ say(`Native: ${native ? `patched (build ${app.stamp.build}, Antigravity ${app.appVersion})` : app.stamp ? `outdated (made for ${app.extractedVersion}, Antigravity is ${app.appVersion})` : 'no'}`);
301
+ say(`Keeper: ${keeper ? pluginDir() : 'no'}`);
260
302
  const dir = installDir();
261
303
  let installed = null;
262
304
  try { installed = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8')).version; } catch { /* not installed */ }
263
- say(`Installed: ${installed ? `${installed} in ${dir}` : 'no'}`);
264
- say(`At logon: ${hasAutostart() ? autostartFile() : 'no'}`);
305
+ say(`Background: ${installed ? `${installed} in ${dir}` : 'not installed'}${hasAutostart() ? `, at logon (${autostartFile()})` : ''}`);
265
306
  const state = await running();
266
- say(`Background: ${state ? `running (process ${state.pid}, version ${state.version})` : 'not running'}`);
307
+ if (state) say(` running (process ${state.pid}, version ${state.version})`);
267
308
  try {
268
309
  const port = readPort();
269
- const results = await evaluateEverywhere(PROBE_SCRIPT, port);
270
- say(`Antigravity: running, port ${port}, ${results.length} window(s)`);
310
+ const results = await evaluateEverywhere(PROBE, port);
311
+ say(`Antigravity: running, port ${port}, ${results.length} window(s) and pane(s)`);
271
312
  for (const r of results) {
272
313
  const host = r.value ? JSON.parse(r.value) : null;
273
314
  say(` ${host ? `themed (${host.theme})` : 'not themed'} ${r.url}`);
@@ -276,10 +317,12 @@ async function status() {
276
317
  say(`Antigravity: ${existsSync(portFile()) ? err.message : `not running (no ${portFile()})`}`);
277
318
  }
278
319
  say();
279
- if (installed && hasAutostart() && state) {
280
- say('OK: installed and permanent.');
320
+ if (native && keeper) {
321
+ say('OK: installed natively, kept across Antigravity updates.');
322
+ } else if (installed && hasAutostart() && state) {
323
+ say('OK: installed with the background process.');
281
324
  } else {
282
- say(`Not permanent. Fix it with: ${NPX} install`);
325
+ say(`Not permanent. Fix it with: ${NPX} install --native`);
283
326
  process.exitCode = 1;
284
327
  }
285
328
  }
@@ -289,16 +332,17 @@ function help() {
289
332
 
290
333
  Usage: ${NPX} <command>
291
334
 
292
- install [--native] Install the themes permanently.
293
- With --native: installs directly into Antigravity with ZERO background processes or servers.
294
- Default: copies themes to ${installDir()} and keeps them synced.
295
- uninstall Remove the themes from Antigravity, the logon entry, and native app directory
335
+ install --native Install the themes permanently, into Antigravity's own files: no background
336
+ process, no logon entry. A small plugin puts them back after an Antigravity update.
337
+ install The older way: copies the themes to ${installDir()} and themes Antigravity
338
+ from a background process started at logon.
339
+ uninstall Remove the themes, the plugin, the background process and its logon entry
296
340
  start Start the background process that themes every window (not permanent)
297
341
  stop Stop it (open windows keep the theme until they reload)
298
342
  on Apply the themes to the open windows once (not permanent)
299
343
  off Stop the background process and remove the themes from the open windows
300
344
  theme [key] List the themes, or switch every window to one
301
- status Show what is installed and running; exit code 1 if not permanent
345
+ status Show what is installed and running; exit code 1 if it is not permanent
302
346
  run [--dev] Run the background process in this terminal (--dev reloads on file changes)`);
303
347
  }
304
348