@anvil-works/anvil-cli 0.5.8 → 0.5.10

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.
Files changed (3) hide show
  1. package/README.md +280 -367
  2. package/dist/cli.js +241 -85
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,19 +1,17 @@
1
- # anvil-cli
2
-
3
- A CLI tool for syncing Anvil apps between your local filesystem and the Anvil IDE. Edit your Anvil apps in your favorite editor and sync changes in real-time.
4
-
5
- ## Features
6
-
7
- - **Real-time Sync**: Watch for file changes and automatically sync to Anvil
8
- - **OAuth Authentication**: Secure authentication via OAuth with automatic token refresh
9
- - **Auto-detection**: Automatically detects app IDs from git remotes or commit history
10
- - **Python Module Support**: Syncs server-side and client-side Python modules
11
- - **Form Support**: Syncs forms with templates and code
12
- - **Theme Assets**: Syncs theme assets and configuration
13
-
14
- ## Installation
15
-
16
- ### macOS/Linux
1
+ # anvil-cli
2
+
3
+ `anvil-cli` is the command-line tool for developing Anvil apps locally.
4
+
5
+ Use it to:
6
+
7
+ - check out an Anvil app into a local folder
8
+ - edit code in your own editor
9
+ - keep that local folder synced with Anvil while you work
10
+ - switch between local code editing and the Anvil designer in the same workflow
11
+
12
+ ## Install
13
+
14
+ ### macOS or Linux
17
15
 
18
16
  ```bash
19
17
  curl -fsSL https://anvil.works/install-cli.sh | sh
@@ -31,433 +29,348 @@ irm https://anvil.works/install-cli.ps1 | iex
31
29
  curl -fsSL https://anvil.works/install-cli.cmd -o install.cmd && install.cmd && del install.cmd
32
30
  ```
33
31
 
34
- ### Manual Fallback
32
+ ### npm fallback
35
33
 
36
34
  ```bash
37
35
  npm install -g @anvil-works/anvil-cli@latest
38
36
  ```
39
-
40
- ## Quick Start
41
37
 
42
- ### 1. Run Configure (Recommended First Step)
38
+ ## Quickstart
39
+
40
+ This is the main happy path:
41
+
42
+ 1. Install `anvil-cli`.
43
+ 2. Run `anvil configure`.
44
+ 3. Run `anvil checkout`.
45
+ 4. Change into the app directory.
46
+ 5. Run `anvil watch`.
47
+
48
+ ### 1. Configure the CLI
43
49
 
44
- Start with guided setup for your default server URL and login:
50
+ Run:
45
51
 
46
52
  ```bash
47
53
  anvil configure
48
54
  ```
49
55
 
50
- When prompted for **Default Anvil server URL**, press Enter unless you're
51
- running a dedicated enterprise/on-prem Anvil installation.
52
- `anvil configure` does not run checkout automatically; it ends by suggesting `anvil checkout`.
56
+ This guided setup helps you:
53
57
 
54
- ### 2. Check Out Your App from the IDE URL
58
+ - set your default Anvil server URL
59
+ - choose a preferred editor, if you want one
60
+ - log in to Anvil
55
61
 
56
- Copy your app URL from the Anvil IDE (for example
57
- `https://anvil.works/build/apps/W36XUTXGNPDK6VEA`) and run:
62
+ If you use `anvil.works`, you can usually accept the default server when prompted.
63
+
64
+ ### 2. Check out your app
65
+
66
+ Run:
67
+
68
+ ```bash
69
+ anvil checkout
70
+ ```
71
+
72
+ This opens an interactive app picker and creates the app directory for you.
73
+
74
+ If you already have the app URL from the Anvil editor, you can also run:
58
75
 
59
76
  ```bash
60
77
  anvil checkout https://anvil.works/build/apps/W36XUTXGNPDK6VEA my-app
61
78
  cd my-app
62
79
  ```
63
80
 
64
- ### 3. Watch Your Changes
81
+ You can also check out by app ID:
65
82
 
66
- When you're ready to sync local edits in real-time:
83
+ ```bash
84
+ anvil checkout W36XUTXGNPDK6VEA my-app --url anvil.works
85
+ cd my-app
86
+ ```
87
+
88
+ ### 3. Start syncing
89
+
90
+ Inside the app directory, run:
67
91
 
68
92
  ```bash
69
93
  anvil watch
70
94
  ```
71
95
 
72
- **Watch Options:**
73
-
74
- - `-A, --appid <APP_ID>` - Specify app ID directly
75
- - `-f, --first` - Auto-select first detected app ID without confirmation
76
- - `-s, --staged-only` - Only sync staged changes (use `git add` to stage files)
77
- - `-a, --auto` - Auto mode: restart on branch changes and sync when behind
78
- - `-O, --open` - Open watched path in your preferred editor (or default app)
79
- - `-u, --url <ANVIL_URL>` - Specify Anvil server URL (e.g., `anvil.works`, `localhost:3000`)
80
- - `-V, --verbose` - Show detailed output
81
-
82
- **Tip:** Open your app in the [Anvil IDE](https://anvil.works) to see your local changes appear in real-time. Changes made in the IDE will also sync back to your local files.
83
-
84
- ## How It Works
85
-
86
- ### Architecture
87
-
88
- ```
89
- ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
90
- │ Local Files │◄────►│ anvil-cli │◄────►│ Anvil Server │◄────►│ Anvil IDE │
91
- │(your editor) │ │ (Node.js CLI)│ │(anvil.works) │ │ (Browser) │
92
- └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
93
- ```
94
-
95
- The diagram shows bidirectional sync:
96
-
97
- - **Local → Anvil**: Your local file changes are synced to Anvil via API calls through anvil-cli
98
- - **Anvil → Local**: Changes made in the Anvil IDE are automatically detected and synced back to your local files via git fetch through anvil-cli
99
-
100
- ### CLI Commands
101
-
102
- | Command | Description |
103
- | -------------------------------------- | -------------------------------------------------------------------- |
104
- | `anvil watch [path]` | Watch directory for changes and sync to Anvil |
105
- | `anvil watch [path] -O` | Open watched path in preferred editor/default app |
106
- | `anvil watch -V` or `--verbose` | Watch with verbose logging (detailed output) |
107
- | `anvil w [path]` | Short form for watch |
108
- | `anvil configure` | Guided setup for default URL and login |
109
- | `anvil checkout [input] [directory]` | Check out an app by URL/app ID, or interactively with no input |
110
- | `anvil login [anvil-server-url]` | Authenticate with Anvil using OAuth (supports smart URL handling) |
111
- | `anvil l [anvil-server-url]` | Short form for login |
112
- | `anvil logout [anvil-server-url]` | Logout from Anvil (optionally specify URL for specific installation) |
113
- | `anvil config <action> [key] [value]` | Manage configuration (set, get, list) |
114
- | `anvil c <action> [key] [value]` | Short form for config |
115
- | `anvil version` | Show version information |
116
- | `anvil -h, --help` | Show help message |
117
- | `anvil -v, --version` | Show version number |
118
-
119
- ### App ID Detection
120
-
121
- anvil-cli can auto-detect your app ID using multiple strategies:
122
-
123
- 1. **Git Remotes**: Checks for Anvil git remotes in the repository
124
-
125
- 2. **Commit Lookup**: Queries Anvil API with current commit ID and branch to find matching app
126
-
127
- If auto-detection fails, you can specify the app ID explicitly:
128
-
129
- ```bash
130
- anvil watch -A YOUR_APP_ID
131
- ```
132
-
133
- ### Anvil URL Detection
134
-
135
- anvil-cli automatically detects which Anvil server to use:
136
-
137
- 1. **Explicit URL**: If you specify `--url` or `-u` flag
138
- 2. **Git Remotes**: Automatically extracts URL from git remotes in your repository
139
- 3. **Logged-in URLs**:
140
- - If one logged-in URL is available, uses it automatically
141
- - If multiple logged-in URLs are available, prompts you to select one
142
- 4. **Global Config**: Falls back to `anvilUrl` in config file
143
- 5. **Default**: Uses `https://anvil.works` (or `http://localhost:3000` in dev mode)
144
-
145
- **Examples:**
146
-
147
- ```bash
148
- # Auto-detect from git remote
149
- anvil watch
150
-
151
- # Specify URL explicitly
152
- anvil watch --url anvil.works
153
- anvil watch --url localhost:3000
154
- anvil watch --url anvil.mycompany.com
155
-
156
- # If multiple logged-in URLs, you'll be prompted to select
157
- anvil watch
158
- # ? Multiple Anvil installations found. Which one would you like to use?
159
- # ❯ https://anvil.works
160
- # https://anvil.company.com
161
- # Cancel
96
+ Leave this running while you work.
97
+
98
+ With `anvil watch` running:
99
+
100
+ - local code changes sync into Anvil
101
+ - compatible changes made in Anvil sync back into your local files
102
+ - you can keep using the Anvil designer when you need it
103
+
104
+ ## Core Commands
105
+
106
+ These are the commands that matter most for normal local development:
107
+
108
+ ### `anvil configure`
109
+
110
+ Guided setup for your default server URL, editor preference, and login.
111
+
112
+ ```bash
113
+ anvil configure
162
114
  ```
163
115
 
164
- ### Checkout Apps
116
+ ### `anvil checkout`
117
+
118
+ Create a local app folder from an editor URL, Git URL, app ID, or interactive picker.
119
+
120
+ ```bash
121
+ anvil checkout [input] [directory]
122
+ anvil co [input] [directory]
123
+ ```
165
124
 
166
- `anvil checkout` accepts an editor URL, git URL, bare app ID, or no input for interactive selection:
125
+ Common examples:
167
126
 
168
127
  ```bash
169
- # Interactive search/select (ordered by most recently edited)
170
128
  anvil checkout
129
+ anvil checkout https://anvil.works/build/apps/W36XUTXGNPDK6VEA my-app
130
+ anvil checkout W36XUTXGNPDK6VEA my-app --url anvil.works
131
+ anvil co -Q "dashboard"
171
132
  ```
172
133
 
173
- In interactive mode, apps are displayed as `App Name (APPID)` and support:
174
- - Arrow-key selection
175
- - Auto-pagination (fetch-ahead with local cache)
176
- - Search by app name or app ID
134
+ Useful options:
135
+
136
+ - `-O, --open` open the app folder after checkout
137
+ - `-b, --branch <BRANCH>` check out a specific branch
138
+ - `-Q, --query <QUERY>` prefill the interactive search
139
+ - `-u, --url <ANVIL_URL>` choose the Anvil installation explicitly
140
+ - `-U, --user <USERNAME>` choose the logged-in account explicitly
141
+ - `-f, --force` override destination safety checks
177
142
 
178
- In non-interactive mode, pass an explicit app input:
179
- - `anvil checkout APPID`
180
- - `anvil checkout https://anvil.works/git/APPID.git`
143
+ ### `anvil watch`
144
+
145
+ Keep your local app folder synced with Anvil while you work.
181
146
 
182
147
  ```bash
183
- # Checkout from editor URL
184
- anvil checkout http://localhost:3000/build/apps/MHVELZG5SZXK2POE/code/assets/theme.css
148
+ anvil watch [path]
149
+ anvil w [path]
150
+ ```
185
151
 
186
- # Checkout from git URL
187
- anvil checkout https://anvil.works/git/MHVELZG5SZXK2POE.git
152
+ Common examples:
188
153
 
189
- # Checkout from app ID (specify URL when needed)
190
- anvil checkout MHVELZG5SZXK2POE --url anvil.works
154
+ ```bash
155
+ anvil watch
156
+ anvil watch -s
157
+ anvil watch -a
158
+ anvil watch -O
159
+ anvil watch --url anvil.company.com
160
+ ```
191
161
 
192
- # Explicit destination directory
193
- anvil checkout MHVELZG5SZXK2POE my-local-app --url anvil.works
162
+ Useful options:
194
163
 
195
- # Clone-style options
196
- anvil checkout MHVELZG5SZXK2POE --branch master --depth 1 --single-branch
197
- anvil checkout MHVELZG5SZXK2POE --origin upstream
198
- anvil checkout MHVELZG5SZXK2POE --quiet
164
+ - `-A, --appid <APP_ID>` specify the app directly
165
+ - `-s, --staged-only` sync only staged files
166
+ - `-a, --auto` handle some branch and sync transitions automatically
167
+ - `-O, --open` open the watched path in your preferred editor
168
+ - `-u, --url <ANVIL_URL>` choose the Anvil installation explicitly
169
+ - `-U, --user <USERNAME>` choose the logged-in account explicitly
199
170
 
200
- # Seed interactive search query
201
- anvil checkout -Q "dashboard"
202
- anvil checkout --query "dashboard"
171
+ ### Other useful commands
203
172
 
204
- # Open watched path in editor/default app
205
- anvil watch -O
206
- anvil watch ./my-app -O
173
+ ```bash
174
+ anvil login
175
+ anvil logout
176
+ anvil config list
177
+ anvil config get <key>
178
+ anvil config set <key> <value>
179
+ anvil config reset
180
+ anvil version
181
+ anvil update
182
+ anvil --help
183
+ anvil <command> --help
184
+ ```
185
+
186
+ Global options:
187
+
188
+ - `-V, --verbose` show detailed output
189
+ - `--json` output NDJSON for scripting or LLM tooling
190
+
191
+ ## Recommended Workflows
192
+
193
+ ### Work between your editor and the Anvil designer
194
+
195
+ The basic loop is:
196
+
197
+ 1. Run `anvil watch` in your app folder.
198
+ 2. Edit Python locally in your editor.
199
+ 3. Open the same app in Anvil when you need the designer.
200
+ 4. Keep `anvil watch` running while you move between them.
201
+
202
+ ### Use branches by default
203
+
204
+ Create or switch to a branch before you start watching:
205
+
206
+ ```bash
207
+ git checkout -b my-feature
208
+ anvil watch
207
209
  ```
208
210
 
209
- Safety behavior:
211
+ If you switch branches while watching, `anvil watch` may prompt you to restart or resync. If you want the CLI to handle that automatically, use:
210
212
 
211
- - Blocks checkout into non-empty destination directories (unless `--force`).
212
- - Blocks checkout into paths inside an existing git repository (unless `--force`).
213
+ ```bash
214
+ anvil watch --auto
215
+ ```
216
+
217
+ ### Use staged-only mode when experimenting
218
+
219
+ If you want tighter control over what syncs to Anvil, use:
220
+
221
+ ```bash
222
+ anvil watch --staged-only
223
+ git add server_code/MyModule.py
224
+ ```
225
+
226
+ Only staged files will sync.
213
227
 
214
- Git HTTPS credentials:
228
+ ### Work with AI assistants
215
229
 
216
- - Checkout configures a clean HTTPS remote URL plus a repo-local Git credential helper (`anvil git-credential`).
217
- - The helper retrieves short-lived access tokens from your logged-in account and refreshes them as needed.
218
- - Per-app bindings are stored in local git config (`anvil.auth.<APPID>.url` and `anvil.auth.<APPID>.username`) so multi-account repos stay deterministic.
230
+ A simple pattern is:
219
231
 
220
- Checkout options:
232
+ 1. Start `anvil watch`.
233
+ 2. Open the app folder in Cursor, VS Code, or another editor.
234
+ 3. Make or review AI-generated changes locally.
235
+ 4. Test the synced result in Anvil.
221
236
 
222
- - `-O, --open` - Open destination after checkout
223
- - `-b, --branch <BRANCH>` - Checkout a specific branch
224
- - `--depth <N>` - Shallow clone with `N` commits of history
225
- - `--single-branch` - Clone only one branch
226
- - `--origin <NAME>` - Use custom remote name instead of `origin`
227
- - `--quiet` - Suppress git clone progress output
228
- - `--verbose` - Verbose git clone output
229
- - `-f, --force` - Override destination safety checks
230
- - `-u, --url <ANVIL_URL>` - Override detected Anvil URL
231
- - `-U, --user <USERNAME>` - Use a specific logged-in account
237
+ If you are iterating on messy changes, combine this with `--staged-only`.
232
238
 
233
- If multiple accounts are logged in for the same URL, `anvil checkout` will
234
- prompt you to select one (interactive mode) or require `--user` in
235
- non-interactive mode.
239
+ ## Multiple Accounts and Enterprise Installs
236
240
 
237
- ## Configuration
241
+ Most users on `anvil.works` do not need to think about this.
238
242
 
239
- ### Authentication
243
+ If you work across multiple Anvil installations or multiple logged-in accounts, the CLI can usually infer the right choice. If it cannot, you can override it with:
240
244
 
241
- Use `anvil login` to authenticate with Anvil:
245
+ - `--url` to choose the Anvil installation
246
+ - `--user` to choose the logged-in account
247
+
248
+ Examples:
249
+
250
+ ```bash
251
+ anvil login anvil.company.com
252
+ anvil checkout APPID my-app --url anvil.company.com
253
+ anvil watch --url anvil.company.com --user user@example.com
254
+ anvil logout --url anvil.company.com --user user@example.com
255
+ ```
256
+
257
+ If you want to change the default server permanently:
258
+
259
+ ```bash
260
+ anvil config set anvilUrl https://anvil.company.com
261
+ ```
262
+
263
+ ## Troubleshooting
264
+
265
+ ### `anvil` command not found
266
+
267
+ Try the install command again, then open a new terminal and run:
268
+
269
+ ```bash
270
+ anvil --help
271
+ ```
272
+
273
+ If the install script fails, use the npm fallback.
274
+
275
+ ### Login or account selection problems
276
+
277
+ Try:
242
278
 
243
279
  ```bash
244
280
  anvil login
281
+ anvil config list
282
+ ```
283
+
284
+ If you use a self-hosted installation:
285
+
286
+ ```bash
287
+ anvil login anvil.company.com
245
288
  ```
246
289
 
247
- The login flow will:
290
+ ### `anvil checkout` cannot find the app
248
291
 
249
- 1. Open your browser to the Anvil OAuth authorization page
250
- 2. Prompt you to authorize anvil-cli
251
- 3. Complete authentication automatically in the CLI
292
+ Try using the app URL directly from the Anvil editor, or pass `--url` and `--user` if the CLI is looking at the wrong installation or account.
252
293
 
253
- Your access and refresh tokens are stored in:
254
- - **macOS**: `~/Library/Preferences/anvil-cli/config.json`
255
- - **Linux**: `~/.config/anvil-cli/config.json`
256
- - **Windows**: `%APPDATA%\anvil-cli\Config\config.json`
294
+ ### Local changes are not appearing in Anvil
257
295
 
258
- Smart URL handling:
296
+ Make sure you are in the correct app directory and restart:
259
297
 
260
298
  ```bash
261
- # anvil automatically adds https://
262
- anvil login anvil.works
263
- anvil login anvil.mycompany.com
299
+ anvil watch
300
+ ```
301
+
302
+ If you are using `--staged-only`, remember that only staged files sync.
303
+
304
+ ### Changes from Anvil are not appearing locally
264
305
 
265
- # localhost uses http://localhost:3000
266
- anvil login localhost:3000
306
+ Make sure `anvil watch` is still running. If the watch session looks unhealthy or branch state changed underneath it, stop it and start it again from the correct branch and app folder.
267
307
 
268
- # full URLs also work
269
- anvil login https://anvil.works
308
+ ### The CLI is using the wrong server
309
+
310
+ Check your current config:
311
+
312
+ ```bash
313
+ anvil config list
270
314
  ```
271
315
 
272
- anvil-cli supports multiple Anvil installations simultaneously. Each URL has
273
- its own authentication tokens.
274
-
275
- ### Config File Location
276
-
277
- - **macOS**: `~/Library/Preferences/anvil-cli/config.json`
278
- - **Linux**: `~/.config/anvil-cli/config.json`
279
- - **Windows**: `%APPDATA%\anvil-cli\Config\config.json`
280
-
281
- ### Stored Configuration
282
-
283
- `config.json` stores settings and auth metadata. OAuth secrets are stored in the
284
- OS keychain when available.
285
-
286
- ```json
287
- {
288
- "anvilUrl": "https://anvil.works",
289
- "verbose": false,
290
- "preferredEditor": "cursor",
291
- "authTokens": {
292
- "https://anvil.works": {
293
- "user@example.com": {
294
- "authToken": null,
295
- "refreshToken": null,
296
- "authTokenExpiresAt": 1735689600
297
- }
298
- }
299
- }
300
- }
316
+ Set the default explicitly if needed:
317
+
318
+ ```bash
319
+ anvil config set anvilUrl https://anvil.company.com
301
320
  ```
302
321
 
303
- If keychain is unavailable (for example some CI/headless environments), tokens
304
- fall back to `config.json`.
305
-
306
- ### Verbose Logging
307
-
308
- By default, anvil-cli shows minimal output. For more detailed logging:
309
-
310
- **One-time:** Use the `-V` or `--verbose` flag:
311
-
312
- ```bash
313
- anvil watch --verbose
314
- ```
315
-
316
- **Persistent:** Set verbose mode in config:
317
-
318
- ```bash
319
- anvil config set verbose true
322
+ ## Config File
323
+
324
+ `anvil-cli` stores local settings in `config.json`.
325
+
326
+ Common locations:
327
+
328
+ - macOS: `~/Library/Preferences/anvil-cli/config.json`
329
+ - Linux: `~/.config/anvil-cli/config.json`
330
+ - Windows: `%APPDATA%\\anvil-cli\\Config\\config.json`
331
+
332
+ This includes settings such as:
333
+
334
+ - `anvilUrl`
335
+ - `preferredEditor`
336
+ - `verbose`
337
+
338
+ Authentication secrets are stored in the OS keychain when available.
339
+
340
+ ## Developing `anvil-cli`
341
+
342
+ Build the CLI:
343
+
344
+ ```bash
345
+ pnpm build
346
+ ```
347
+
348
+ Run in watch mode:
349
+
350
+ ```bash
351
+ pnpm dev
320
352
  ```
321
353
 
322
- ### Preferred Editor
354
+ Type-check:
323
355
 
324
- Set this during `anvil configure`.
356
+ ```bash
357
+ pnpm tsc --noEmit
358
+ ```
325
359
 
326
- If needed later, you can still set it manually with:
360
+ Run tests:
327
361
 
328
362
  ```bash
329
- anvil config set preferredEditor <editor>
363
+ source tests/setup-test-env.sh
364
+ pnpm test
330
365
  ```
331
366
 
332
- `preferredEditor` is used by checkout for post-checkout guidance and `-O/--open`.
333
- It is also used by `anvil watch -O`.
334
-
335
- Verbose output includes timestamped diagnostic lines with file routing details, sync status checks, and other internal processing information.
336
-
337
- ### Enterprise Configuration
338
-
339
- If you're using an Anvil server other than `anvil.works` (e.g., enterprise/on-premise installation), you can work with multiple installations:
340
-
341
- **Option 1: Login with URL** (recommended)
342
-
343
- ```bash
344
- # Login to your enterprise installation
345
- anvil login anvil.mycompany.com
346
-
347
- # Or with full URL
348
- anvil login https://anvil.mycompany.com
349
- ```
350
-
351
- **Option 2: Specify URL when syncing**
352
-
353
- ```bash
354
- # Sync with specific URL
355
- anvil watch --url anvil.mycompany.com
356
- ```
357
-
358
- **Option 3: Set global default**
359
-
360
- ```bash
361
- anvil config set anvilUrl https://anvil.mycompany.com
362
- anvil login
363
- ```
364
-
365
- **Multiple Installations:**
366
-
367
- anvil-cli supports multiple Anvil installations simultaneously. Each installation has its own authentication tokens:
368
-
369
- ```bash
370
- # Login to multiple installations
371
- anvil login anvil.works
372
- anvil login anvil.company-a.com
373
- anvil login anvil.company-b.com
374
-
375
- # If URL is not resolved from git remotes and multiple logged-in URLs exist,
376
- # you'll be prompted to select which one to use
377
- # Or specify explicitly:
378
- anvil watch --url anvil.company-a.com
367
+ Run one test file:
368
+
369
+ ```bash
370
+ source tests/setup-test-env.sh
371
+ pnpm rstest tests/some.test.ts
379
372
  ```
380
-
381
- **URL Resolution Priority:**
382
-
383
- 1. Explicit `--url` flag
384
- 2. Git remote detection (from current repository)
385
- 3. Logged-in URL selection:
386
- - One URL: auto-select
387
- - Multiple URLs: prompt to select
388
- 4. `anvilUrl` in config file
389
- 5. Default based on `NODE_ENV`:
390
- - Production: `https://anvil.works`
391
- - Development: `http://localhost:3000`
392
-
393
- View all configuration:
394
-
395
- ```bash
396
- anvil config list
397
- ```
398
-
399
- ### Logout
400
-
401
- Logout from Anvil installations:
402
-
403
- **Logout from all accounts:**
404
-
405
- ```bash
406
- anvil logout
407
- ```
408
-
409
- **Logout from specific installation:**
410
-
411
- ```bash
412
- anvil logout anvil.works
413
- anvil logout anvil.mycompany.com
414
- anvil logout --url localhost:3000
415
- ```
416
-
417
- **Without URL:** `anvil logout` uses total logged-in account count across all URLs:
418
-
419
- - If you have **one account total**, it logs out immediately
420
- - If you have **multiple accounts total**, you'll be prompted to:
421
-
422
- - Logout from one account (then select which one)
423
- - Logout from all accounts
424
- - Cancel
425
-
426
- ## Troubleshooting
427
-
428
- ### "No app ID found"
429
-
430
- - Ensure you're in an Anvil app directory with `anvil.yaml`
431
- - Check git remotes: `git remote -v`
432
- - Specify app ID explicitly: `anvil watch -A YOUR_APP_ID`
433
- - Use `-f, --first` to auto-select the first detected app ID: `anvil watch -f`
434
-
435
- ### "Authentication failed" or "Not logged in to [URL]"
436
-
437
- - Run `anvil login [url]` to authenticate with the specific Anvil installation
438
- - If you see "Not logged in to [URL]", you need to login to that specific URL:
439
-
440
- ```bash
441
- anvil login https://anvil.mycompany.com
442
- ```
443
-
444
- - Your access token may have expired and refresh failed - re-login to get new tokens
445
- - For multiple installations, make sure you're logged in to the correct one
446
-
447
- ### Switch back to hosted Anvil
448
-
449
- - Reset the server URL to the default cloud host:
450
-
451
- ```bash
452
- anvil config set anvilUrl https://anvil.works
453
- ```
454
-
455
- - Or reset all config values (also clears custom URLs and logs out all accounts):
456
-
457
- ```bash
458
- anvil config reset
459
- ```
460
-
461
- ## License
462
-
373
+
374
+ ## License
375
+
463
376
  MIT
package/dist/cli.js CHANGED
@@ -42538,6 +42538,13 @@ var __webpack_exports__ = {};
42538
42538
  function getConfig(key) {
42539
42539
  return config_config.get(key);
42540
42540
  }
42541
+ function deleteConfig(key) {
42542
+ if (key in configDefaults) config_config.set(key, configDefaults[key]);
42543
+ else {
42544
+ const store = config_config.store;
42545
+ if (store && "object" == typeof store && key in store) config_config.delete(key);
42546
+ }
42547
+ }
42541
42548
  function resetConfig() {
42542
42549
  for (const [key, value] of Object.entries(configDefaults))config_config.set(key, value);
42543
42550
  const currentStore = config_config.store;
@@ -42670,7 +42677,7 @@ var __webpack_exports__ = {};
42670
42677
  ...config
42671
42678
  };
42672
42679
  }
42673
- function globalConfig_getGlobalOutputConfig() {
42680
+ function getGlobalOutputConfig() {
42674
42681
  return globalOutputConfig;
42675
42682
  }
42676
42683
  const logger_timestamp = ()=>new Date().toISOString();
@@ -42691,10 +42698,20 @@ var __webpack_exports__ = {};
42691
42698
  function writeJson(entry) {
42692
42699
  console.log(JSON.stringify(entry));
42693
42700
  }
42701
+ function logJsonResult(success, options) {
42702
+ if (getGlobalOutputConfig().jsonMode) writeJson({
42703
+ type: "result",
42704
+ success,
42705
+ message: options?.message,
42706
+ data: options?.data,
42707
+ error: options?.error,
42708
+ timestamp: logger_timestamp()
42709
+ });
42710
+ }
42694
42711
  class DefaultLogger {
42695
42712
  paused = false;
42696
42713
  get jsonMode() {
42697
- return globalConfig_getGlobalOutputConfig().jsonMode;
42714
+ return getGlobalOutputConfig().jsonMode;
42698
42715
  }
42699
42716
  log(...args) {
42700
42717
  if (this.paused) return;
@@ -42796,7 +42813,7 @@ var __webpack_exports__ = {};
42796
42813
  this.sessionVerbose = options.verbose ?? false;
42797
42814
  }
42798
42815
  get jsonMode() {
42799
- return globalConfig_getGlobalOutputConfig().jsonMode;
42816
+ return getGlobalOutputConfig().jsonMode;
42800
42817
  }
42801
42818
  isVerbose() {
42802
42819
  return this.sessionVerbose || getConfig("verbose");
@@ -50593,7 +50610,7 @@ var __webpack_exports__ = {};
50593
50610
  const execFile = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_.execFile);
50594
50611
  const open_dirname = external_node_path_.dirname((0, external_node_url_namespaceObject.fileURLToPath)(__rslib_import_meta_url__));
50595
50612
  const localXdgOpenPath = external_node_path_.join(open_dirname, 'xdg-open');
50596
- const { platform, arch } = external_node_process_;
50613
+ const { platform: open_platform, arch } = external_node_process_;
50597
50614
  async function getWindowsDefaultBrowserFromWsl() {
50598
50615
  const powershellPath = await powerShellPath();
50599
50616
  const rawCommand = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
@@ -50686,13 +50703,13 @@ var __webpack_exports__ = {};
50686
50703
  let command;
50687
50704
  const cliArguments = [];
50688
50705
  const childProcessOptions = {};
50689
- if ('darwin' === platform) {
50706
+ if ('darwin' === open_platform) {
50690
50707
  command = 'open';
50691
50708
  if (options.wait) cliArguments.push('--wait-apps');
50692
50709
  if (options.background) cliArguments.push('--background');
50693
50710
  if (options.newInstance) cliArguments.push('--new');
50694
50711
  if (app) cliArguments.push('-a', app);
50695
- } else if ('win32' !== platform && (!is_wsl || isInsideContainer() || app)) {
50712
+ } else if ('win32' !== open_platform && (!is_wsl || isInsideContainer() || app)) {
50696
50713
  if (app) command = app;
50697
50714
  else {
50698
50715
  const isBundled = !open_dirname || '/' === open_dirname;
@@ -50701,7 +50718,7 @@ var __webpack_exports__ = {};
50701
50718
  await external_node_fs_promises_namespaceObject.access(localXdgOpenPath, external_node_fs_promises_namespaceObject.constants.X_OK);
50702
50719
  exeLocalXdgOpen = true;
50703
50720
  } catch {}
50704
- const useSystemXdgOpen = external_node_process_.versions.electron ?? ('android' === platform || isBundled || !exeLocalXdgOpen);
50721
+ const useSystemXdgOpen = external_node_process_.versions.electron ?? ('android' === open_platform || isBundled || !exeLocalXdgOpen);
50705
50722
  command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
50706
50723
  }
50707
50724
  if (appArguments.length > 0) cliArguments.push(...appArguments);
@@ -50727,7 +50744,7 @@ var __webpack_exports__ = {};
50727
50744
  }
50728
50745
  options.target = external_node_buffer_namespaceObject.Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
50729
50746
  }
50730
- if ('darwin' === platform && appArguments.length > 0) cliArguments.push('--args', ...appArguments);
50747
+ if ('darwin' === open_platform && appArguments.length > 0) cliArguments.push('--args', ...appArguments);
50731
50748
  if (options.target) cliArguments.push(options.target);
50732
50749
  const subprocess = external_node_child_process_.spawn(command, cliArguments, childProcessOptions);
50733
50750
  if (options.wait) return new Promise((resolve, reject)=>{
@@ -50753,9 +50770,9 @@ var __webpack_exports__ = {};
50753
50770
  if (!archBinary) throw new Error(`${arch} is not supported`);
50754
50771
  return archBinary;
50755
50772
  }
50756
- function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
50773
+ function detectPlatformBinary({ [open_platform]: platformBinary }, { wsl }) {
50757
50774
  if (wsl && is_wsl) return detectArchBinary(wsl);
50758
- if (!platformBinary) throw new Error(`${platform} is not supported`);
50775
+ if (!platformBinary) throw new Error(`${open_platform} is not supported`);
50759
50776
  return detectArchBinary(platformBinary);
50760
50777
  }
50761
50778
  const open_apps = {};
@@ -50882,6 +50899,11 @@ var __webpack_exports__ = {};
50882
50899
  if ("win32" === process.platform) return tokens.map(quoteForCmdShell).join(" ");
50883
50900
  return tokens.map(quoteForPosixShell).join(" ");
50884
50901
  }
50902
+ function formatFallbackEditorLabel(preferredEditorCommand) {
50903
+ const trimmed = preferredEditorCommand.trim();
50904
+ if (!trimmed) return "your editor";
50905
+ return trimmed;
50906
+ }
50885
50907
  async function openPathInEditorOrDefault(targetPath, preferredEditorCommand, deps = defaultDeps) {
50886
50908
  if (preferredEditorCommand) if (deps.isCommandAvailable(preferredEditorCommand)) try {
50887
50909
  const tokens = parseCommandTokens(preferredEditorCommand);
@@ -50892,9 +50914,9 @@ var __webpack_exports__ = {};
50892
50914
  ]));
50893
50915
  return;
50894
50916
  } catch (error) {
50895
- logger_logger.warn(`Failed to open preferred editor '${preferredEditorCommand}': ${errors_getErrorMessage(error)}. Falling back to the system default app.`);
50917
+ logger_logger.warn(`Failed to open ${formatFallbackEditorLabel(preferredEditorCommand)} automatically: ${errors_getErrorMessage(error)}.`);
50896
50918
  }
50897
- else logger_logger.warn(`Preferred editor command '${preferredEditorCommand}' was not found in PATH. Falling back to the system default app.`);
50919
+ else logger_logger.info(`Open ${formatFallbackEditorLabel(preferredEditorCommand)} to edit your app.`);
50898
50920
  await deps.openSystem(targetPath);
50899
50921
  }
50900
50922
  function decideFallbackUrl(explicitUrl, availableUrls = getAvailableAnvilUrls()) {
@@ -51613,7 +51635,7 @@ var __webpack_exports__ = {};
51613
51635
  if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
51614
51636
  else logger_logger.error(`Not logged in to ${anvilUrl}`);
51615
51637
  logger_logger.verbose(chalk_source.yellow("Please log in first:"));
51616
- console.log(chalk_source.cyan(` anvil login ${anvilUrl.replace(/^https?:\/\//, "")}`));
51638
+ logger_logger.info(chalk_source.cyan(` anvil login ${anvilUrl.replace(/^https?:\/\//, "")}`));
51617
51639
  process.exit(1);
51618
51640
  }
51619
51641
  logger_logger.verbose(chalk_source.green("✓ Authentication tokens found"));
@@ -52803,6 +52825,54 @@ var __webpack_exports__ = {};
52803
52825
  await deps.logout();
52804
52826
  deps.resetConfig();
52805
52827
  }
52828
+ function performConfigDelete(key) {
52829
+ const allConfig = getAllConfig();
52830
+ const existed = Object.prototype.hasOwnProperty.call(allConfig, key);
52831
+ deleteConfig(key);
52832
+ return {
52833
+ existed
52834
+ };
52835
+ }
52836
+ function getConfigListData() {
52837
+ const allConfig = getAllConfig();
52838
+ const resolvedUrl = resolveAnvilUrl();
52839
+ const defaultUrl = isDevMode() ? "http://localhost:3000" : "https://anvil.works";
52840
+ const authTokens = allConfig.authTokens;
52841
+ const urls = authTokens && "object" == typeof authTokens ? Object.keys(authTokens).sort() : [];
52842
+ const accounts = [];
52843
+ for (const url of urls){
52844
+ const urlData = authTokens?.[url];
52845
+ if (!(!urlData || "object" != typeof urlData || Array.isArray(urlData))) for (const username of Object.keys(urlData).sort()){
52846
+ const accountData = urlData[username];
52847
+ const hasConfigTokens = !!(accountData?.authToken || accountData?.refreshToken);
52848
+ const hasAnyTokens = hasTokensForUrl(url, username);
52849
+ accounts.push({
52850
+ url,
52851
+ username,
52852
+ status: hasConfigTokens ? "config" : hasAnyTokens ? "keychain" : "none"
52853
+ });
52854
+ }
52855
+ }
52856
+ const skipKeys = new Set([
52857
+ "anvilUrl",
52858
+ "authTokens"
52859
+ ]);
52860
+ const values = {};
52861
+ for (const [key, value] of Object.entries(allConfig))if (!skipKeys.has(key)) values[key] = value;
52862
+ return {
52863
+ anvilUrl: {
52864
+ value: resolvedUrl,
52865
+ defaultValue: defaultUrl,
52866
+ isDefault: resolvedUrl === defaultUrl
52867
+ },
52868
+ authTokens: {
52869
+ totalUrls: urls.length,
52870
+ totalAccounts: accounts.filter((account)=>"none" !== account.status).length,
52871
+ accounts
52872
+ },
52873
+ values
52874
+ };
52875
+ }
52806
52876
  function registerConfigCommand(program) {
52807
52877
  const configCommand = program.command("config").description("Manage configuration").alias("c");
52808
52878
  configCommand.command("get <key>").description("Get a configuration value").action(async (key)=>{
@@ -52829,49 +52899,35 @@ var __webpack_exports__ = {};
52829
52899
  process.exit(1);
52830
52900
  }
52831
52901
  });
52902
+ configCommand.command("delete <key>").alias("unset").description("Delete a configuration value or reset it to its default").action(async (key)=>{
52903
+ try {
52904
+ const result = performConfigDelete(key);
52905
+ if (result.existed) logger_logger.success(`Deleted ${key}`);
52906
+ else logger_logger.warn(`Key '${key}' was not set; applied default/empty value if applicable.`);
52907
+ } catch (error) {
52908
+ logger_logger.error(error instanceof Error ? error.message : String(error));
52909
+ process.exit(1);
52910
+ }
52911
+ });
52832
52912
  configCommand.command("list").description("List all configuration values").action(async ()=>{
52833
52913
  try {
52834
- const allConfig = getAllConfig();
52914
+ const configData = getConfigListData();
52915
+ if (getGlobalOutputConfig().jsonMode) return void logJsonResult(true, {
52916
+ data: configData
52917
+ });
52835
52918
  console.log(chalk_source.bold("\nConfiguration:"));
52836
52919
  console.log(chalk_source.gray("─".repeat(50)));
52837
- const resolvedUrl = resolveAnvilUrl();
52838
- const defaultUrl = isDevMode() ? "http://localhost:3000" : "https://anvil.works";
52839
- const defaultHint = resolvedUrl === defaultUrl ? "" : chalk_source.gray(` (default: ${defaultUrl})`);
52840
- console.log(chalk_source.cyan("anvilUrl") + ` = ${resolvedUrl}${defaultHint}`);
52841
- const authTokens = allConfig.authTokens;
52842
- if (authTokens && "object" == typeof authTokens) {
52843
- const urls = Object.keys(authTokens).sort();
52844
- if (urls.length > 0) {
52845
- let totalAccounts = 0;
52846
- for (const url of urls){
52847
- const urlData = authTokens[url];
52848
- if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
52849
- const usernames = Object.keys(urlData);
52850
- totalAccounts += usernames.filter((username)=>hasTokensForUrl(url, username)).length;
52851
- }
52852
- }
52853
- console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} logged-in account(s) across ${urls.length} URL(s)`)}`);
52854
- for (const url of urls){
52855
- const urlData = authTokens[url];
52856
- if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
52857
- const usernames = Object.keys(urlData).sort();
52858
- if (usernames.length > 0) for (const username of usernames){
52859
- const accountData = urlData[username];
52860
- const hasConfigTokens = !!(accountData?.authToken || accountData?.refreshToken);
52861
- const hasAnyTokens = hasTokensForUrl(url, username);
52862
- const status = hasConfigTokens ? chalk_source.green("✓ logged in (config)") : hasAnyTokens ? chalk_source.green("✓ logged in (keychain)") : chalk_source.gray("(no tokens)");
52863
- console.log(chalk_source.gray(` ${url}: ${username} ${status}`));
52864
- }
52865
- }
52866
- }
52867
- } else console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray("(no accounts)")}`);
52920
+ const defaultHint = configData.anvilUrl.isDefault ? "" : chalk_source.gray(` (default: ${configData.anvilUrl.defaultValue})`);
52921
+ console.log(chalk_source.cyan("anvilUrl") + ` = ${configData.anvilUrl.value}${defaultHint}`);
52922
+ if (configData.authTokens.totalUrls > 0) {
52923
+ console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${configData.authTokens.totalAccounts} logged-in account(s) across ${configData.authTokens.totalUrls} URL(s)`)}`);
52924
+ for (const account of configData.authTokens.accounts){
52925
+ const status = "config" === account.status ? chalk_source.green("✓ logged in (config)") : "keychain" === account.status ? chalk_source.green("✓ logged in (keychain)") : chalk_source.gray("(no tokens)");
52926
+ console.log(chalk_source.gray(` ${account.url}: ${account.username} ${status}`));
52927
+ }
52868
52928
  } else console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray("(none)")}`);
52869
- const skipKeys = new Set([
52870
- "anvilUrl",
52871
- "authTokens"
52872
- ]);
52873
- for (const [k, v] of Object.entries(allConfig))if (!skipKeys.has(k)) if (null === v) console.log(chalk_source.cyan(k) + ` = ${chalk_source.gray("null")}`);
52874
- else console.log(chalk_source.cyan(k) + ` = ${v}`);
52929
+ for (const [key, value] of Object.entries(configData.values))if (null === value) console.log(chalk_source.cyan(key) + ` = ${chalk_source.gray("null")}`);
52930
+ else console.log(chalk_source.cyan(key) + ` = ${value}`);
52875
52931
  console.log(chalk_source.gray("─".repeat(50)));
52876
52932
  console.log();
52877
52933
  } catch (e) {
@@ -52889,11 +52945,23 @@ var __webpack_exports__ = {};
52889
52945
  }
52890
52946
  });
52891
52947
  }
52948
+ function getVersionInfo(version) {
52949
+ return {
52950
+ version,
52951
+ nodeVersion: process.version,
52952
+ platform: process.platform,
52953
+ arch: process.arch
52954
+ };
52955
+ }
52892
52956
  function registerVersionCommand(program, version) {
52893
52957
  program.command("version").description("Show version information").action(()=>{
52894
- console.log(chalk_source.cyan.bold(`\n⚙ anvil ${version}`));
52895
- console.log(chalk_source.gray(` Node.js ${process.version}`));
52896
- console.log(chalk_source.gray(` Platform: ${process.platform} ${process.arch}`));
52958
+ const info = getVersionInfo(version);
52959
+ if (getGlobalOutputConfig().jsonMode) return void logJsonResult(true, {
52960
+ data: info
52961
+ });
52962
+ console.log(chalk_source.cyan.bold(`\n⚙ anvil ${info.version}`));
52963
+ console.log(chalk_source.gray(` Node.js ${info.nodeVersion}`));
52964
+ console.log(chalk_source.gray(` Platform: ${info.platform} ${info.arch}`));
52897
52965
  console.log();
52898
52966
  });
52899
52967
  }
@@ -52903,6 +52971,68 @@ var __webpack_exports__ = {};
52903
52971
  runInteractiveLoginFlow: runInteractiveLoginFlow
52904
52972
  };
52905
52973
  const DEFAULT_ANVIL_SERVER_URL_PROMPT = "Default Anvil server URL";
52974
+ const CUSTOM_EDITOR_CHOICE = "__custom_editor_command__";
52975
+ function formatCustomEditorChoice(currentEditor) {
52976
+ if (!currentEditor) return "Enter custom editor command";
52977
+ return `Custom editor command (${currentEditor})`;
52978
+ }
52979
+ async function promptForPreferredEditor(currentEditor, installedEditors, ui) {
52980
+ if (0 === installedEditors.length) {
52981
+ ui.warn("No supported editor command was found in PATH.");
52982
+ ui.info(chalk_source.gray("If your editor is installed, its shell command may not be available in this terminal."));
52983
+ const action = await ui.select("Preferred editor", [
52984
+ {
52985
+ name: "None",
52986
+ value: ""
52987
+ },
52988
+ {
52989
+ name: formatCustomEditorChoice(currentEditor),
52990
+ value: CUSTOM_EDITOR_CHOICE
52991
+ }
52992
+ ], currentEditor ? CUSTOM_EDITOR_CHOICE : "");
52993
+ if (action !== CUSTOM_EDITOR_CHOICE) return action;
52994
+ const customDefault = currentEditor && !preferredEditors.includes(currentEditor) ? currentEditor : "";
52995
+ const answer = await ui.prompt([
52996
+ {
52997
+ type: "input",
52998
+ name: "preferredEditorCommand",
52999
+ message: "Custom editor command",
53000
+ default: customDefault
53001
+ }
53002
+ ]);
53003
+ return answer.preferredEditorCommand.trim();
53004
+ }
53005
+ if (installedEditors.length < preferredEditors.length) {
53006
+ const unavailableCount = preferredEditors.length - installedEditors.length;
53007
+ ui.verbose(chalk_source.gray(`${unavailableCount} supported editor(s) are not installed and were hidden.`));
53008
+ }
53009
+ const editorChoices = [
53010
+ {
53011
+ name: "None",
53012
+ value: ""
53013
+ },
53014
+ ...installedEditors.map((editor)=>({
53015
+ name: editor,
53016
+ value: editor
53017
+ })),
53018
+ {
53019
+ name: formatCustomEditorChoice(currentEditor),
53020
+ value: CUSTOM_EDITOR_CHOICE
53021
+ }
53022
+ ];
53023
+ const defaultValue = editorChoices.some((choice)=>choice.value === currentEditor) ? currentEditor : currentEditor ? CUSTOM_EDITOR_CHOICE : "";
53024
+ const selectedEditor = await ui.select("Preferred editor", editorChoices, defaultValue);
53025
+ if (selectedEditor !== CUSTOM_EDITOR_CHOICE) return selectedEditor;
53026
+ const answer = await ui.prompt([
53027
+ {
53028
+ type: "input",
53029
+ name: "preferredEditorCommand",
53030
+ message: "Custom editor command",
53031
+ default: currentEditor && !installedEditors.includes(currentEditor) ? currentEditor : ""
53032
+ }
53033
+ ]);
53034
+ return answer.preferredEditorCommand.trim();
53035
+ }
52906
53036
  async function getConfigureVersionStatus(currentVersion, latestVersionGetter = getLatestVersion) {
52907
53037
  const latestVersion = await latestVersionGetter();
52908
53038
  if (!latestVersion) return {
@@ -52961,25 +53091,11 @@ var __webpack_exports__ = {};
52961
53091
  const configuredAnvilUrl = parseConfigSetValue("anvilUrl", urlAnswer.anvilUrl);
52962
53092
  setConfig("anvilUrl", configuredAnvilUrl);
52963
53093
  logger_logger.success(`Set default Anvil server URL (anvilUrl) = ${configuredAnvilUrl}`);
52964
- const currentEditor = String(getConfig("preferredEditor") || "").trim().toLowerCase();
53094
+ const currentEditor = String(getConfig("preferredEditor") || "").trim();
53095
+ const currentEditorNormalized = currentEditor.toLowerCase();
52965
53096
  const installedEditors = getInstalledPreferredEditors();
52966
- if (0 === installedEditors.length) logger_logger.warn("No supported editor command was found in PATH. Leaving preferredEditor unset is recommended.");
52967
- else if (installedEditors.length < preferredEditors.length) {
52968
- const unavailableCount = preferredEditors.length - installedEditors.length;
52969
- logger_logger.verbose(chalk_source.gray(`${unavailableCount} supported editor(s) are not installed and were hidden.`));
52970
- }
52971
- const editorChoices = [
52972
- {
52973
- name: "None",
52974
- value: ""
52975
- },
52976
- ...installedEditors.map((editor)=>({
52977
- name: editor,
52978
- value: editor
52979
- }))
52980
- ];
52981
- if (currentEditor && !editorChoices.some((choice)=>choice.value === currentEditor)) logger_logger.warn(`Configured preferredEditor '${currentEditor}' is not currently available in PATH.`);
52982
- const selectedEditor = await logger_logger.select("Preferred editor", editorChoices, editorChoices.some((choice)=>choice.value === currentEditor) ? currentEditor : "");
53097
+ if (currentEditor && installedEditors.length > 0 && !installedEditors.includes(currentEditorNormalized)) logger_logger.warn(`Configured preferredEditor '${currentEditor}' is not currently available in PATH.`);
53098
+ const selectedEditor = await promptForPreferredEditor(currentEditor, installedEditors, logger_logger);
52983
53099
  setConfig("preferredEditor", selectedEditor);
52984
53100
  if (selectedEditor) logger_logger.success(`Set preferredEditor = ${selectedEditor}`);
52985
53101
  else logger_logger.info("Left preferredEditor unset.");
@@ -53025,11 +53141,23 @@ var __webpack_exports__ = {};
53025
53141
  getLatestVersion().then((latestVersion)=>{
53026
53142
  if (latestVersion && semver_default().valid(VERSION) && semver_default().valid(latestVersion) && semver_default().lt(VERSION, latestVersion)) {
53027
53143
  logger_logger.warn("A new version of anvil is available!");
53028
- console.log(chalk_source.gray(` Current: ${VERSION} → Latest: ${latestVersion}`) + chalk_source.cyan("\n Run 'anvil update' to see update commands."));
53029
- console.log();
53144
+ logger_logger.info(chalk_source.gray(` Current: ${VERSION} → Latest: ${latestVersion}`) + chalk_source.cyan("\n Run 'anvil update' to see update commands."));
53030
53145
  }
53031
53146
  }).catch(()=>{});
53032
53147
  }
53148
+ function getUpdateInstructions(platform) {
53149
+ if ("win32" === platform) return [
53150
+ "try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }",
53151
+ "curl -fsSL https://anvil.works/install-cli.cmd -o install.cmd && install.cmd && del install.cmd || npm install -g @anvil-works/anvil-cli@latest",
53152
+ "npm install -g @anvil-works/anvil-cli@latest",
53153
+ "pnpm add -g @anvil-works/anvil-cli@latest"
53154
+ ];
53155
+ return [
53156
+ "curl -fsSL https://anvil.works/install-cli.sh | sh || npm install -g @anvil-works/anvil-cli@latest",
53157
+ "npm install -g @anvil-works/anvil-cli@latest",
53158
+ "pnpm add -g @anvil-works/anvil-cli@latest"
53159
+ ];
53160
+ }
53033
53161
  async function handleUpdateCommand() {
53034
53162
  try {
53035
53163
  logger_logger.progress("update", "Checking for updates...");
@@ -53044,26 +53172,54 @@ var __webpack_exports__ = {};
53044
53172
  process.exit(1);
53045
53173
  }
53046
53174
  if (semver_default().eq(VERSION, latestVersion)) {
53047
- logger_logger.success(`You're already using the latest version: ${VERSION}`);
53048
- console.log();
53175
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
53176
+ data: {
53177
+ currentVersion: VERSION,
53178
+ latestVersion,
53179
+ status: "up-to-date"
53180
+ }
53181
+ });
53182
+ else {
53183
+ logger_logger.success(`You're already using the latest version: ${VERSION}`);
53184
+ console.log();
53185
+ }
53049
53186
  return;
53050
53187
  }
53051
53188
  if (semver_default().gt(VERSION, latestVersion)) {
53052
- logger_logger.warn(`You're using version ${VERSION}, which is newer than the latest published version ${latestVersion}.`);
53053
- console.log(chalk_source.gray(" This might be a development build."));
53054
- console.log();
53189
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
53190
+ data: {
53191
+ currentVersion: VERSION,
53192
+ latestVersion,
53193
+ status: "ahead",
53194
+ note: "This might be a development build."
53195
+ }
53196
+ });
53197
+ else {
53198
+ logger_logger.warn(`You're using version ${VERSION}, which is newer than the latest published version ${latestVersion}.`);
53199
+ console.log(chalk_source.gray(" This might be a development build."));
53200
+ console.log();
53201
+ }
53055
53202
  return;
53056
53203
  }
53204
+ const instructions = getUpdateInstructions(process.platform);
53205
+ if (getGlobalOutputConfig().jsonMode) return void logJsonResult(true, {
53206
+ data: {
53207
+ currentVersion: VERSION,
53208
+ latestVersion,
53209
+ status: "update-available",
53210
+ instructions
53211
+ }
53212
+ });
53057
53213
  logger_logger.warn(`↑ Update available: ${VERSION} → ${latestVersion}`);
53058
53214
  console.log();
53059
53215
  console.log(chalk_source.cyan("To update, run one of the following commands:"));
53060
53216
  console.log();
53061
53217
  if ("win32" === process.platform) {
53062
- console.log(chalk_source.gray(" PowerShell: ") + chalk_source.white("try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }"));
53063
- console.log(chalk_source.gray(" CMD: ") + chalk_source.white("curl -fsSL https://anvil.works/install-cli.cmd -o install.cmd && install.cmd && del install.cmd || npm install -g @anvil-works/anvil-cli@latest"));
53064
- } else console.log(chalk_source.gray(" macOS/Linux: ") + chalk_source.white("curl -fsSL https://anvil.works/install-cli.sh | sh || npm install -g @anvil-works/anvil-cli@latest"));
53065
- console.log(chalk_source.gray(" npm: ") + chalk_source.white("npm install -g @anvil-works/anvil-cli@latest"));
53066
- console.log(chalk_source.gray(" pnpm: ") + chalk_source.white("pnpm add -g @anvil-works/anvil-cli@latest"));
53218
+ console.log(chalk_source.gray(" PowerShell: ") + chalk_source.white(instructions[0]));
53219
+ console.log(chalk_source.gray(" CMD: ") + chalk_source.white(instructions[1]));
53220
+ } else console.log(chalk_source.gray(" macOS/Linux: ") + chalk_source.white(instructions[0]));
53221
+ console.log(chalk_source.gray(" npm: ") + chalk_source.white(instructions[instructions.length - 2]));
53222
+ console.log(chalk_source.gray(" pnpm: ") + chalk_source.white(instructions[instructions.length - 1]));
53067
53223
  console.log();
53068
53224
  } catch (e) {
53069
53225
  logger_logger.error("Error: " + e.message);
@@ -53071,7 +53227,7 @@ var __webpack_exports__ = {};
53071
53227
  }
53072
53228
  }
53073
53229
  function addGlobalOptionsHelp(command) {
53074
- if ("help" !== command.name()) command.addHelpText("after", "\n" + chalk_source.bold("Global Options:") + "\n -V, --verbose Show detailed output\n");
53230
+ if ("help" !== command.name()) command.addHelpText("after", "\n" + chalk_source.bold("Global Options:") + "\n --json Output in JSON format (NDJSON) for scripting/LLM consumption\n -V, --verbose Show detailed output\n");
53075
53231
  command.commands.forEach((subcommand)=>addGlobalOptionsHelp(subcommand));
53076
53232
  }
53077
53233
  function buildProgram() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",