@anvil-works/anvil-cli 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -115
- package/dist/cli.js +721 -101
- package/dist/index.js +12 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,81 +17,38 @@ A CLI tool for syncing Anvil apps between your local filesystem and the Anvil ID
|
|
|
17
17
|
npm install -g @anvil-works/anvil-cli
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
## Quick Start
|
|
21
|
-
|
|
22
|
-
###
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Your access and refresh tokens are stored in:
|
|
53
|
-
- **macOS**: `~/Library/Preferences/anvil-cli/config.json`
|
|
54
|
-
- **Linux**: `~/.config/anvil-cli/config.json`
|
|
55
|
-
- **Windows**: `%APPDATA%\anvil-cli\Config\config.json`
|
|
56
|
-
|
|
57
|
-
**Smart URL Handling:** You can specify the Anvil server URL directly:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
# These all work - anvil automatically adds https://
|
|
61
|
-
anvil login anvil.works
|
|
62
|
-
anvil login anvil.mycompany.com
|
|
63
|
-
|
|
64
|
-
# localhost automatically uses http://
|
|
65
|
-
anvil login localhost:3000
|
|
66
|
-
|
|
67
|
-
# Full URLs also work
|
|
68
|
-
anvil login https://anvil.works
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**Multiple Accounts:** anvil-cli supports multiple Anvil installations simultaneously. Each URL has its own authentication tokens, so you can work with different enterprise installations or switch between them easily.
|
|
72
|
-
|
|
73
|
-
### 2. Watch Your Changes
|
|
74
|
-
|
|
75
|
-
Navigate to your local Anvil app directory and start watching:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
cd /path/to/your/anvil/app
|
|
79
|
-
anvil watch
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Or use the short form:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
anvil w
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
You can specify the app ID explicitly:
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
anvil watch -A YOUR_APP_ID
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Watch Options:**
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Run Onboarding (Recommended First Step)
|
|
23
|
+
|
|
24
|
+
Start with guided setup for your default server URL, login, and optional checkout:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
anvil onboard
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When prompted for **Default Anvil server URL**, press Enter unless you're
|
|
31
|
+
running a dedicated enterprise/on-prem Anvil installation.
|
|
32
|
+
|
|
33
|
+
### 2. Check Out Your App from the IDE URL
|
|
34
|
+
|
|
35
|
+
Copy your app URL from the Anvil IDE (for example
|
|
36
|
+
`https://anvil.works/build/apps/W36XUTXGNPDK6VEA`) and run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
anvil checkout https://anvil.works/build/apps/W36XUTXGNPDK6VEA my-app
|
|
40
|
+
cd my-app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Watch Your Changes
|
|
44
|
+
|
|
45
|
+
When you're ready to sync local edits in real-time:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
anvil watch
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Watch Options:**
|
|
95
52
|
|
|
96
53
|
- `-A, --appid <APP_ID>` - Specify app ID directly
|
|
97
54
|
- `-f, --first` - Auto-select first detected app ID without confirmation
|
|
@@ -122,11 +79,13 @@ The diagram shows bidirectional sync:
|
|
|
122
79
|
|
|
123
80
|
| Command | Description |
|
|
124
81
|
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
125
|
-
| `anvil watch [path]` | Watch directory for changes and sync to Anvil |
|
|
126
|
-
| `anvil watch -V` or `--verbose` | Watch with verbose logging (detailed output) |
|
|
127
|
-
| `anvil w [path]` | Short form for watch |
|
|
128
|
-
| `anvil
|
|
129
|
-
| `anvil
|
|
82
|
+
| `anvil watch [path]` | Watch directory for changes and sync to Anvil |
|
|
83
|
+
| `anvil watch -V` or `--verbose` | Watch with verbose logging (detailed output) |
|
|
84
|
+
| `anvil w [path]` | Short form for watch |
|
|
85
|
+
| `anvil onboard` | Guided setup for default URL, login, and optional checkout |
|
|
86
|
+
| `anvil checkout <input> [directory]` | Check out an Anvil app locally from editor URL, git URL, or app ID |
|
|
87
|
+
| `anvil login [anvil-server-url]` | Authenticate with Anvil using OAuth (supports smart URL handling) |
|
|
88
|
+
| `anvil l [anvil-server-url]` | Short form for login |
|
|
130
89
|
| `anvil logout [anvil-server-url]` | Logout from Anvil (optionally specify URL for specific installation) |
|
|
131
90
|
| `anvil config <action> [key] [value]` | Manage configuration (set, get, list) |
|
|
132
91
|
| `anvil c <action> [key] [value]` | Short form for config |
|
|
@@ -177,40 +136,127 @@ anvil watch
|
|
|
177
136
|
# ❯ https://anvil.works
|
|
178
137
|
# https://anvil.company.com
|
|
179
138
|
# Cancel
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Checkout Apps
|
|
142
|
+
|
|
143
|
+
`anvil checkout` accepts an editor URL, git URL, or bare app ID:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Checkout from editor URL
|
|
147
|
+
anvil checkout http://localhost:3000/build/apps/MHVELZG5SZXK2POE/code/assets/theme.css
|
|
148
|
+
|
|
149
|
+
# Checkout from git URL
|
|
150
|
+
anvil checkout https://anvil.works/git/MHVELZG5SZXK2POE.git
|
|
151
|
+
|
|
152
|
+
# Checkout from app ID (specify URL when needed)
|
|
153
|
+
anvil checkout MHVELZG5SZXK2POE --url anvil.works
|
|
154
|
+
|
|
155
|
+
# Explicit destination directory
|
|
156
|
+
anvil checkout MHVELZG5SZXK2POE my-local-app --url anvil.works
|
|
157
|
+
|
|
158
|
+
# Clone-style options
|
|
159
|
+
anvil checkout MHVELZG5SZXK2POE --branch master --depth 1 --single-branch
|
|
160
|
+
anvil checkout MHVELZG5SZXK2POE --origin upstream
|
|
161
|
+
anvil checkout MHVELZG5SZXK2POE --quiet
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Safety behavior:
|
|
165
|
+
|
|
166
|
+
- Blocks checkout into non-empty destination directories (unless `--force`).
|
|
167
|
+
- Blocks checkout into paths inside an existing git repository (unless `--force`).
|
|
168
|
+
|
|
169
|
+
Git HTTPS credentials:
|
|
170
|
+
|
|
171
|
+
- Checkout configures a clean HTTPS remote URL plus a repo-local Git credential helper (`anvil git-credential`).
|
|
172
|
+
- The helper retrieves short-lived access tokens from your logged-in account and refreshes them as needed.
|
|
173
|
+
- Per-app bindings are stored in local git config (`anvil.auth.<APPID>.url` and `anvil.auth.<APPID>.username`) so multi-account repos stay deterministic.
|
|
174
|
+
|
|
175
|
+
Checkout options:
|
|
176
|
+
|
|
177
|
+
- `-O, --open` - Open destination after checkout
|
|
178
|
+
- `-b, --branch <BRANCH>` - Checkout a specific branch
|
|
179
|
+
- `--depth <N>` - Shallow clone with `N` commits of history
|
|
180
|
+
- `--single-branch` - Clone only one branch
|
|
181
|
+
- `--origin <NAME>` - Use custom remote name instead of `origin`
|
|
182
|
+
- `-q, --quiet` - Suppress git clone progress output
|
|
183
|
+
- `--verbose` - Verbose git clone output
|
|
184
|
+
- `-f, --force` - Override destination safety checks
|
|
185
|
+
- `-u, --url <ANVIL_URL>` - Override detected Anvil URL
|
|
186
|
+
- `-U, --user <USERNAME>` - Use a specific logged-in account
|
|
187
|
+
|
|
188
|
+
If multiple accounts are logged in for the same URL, `anvil checkout` will
|
|
189
|
+
prompt you to select one (interactive mode) or require `--user` in
|
|
190
|
+
non-interactive mode.
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
### Authentication
|
|
195
|
+
|
|
196
|
+
Use `anvil login` to authenticate with Anvil:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
anvil login
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The login flow will:
|
|
203
|
+
|
|
204
|
+
1. Open your browser to the Anvil OAuth authorization page
|
|
205
|
+
2. Prompt you to authorize anvil-cli
|
|
206
|
+
3. Complete authentication automatically in the CLI
|
|
207
|
+
|
|
208
|
+
Your access and refresh tokens are stored in:
|
|
209
|
+
- **macOS**: `~/Library/Preferences/anvil-cli/config.json`
|
|
210
|
+
- **Linux**: `~/.config/anvil-cli/config.json`
|
|
211
|
+
- **Windows**: `%APPDATA%\anvil-cli\Config\config.json`
|
|
212
|
+
|
|
213
|
+
Smart URL handling:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# anvil automatically adds https://
|
|
217
|
+
anvil login anvil.works
|
|
218
|
+
anvil login anvil.mycompany.com
|
|
219
|
+
|
|
220
|
+
# localhost uses http://localhost:3000
|
|
221
|
+
anvil login localhost:3000
|
|
222
|
+
|
|
223
|
+
# full URLs also work
|
|
224
|
+
anvil login https://anvil.works
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
anvil-cli supports multiple Anvil installations simultaneously. Each URL has
|
|
228
|
+
its own authentication tokens.
|
|
229
|
+
|
|
230
|
+
### Config File Location
|
|
185
231
|
|
|
186
232
|
- **macOS**: `~/Library/Preferences/anvil-cli/config.json`
|
|
187
233
|
- **Linux**: `~/.config/anvil-cli/config.json`
|
|
188
234
|
- **Windows**: `%APPDATA%\anvil-cli\Config\config.json`
|
|
189
235
|
|
|
190
|
-
### Stored Configuration
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"https://anvil.
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
### Stored Configuration
|
|
237
|
+
|
|
238
|
+
`config.json` stores settings and auth metadata. OAuth secrets are stored in the
|
|
239
|
+
OS keychain when available.
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"anvilUrl": "https://anvil.works",
|
|
244
|
+
"verbose": false,
|
|
245
|
+
"preferredEditor": "cursor",
|
|
246
|
+
"authTokens": {
|
|
247
|
+
"https://anvil.works": {
|
|
248
|
+
"user@example.com": {
|
|
249
|
+
"authToken": null,
|
|
250
|
+
"refreshToken": null,
|
|
251
|
+
"authTokenExpiresAt": 1735689600
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
If keychain is unavailable (for example some CI/headless environments), tokens
|
|
259
|
+
fall back to `config.json`.
|
|
214
260
|
|
|
215
261
|
### Verbose Logging
|
|
216
262
|
|
|
@@ -222,13 +268,25 @@ By default, anvil-cli shows minimal output. For more detailed logging:
|
|
|
222
268
|
anvil watch --verbose
|
|
223
269
|
```
|
|
224
270
|
|
|
225
|
-
**Persistent:** Set verbose mode in config:
|
|
271
|
+
**Persistent:** Set verbose mode in config:
|
|
226
272
|
|
|
227
273
|
```bash
|
|
228
|
-
anvil config set verbose true
|
|
229
|
-
```
|
|
274
|
+
anvil config set verbose true
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Preferred Editor
|
|
278
|
+
|
|
279
|
+
Set this during `anvil onboard`.
|
|
280
|
+
|
|
281
|
+
If needed later, you can still set it manually with:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
anvil config set preferredEditor <editor>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`preferredEditor` is used by checkout for post-checkout guidance and `-O/--open`.
|
|
230
288
|
|
|
231
|
-
Verbose output
|
|
289
|
+
Verbose output includes timestamped diagnostic lines with file routing details, sync status checks, and other internal processing information.
|
|
232
290
|
|
|
233
291
|
### Enterprise Configuration
|
|
234
292
|
|
|
@@ -348,7 +406,7 @@ anvil login https://anvil.mycompany.com
|
|
|
348
406
|
anvil config set anvilUrl https://anvil.works
|
|
349
407
|
```
|
|
350
408
|
|
|
351
|
-
- Or reset all config values (also clears custom URLs):
|
|
409
|
+
- Or reset all config values (also clears custom URLs and logs out all accounts):
|
|
352
410
|
|
|
353
411
|
```bash
|
|
354
412
|
anvil config reset
|
|
@@ -356,4 +414,4 @@ anvil login https://anvil.mycompany.com
|
|
|
356
414
|
|
|
357
415
|
## License
|
|
358
416
|
|
|
359
|
-
|
|
417
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -42459,8 +42459,31 @@ var __webpack_exports__ = {};
|
|
|
42459
42459
|
}
|
|
42460
42460
|
const configDefaults = {
|
|
42461
42461
|
devMode: false,
|
|
42462
|
-
verbose: false
|
|
42462
|
+
verbose: false,
|
|
42463
|
+
preferredEditor: ""
|
|
42464
|
+
};
|
|
42465
|
+
const preferredEditors = [
|
|
42466
|
+
"vscode",
|
|
42467
|
+
"cursor",
|
|
42468
|
+
"nvim",
|
|
42469
|
+
"zed",
|
|
42470
|
+
"windsurf",
|
|
42471
|
+
"pycharm"
|
|
42472
|
+
];
|
|
42473
|
+
const preferredEditorCommandByValue = {
|
|
42474
|
+
vscode: "code",
|
|
42475
|
+
cursor: "cursor",
|
|
42476
|
+
nvim: "nvim",
|
|
42477
|
+
zed: "zed",
|
|
42478
|
+
windsurf: "windsurf",
|
|
42479
|
+
pycharm: "charm"
|
|
42463
42480
|
};
|
|
42481
|
+
const settableConfigKeys = [
|
|
42482
|
+
"anvilUrl",
|
|
42483
|
+
"devMode",
|
|
42484
|
+
"verbose",
|
|
42485
|
+
"preferredEditor"
|
|
42486
|
+
];
|
|
42464
42487
|
function isTestMode() {
|
|
42465
42488
|
return "test" === process.env.NODE_ENV || !!process.env.RSTEST;
|
|
42466
42489
|
}
|
|
@@ -42532,6 +42555,23 @@ var __webpack_exports__ = {};
|
|
|
42532
42555
|
}
|
|
42533
42556
|
return value;
|
|
42534
42557
|
}
|
|
42558
|
+
function parseConfigSetValue(key, value) {
|
|
42559
|
+
if (!settableConfigKeys.includes(key)) throw new Error(`✖ Error: unknown config key '${key}'. Allowed keys: ${settableConfigKeys.join(", ")}`);
|
|
42560
|
+
if ("preferredEditor" === key) {
|
|
42561
|
+
let normalized = value.trim().toLowerCase();
|
|
42562
|
+
if ("code" === normalized) normalized = "vscode";
|
|
42563
|
+
if (!preferredEditors.includes(normalized)) throw new Error(`✖ Error: preferredEditor must be one of: ${preferredEditors.join(", ")} (alias: code -> vscode)`);
|
|
42564
|
+
return normalized;
|
|
42565
|
+
}
|
|
42566
|
+
if ("anvilUrl" === key) return value.trim().replace(/\/+$/, "");
|
|
42567
|
+
return coerceConfigValue(key, value);
|
|
42568
|
+
}
|
|
42569
|
+
function getPreferredEditorCommand(preferredEditorValue) {
|
|
42570
|
+
const normalized = preferredEditorValue.trim().toLowerCase();
|
|
42571
|
+
if ("code" === normalized) return "code";
|
|
42572
|
+
if (preferredEditors.includes(normalized)) return preferredEditorCommandByValue[normalized];
|
|
42573
|
+
return preferredEditorValue;
|
|
42574
|
+
}
|
|
42535
42575
|
function getAllConfig() {
|
|
42536
42576
|
return config_config.store;
|
|
42537
42577
|
}
|
|
@@ -47584,6 +47624,7 @@ var __webpack_exports__ = {};
|
|
|
47584
47624
|
});
|
|
47585
47625
|
this.ws.on("error", (error)=>{
|
|
47586
47626
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
|
|
47627
|
+
if (this.isClosing) return;
|
|
47587
47628
|
if (error.message.includes("502")) logger_logger.error(chalk_source.red("WebSocket connection failed (502 Bad Gateway) - likely a temporary server issue"));
|
|
47588
47629
|
else logger_logger.error(chalk_source.red(`WebSocket error: ${error.message}`));
|
|
47589
47630
|
this.emit("error", {
|
|
@@ -47684,15 +47725,20 @@ var __webpack_exports__ = {};
|
|
|
47684
47725
|
}
|
|
47685
47726
|
teardownSocket() {
|
|
47686
47727
|
this.stopHeartbeat();
|
|
47687
|
-
|
|
47728
|
+
const socket = this.ws;
|
|
47729
|
+
this.ws = null;
|
|
47730
|
+
if (!socket) return;
|
|
47688
47731
|
try {
|
|
47689
47732
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
|
|
47690
|
-
|
|
47691
|
-
|
|
47733
|
+
if (socket.readyState === ws_wrapper.CONNECTING) {
|
|
47734
|
+
socket.once("error", ()=>{});
|
|
47735
|
+
socket.terminate();
|
|
47736
|
+
return;
|
|
47737
|
+
}
|
|
47738
|
+
if (socket.readyState === ws_wrapper.OPEN) return void socket.close();
|
|
47692
47739
|
} catch (e) {
|
|
47693
47740
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
|
|
47694
47741
|
}
|
|
47695
|
-
this.ws = null;
|
|
47696
47742
|
}
|
|
47697
47743
|
}
|
|
47698
47744
|
async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
|
|
@@ -49956,6 +50002,121 @@ var __webpack_exports__ = {};
|
|
|
49956
50002
|
session.hasUncommittedChanges = hasUncommittedChanges;
|
|
49957
50003
|
return session;
|
|
49958
50004
|
}
|
|
50005
|
+
function getUrlConfigKey(appId) {
|
|
50006
|
+
return `anvil.auth.${appId}.url`;
|
|
50007
|
+
}
|
|
50008
|
+
function getUsernameConfigKey(appId) {
|
|
50009
|
+
return `anvil.auth.${appId}.username`;
|
|
50010
|
+
}
|
|
50011
|
+
async function getLocalConfigValue(repoPath, key) {
|
|
50012
|
+
try {
|
|
50013
|
+
const value = (await esm_default(repoPath).raw([
|
|
50014
|
+
"config",
|
|
50015
|
+
"--local",
|
|
50016
|
+
"--get",
|
|
50017
|
+
key
|
|
50018
|
+
])).trim();
|
|
50019
|
+
return value || void 0;
|
|
50020
|
+
} catch {
|
|
50021
|
+
return;
|
|
50022
|
+
}
|
|
50023
|
+
}
|
|
50024
|
+
async function getAppAuthBinding(repoPath, appId) {
|
|
50025
|
+
const [url, username] = await Promise.all([
|
|
50026
|
+
getLocalConfigValue(repoPath, getUrlConfigKey(appId)),
|
|
50027
|
+
getLocalConfigValue(repoPath, getUsernameConfigKey(appId))
|
|
50028
|
+
]);
|
|
50029
|
+
return {
|
|
50030
|
+
url: url ? normalizeAnvilUrl(url) : void 0,
|
|
50031
|
+
username: username || void 0
|
|
50032
|
+
};
|
|
50033
|
+
}
|
|
50034
|
+
async function setAppAuthBinding(repoPath, appId, binding) {
|
|
50035
|
+
const git = esm_default(repoPath);
|
|
50036
|
+
if (binding.url) await git.raw([
|
|
50037
|
+
"config",
|
|
50038
|
+
"--local",
|
|
50039
|
+
getUrlConfigKey(appId),
|
|
50040
|
+
normalizeAnvilUrl(binding.url)
|
|
50041
|
+
]);
|
|
50042
|
+
if (binding.username) await git.raw([
|
|
50043
|
+
"config",
|
|
50044
|
+
"--local",
|
|
50045
|
+
getUsernameConfigKey(appId),
|
|
50046
|
+
binding.username
|
|
50047
|
+
]);
|
|
50048
|
+
}
|
|
50049
|
+
function getCleanGitRemoteUrl(appId, anvilUrl) {
|
|
50050
|
+
const normalized = normalizeAnvilUrl(anvilUrl);
|
|
50051
|
+
const url = new URL(normalized);
|
|
50052
|
+
return `${url.protocol}//${url.host}/git/${appId}.git`;
|
|
50053
|
+
}
|
|
50054
|
+
async function configureCredentialHelperForUrl(repoPath, anvilUrl) {
|
|
50055
|
+
const normalized = normalizeAnvilUrl(anvilUrl);
|
|
50056
|
+
const url = new URL(normalized);
|
|
50057
|
+
const scope = `${url.protocol}//${url.host}`;
|
|
50058
|
+
const git = esm_default(repoPath);
|
|
50059
|
+
try {
|
|
50060
|
+
await git.raw([
|
|
50061
|
+
"config",
|
|
50062
|
+
"--local",
|
|
50063
|
+
"--unset-all",
|
|
50064
|
+
`credential.${scope}.helper`
|
|
50065
|
+
]);
|
|
50066
|
+
} catch {}
|
|
50067
|
+
await git.raw([
|
|
50068
|
+
"config",
|
|
50069
|
+
"--local",
|
|
50070
|
+
"--add",
|
|
50071
|
+
`credential.${scope}.helper`,
|
|
50072
|
+
""
|
|
50073
|
+
]);
|
|
50074
|
+
await git.raw([
|
|
50075
|
+
"config",
|
|
50076
|
+
"--local",
|
|
50077
|
+
"--add",
|
|
50078
|
+
`credential.${scope}.helper`,
|
|
50079
|
+
"!anvil git-credential"
|
|
50080
|
+
]);
|
|
50081
|
+
await git.raw([
|
|
50082
|
+
"config",
|
|
50083
|
+
"--local",
|
|
50084
|
+
`credential.${scope}.useHttpPath`,
|
|
50085
|
+
"true"
|
|
50086
|
+
]);
|
|
50087
|
+
await git.raw([
|
|
50088
|
+
"config",
|
|
50089
|
+
"--local",
|
|
50090
|
+
`credential.${scope}.username`,
|
|
50091
|
+
"git"
|
|
50092
|
+
]);
|
|
50093
|
+
}
|
|
50094
|
+
async function hardenCheckoutGitAuth(options) {
|
|
50095
|
+
const { repoPath, appId, anvilUrl, username } = options;
|
|
50096
|
+
const remoteName = options.remoteName || "origin";
|
|
50097
|
+
const cleanRemoteUrl = getCleanGitRemoteUrl(appId, anvilUrl);
|
|
50098
|
+
const git = esm_default(repoPath);
|
|
50099
|
+
await git.raw([
|
|
50100
|
+
"remote",
|
|
50101
|
+
"set-url",
|
|
50102
|
+
remoteName,
|
|
50103
|
+
cleanRemoteUrl
|
|
50104
|
+
]);
|
|
50105
|
+
await configureCredentialHelperForUrl(repoPath, anvilUrl);
|
|
50106
|
+
await setAppAuthBinding(repoPath, appId, {
|
|
50107
|
+
url: anvilUrl,
|
|
50108
|
+
username
|
|
50109
|
+
});
|
|
50110
|
+
return {
|
|
50111
|
+
cleanRemoteUrl
|
|
50112
|
+
};
|
|
50113
|
+
}
|
|
50114
|
+
function parseAppIdFromGitPath(pathValue) {
|
|
50115
|
+
if (!pathValue) return;
|
|
50116
|
+
const normalizedPath = pathValue.startsWith("/") ? pathValue : `/${pathValue}`;
|
|
50117
|
+
const match = normalizedPath.match(/\/git\/([A-Z0-9]+)\.git(?:$|\/)/);
|
|
50118
|
+
return match ? match[1] : void 0;
|
|
50119
|
+
}
|
|
49959
50120
|
function decideFallbackUrl(explicitUrl, availableUrls = getAvailableAnvilUrls()) {
|
|
49960
50121
|
if (explicitUrl) return {
|
|
49961
50122
|
source: "explicit",
|
|
@@ -50546,14 +50707,24 @@ var __webpack_exports__ = {};
|
|
|
50546
50707
|
let anvilUrl;
|
|
50547
50708
|
let username = explicitUsername;
|
|
50548
50709
|
let fallbackUrl;
|
|
50710
|
+
let shouldPersistUsernameBinding = false;
|
|
50549
50711
|
if (explicitAppId) {
|
|
50550
50712
|
finalAppId = explicitAppId;
|
|
50713
|
+
const binding = await getAppAuthBinding(repoPath, explicitAppId);
|
|
50714
|
+
if (binding.url && !explicitUrl) {
|
|
50715
|
+
anvilUrl = normalizeAnvilUrl(binding.url);
|
|
50716
|
+
logger_logger.verbose(chalk_source.cyan("Resolved URL from binding for app ID: ") + chalk_source.bold(anvilUrl));
|
|
50717
|
+
}
|
|
50718
|
+
if (binding.username && !explicitUsername) {
|
|
50719
|
+
username = binding.username;
|
|
50720
|
+
logger_logger.verbose(chalk_source.cyan("Resolved username from binding for app ID: ") + chalk_source.bold(username));
|
|
50721
|
+
}
|
|
50551
50722
|
const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
|
|
50552
|
-
if (remoteInfo.detectedUrl && !explicitUrl) {
|
|
50723
|
+
if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
|
|
50553
50724
|
anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
|
|
50554
50725
|
logger_logger.verbose(chalk_source.cyan("Resolved URL from remote for app ID: ") + chalk_source.bold(anvilUrl));
|
|
50555
50726
|
}
|
|
50556
|
-
if (remoteInfo.detectedUsername && !explicitUsername) {
|
|
50727
|
+
if (remoteInfo.detectedUsername && !explicitUsername && !username) {
|
|
50557
50728
|
username = remoteInfo.detectedUsername;
|
|
50558
50729
|
logger_logger.verbose(chalk_source.cyan("Resolved username from remote for app ID: ") + chalk_source.bold(username));
|
|
50559
50730
|
}
|
|
@@ -50608,6 +50779,16 @@ var __webpack_exports__ = {};
|
|
|
50608
50779
|
if (selected.detectedUsername && !username) username = selected.detectedUsername;
|
|
50609
50780
|
}
|
|
50610
50781
|
}
|
|
50782
|
+
const binding = await getAppAuthBinding(repoPath, finalAppId);
|
|
50783
|
+
if (binding.url && !explicitUrl) {
|
|
50784
|
+
anvilUrl = normalizeAnvilUrl(binding.url);
|
|
50785
|
+
logger_logger.verbose(chalk_source.cyan("Using app binding URL: ") + chalk_source.bold(anvilUrl));
|
|
50786
|
+
}
|
|
50787
|
+
if (binding.username && !explicitUsername) {
|
|
50788
|
+
username = binding.username;
|
|
50789
|
+
logger_logger.verbose(chalk_source.cyan("Using app binding username: ") + chalk_source.bold(username));
|
|
50790
|
+
}
|
|
50791
|
+
shouldPersistUsernameBinding = !binding.username && !explicitUsername;
|
|
50611
50792
|
if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
|
|
50612
50793
|
else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
|
|
50613
50794
|
else {
|
|
@@ -50627,6 +50808,13 @@ var __webpack_exports__ = {};
|
|
|
50627
50808
|
process.exit(0);
|
|
50628
50809
|
}
|
|
50629
50810
|
username = resolvedUsername;
|
|
50811
|
+
if (shouldPersistUsernameBinding && username && auth_getAccountsForUrl(anvilUrl).length > 1) {
|
|
50812
|
+
await setAppAuthBinding(repoPath, finalAppId, {
|
|
50813
|
+
url: anvilUrl,
|
|
50814
|
+
username
|
|
50815
|
+
});
|
|
50816
|
+
logger_logger.verbose(chalk_source.cyan("Saved app account binding for future non-interactive use."));
|
|
50817
|
+
}
|
|
50630
50818
|
if (username) logger_logger.verbose(chalk_source.cyan("Using account: ") + chalk_source.bold(username));
|
|
50631
50819
|
if (!hasTokensForUrl(anvilUrl, username)) {
|
|
50632
50820
|
if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
|
|
@@ -50685,7 +50873,6 @@ var __webpack_exports__ = {};
|
|
|
50685
50873
|
});
|
|
50686
50874
|
return watchCommand;
|
|
50687
50875
|
}
|
|
50688
|
-
var external_http_ = __webpack_require__("http");
|
|
50689
50876
|
const external_node_url_namespaceObject = require("node:url");
|
|
50690
50877
|
var external_node_child_process_ = __webpack_require__("node:child_process");
|
|
50691
50878
|
let isDockerCached;
|
|
@@ -51148,6 +51335,7 @@ var __webpack_exports__ = {};
|
|
|
51148
51335
|
defineLazyProperty(open_apps, 'browser', ()=>'browser');
|
|
51149
51336
|
defineLazyProperty(open_apps, 'browserPrivate', ()=>'browserPrivate');
|
|
51150
51337
|
const node_modules_open = open_open;
|
|
51338
|
+
var external_http_ = __webpack_require__("http");
|
|
51151
51339
|
const successPage = `<!DOCTYPE html>
|
|
51152
51340
|
<html lang="en">
|
|
51153
51341
|
<head>
|
|
@@ -51305,6 +51493,9 @@ var __webpack_exports__ = {};
|
|
|
51305
51493
|
</body>
|
|
51306
51494
|
</html>`;
|
|
51307
51495
|
}
|
|
51496
|
+
function oauth_login_base64url(buf) {
|
|
51497
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
51498
|
+
}
|
|
51308
51499
|
async function waitForOAuthWithPrompt(authUrl, oauthPromise) {
|
|
51309
51500
|
const rl = external_readline_namespaceObject.createInterface({
|
|
51310
51501
|
input: process.stdin,
|
|
@@ -51329,8 +51520,382 @@ var __webpack_exports__ = {};
|
|
|
51329
51520
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
51330
51521
|
return result.oauth;
|
|
51331
51522
|
}
|
|
51332
|
-
function
|
|
51333
|
-
|
|
51523
|
+
async function runInteractiveLoginFlow(anvilUrl) {
|
|
51524
|
+
const codeVerifier = external_crypto_.randomBytes(48).toString("hex");
|
|
51525
|
+
const codeChallenge = oauth_login_base64url(external_crypto_.createHash("sha256").update(codeVerifier, "ascii").digest());
|
|
51526
|
+
const state = external_crypto_.randomBytes(16).toString("hex");
|
|
51527
|
+
const server = external_http_.createServer();
|
|
51528
|
+
await new Promise((resolve)=>server.listen(0, "127.0.0.1", resolve));
|
|
51529
|
+
const address = server.address();
|
|
51530
|
+
if (!address || "string" == typeof address) throw new Error("No address");
|
|
51531
|
+
const port = address.port;
|
|
51532
|
+
const redirectUri = `http://127.0.0.1:${port}/oauth-callback`;
|
|
51533
|
+
const SCOPES = "apps:read apps:write user:read";
|
|
51534
|
+
const authUrl = new URL(`${anvilUrl}/oauth/authorize`);
|
|
51535
|
+
authUrl.searchParams.set("response_type", "code");
|
|
51536
|
+
authUrl.searchParams.set("client_id", "anvil-sync");
|
|
51537
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
51538
|
+
authUrl.searchParams.set("scope", SCOPES);
|
|
51539
|
+
authUrl.searchParams.set("state", state);
|
|
51540
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
51541
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
51542
|
+
const codePromise = new Promise((resolve, reject)=>{
|
|
51543
|
+
server.on("request", (req, res)=>{
|
|
51544
|
+
if (!req.url) return;
|
|
51545
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
51546
|
+
if ("/oauth-callback" !== url.pathname) return;
|
|
51547
|
+
const code = url.searchParams.get("code") || void 0;
|
|
51548
|
+
const error = url.searchParams.get("error") || void 0;
|
|
51549
|
+
const recvState = url.searchParams.get("state");
|
|
51550
|
+
if (!recvState || !code && !error) {
|
|
51551
|
+
res.statusCode = 400;
|
|
51552
|
+
res.end("Missing code, error or state");
|
|
51553
|
+
reject(new Error("Missing code/state/error"));
|
|
51554
|
+
server.close();
|
|
51555
|
+
return;
|
|
51556
|
+
}
|
|
51557
|
+
res.statusCode = 200;
|
|
51558
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
51559
|
+
if (code) res.end(successPage);
|
|
51560
|
+
else res.end(errorPage(error || "unknown"));
|
|
51561
|
+
resolve({
|
|
51562
|
+
code,
|
|
51563
|
+
error,
|
|
51564
|
+
recvState
|
|
51565
|
+
});
|
|
51566
|
+
server.closeAllConnections();
|
|
51567
|
+
server.close();
|
|
51568
|
+
});
|
|
51569
|
+
});
|
|
51570
|
+
logger_logger.info(chalk_source.blue("Link to your Anvil account to continue."));
|
|
51571
|
+
console.log();
|
|
51572
|
+
logger_logger.info(chalk_source.gray(` ${authUrl}`));
|
|
51573
|
+
console.log();
|
|
51574
|
+
const { code, error, recvState } = await waitForOAuthWithPrompt(authUrl, codePromise);
|
|
51575
|
+
if (recvState !== state) throw new Error("Invalid state received from OAuth callback");
|
|
51576
|
+
if (error) throw new Error(`Error received from OAuth callback: ${error}`);
|
|
51577
|
+
const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
|
|
51578
|
+
method: "POST",
|
|
51579
|
+
headers: {
|
|
51580
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
51581
|
+
},
|
|
51582
|
+
body: new URLSearchParams({
|
|
51583
|
+
grant_type: "authorization_code",
|
|
51584
|
+
code: code,
|
|
51585
|
+
redirect_uri: redirectUri,
|
|
51586
|
+
client_id: "anvil-sync",
|
|
51587
|
+
code_verifier: codeVerifier
|
|
51588
|
+
})
|
|
51589
|
+
});
|
|
51590
|
+
if (!tokenResponse.ok) {
|
|
51591
|
+
const errorText = await tokenResponse.text();
|
|
51592
|
+
throw new Error(`Failed to exchange authorization code for token. ${errorText}`);
|
|
51593
|
+
}
|
|
51594
|
+
const tokenData = await tokenResponse.json();
|
|
51595
|
+
return login(anvilUrl, {
|
|
51596
|
+
access_token: tokenData.access_token,
|
|
51597
|
+
refresh_token: tokenData.refresh_token,
|
|
51598
|
+
expires_in: tokenData.expires_in,
|
|
51599
|
+
scope: tokenData.scope
|
|
51600
|
+
});
|
|
51601
|
+
}
|
|
51602
|
+
const defaultCheckoutDeps = {
|
|
51603
|
+
getValidAuthToken: auth_getValidAuthToken,
|
|
51604
|
+
validateAppId: validateAppId,
|
|
51605
|
+
runInteractiveLoginFlow: runInteractiveLoginFlow,
|
|
51606
|
+
clone: async (repoUrl, destinationPath, options)=>{
|
|
51607
|
+
const cloneArgs = [];
|
|
51608
|
+
if (options?.branch) cloneArgs.push("--branch", options.branch);
|
|
51609
|
+
if ("number" == typeof options?.depth) cloneArgs.push("--depth", String(options.depth));
|
|
51610
|
+
if (options?.singleBranch) cloneArgs.push("--single-branch");
|
|
51611
|
+
if (options?.origin) cloneArgs.push("--origin", options.origin);
|
|
51612
|
+
if (options?.quiet) cloneArgs.push("--quiet");
|
|
51613
|
+
if (options?.verbose) cloneArgs.push("--verbose");
|
|
51614
|
+
await esm_default().clone(repoUrl, destinationPath, cloneArgs);
|
|
51615
|
+
},
|
|
51616
|
+
hardenCheckoutGitAuth: hardenCheckoutGitAuth
|
|
51617
|
+
};
|
|
51618
|
+
function parseCheckoutInput(input) {
|
|
51619
|
+
const trimmed = input.trim();
|
|
51620
|
+
if (!trimmed) throw new Error("Input is required.");
|
|
51621
|
+
if (/^[A-Z0-9]+$/.test(trimmed)) return {
|
|
51622
|
+
appId: trimmed
|
|
51623
|
+
};
|
|
51624
|
+
const asUrl = /^https?:\/\//.test(trimmed) ? trimmed : normalizeAnvilUrl(trimmed);
|
|
51625
|
+
let parsed;
|
|
51626
|
+
try {
|
|
51627
|
+
parsed = new URL(asUrl);
|
|
51628
|
+
} catch {
|
|
51629
|
+
throw new Error("Input must be an editor URL, /git URL, or bare app ID.");
|
|
51630
|
+
}
|
|
51631
|
+
const gitMatch = parsed.pathname.match(/\/git\/([A-Z0-9]+)\.git(?:$|\/)/);
|
|
51632
|
+
if (gitMatch) return {
|
|
51633
|
+
appId: gitMatch[1],
|
|
51634
|
+
detectedUrl: `${parsed.protocol}//${parsed.host}`
|
|
51635
|
+
};
|
|
51636
|
+
const appsMatch = parsed.pathname.match(/\/apps\/([A-Z0-9]+)(?:\/|$)/);
|
|
51637
|
+
if (appsMatch) return {
|
|
51638
|
+
appId: appsMatch[1],
|
|
51639
|
+
detectedUrl: `${parsed.protocol}//${parsed.host}`
|
|
51640
|
+
};
|
|
51641
|
+
throw new Error("Could not extract app ID. Expected URL path containing /apps/<APPID> or /git/<APPID>.git.");
|
|
51642
|
+
}
|
|
51643
|
+
function sanitizeDirectoryName(name) {
|
|
51644
|
+
return name.trim().replace(/\s+/g, "_").replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
51645
|
+
}
|
|
51646
|
+
function formatPathForDisplay(destinationPath) {
|
|
51647
|
+
const relative = external_path_default().relative(process.cwd(), destinationPath);
|
|
51648
|
+
if ("" === relative) return ".";
|
|
51649
|
+
if (relative.startsWith("..")) return destinationPath;
|
|
51650
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
51651
|
+
}
|
|
51652
|
+
function getDefaultDestinationDirectory(appId, appName) {
|
|
51653
|
+
if (appName) {
|
|
51654
|
+
const sanitized = sanitizeDirectoryName(appName);
|
|
51655
|
+
if (sanitized) return sanitized;
|
|
51656
|
+
}
|
|
51657
|
+
return appId;
|
|
51658
|
+
}
|
|
51659
|
+
async function resolveCheckoutUrl(explicitUrl, parsedUrl) {
|
|
51660
|
+
if (explicitUrl) return normalizeAnvilUrl(explicitUrl);
|
|
51661
|
+
if (parsedUrl) return normalizeAnvilUrl(parsedUrl);
|
|
51662
|
+
const decision = decideFallbackUrl(void 0);
|
|
51663
|
+
if ("available-multiple" !== decision.source) return decision.url;
|
|
51664
|
+
const choices = decision.urls.map((url)=>({
|
|
51665
|
+
name: url,
|
|
51666
|
+
value: url
|
|
51667
|
+
}));
|
|
51668
|
+
choices.push({
|
|
51669
|
+
name: "Cancel",
|
|
51670
|
+
value: null
|
|
51671
|
+
});
|
|
51672
|
+
return logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
|
|
51673
|
+
}
|
|
51674
|
+
async function isPathInsideGitRepo(targetPath) {
|
|
51675
|
+
const resolved = external_path_default().resolve(targetPath);
|
|
51676
|
+
let current = resolved;
|
|
51677
|
+
while(!external_fs_.existsSync(current)){
|
|
51678
|
+
const parent = external_path_default().dirname(current);
|
|
51679
|
+
if (parent === current) return false;
|
|
51680
|
+
current = parent;
|
|
51681
|
+
}
|
|
51682
|
+
try {
|
|
51683
|
+
const output = (await esm_default(current).raw([
|
|
51684
|
+
"rev-parse",
|
|
51685
|
+
"--is-inside-work-tree"
|
|
51686
|
+
])).trim();
|
|
51687
|
+
return "true" === output;
|
|
51688
|
+
} catch {
|
|
51689
|
+
return false;
|
|
51690
|
+
}
|
|
51691
|
+
}
|
|
51692
|
+
async function isDirectoryNonEmpty(destinationPath) {
|
|
51693
|
+
if (!external_fs_.existsSync(destinationPath)) return false;
|
|
51694
|
+
const stats = await external_fs_.promises.stat(destinationPath);
|
|
51695
|
+
if (!stats.isDirectory()) return true;
|
|
51696
|
+
const entries = await external_fs_.promises.readdir(destinationPath);
|
|
51697
|
+
return entries.length > 0;
|
|
51698
|
+
}
|
|
51699
|
+
async function validateCheckoutDestination(destinationPath, force = false) {
|
|
51700
|
+
const nonEmpty = await isDirectoryNonEmpty(destinationPath);
|
|
51701
|
+
if (nonEmpty && !force) throw new Error(`Destination '${destinationPath}' already exists and is not empty.`);
|
|
51702
|
+
const insideGit = await isPathInsideGitRepo(destinationPath);
|
|
51703
|
+
if (insideGit && !force) throw new Error(`Destination '${destinationPath}' is inside an existing Git repository.`);
|
|
51704
|
+
if (force && nonEmpty) logger_logger.warn(`--force set: destination '${destinationPath}' is non-empty and clone may still fail.`);
|
|
51705
|
+
if (force && insideGit) logger_logger.warn("--force set: cloning inside an existing Git repository.");
|
|
51706
|
+
}
|
|
51707
|
+
async function ensureCheckoutAuthToken(anvilUrl, username, deps = defaultCheckoutDeps) {
|
|
51708
|
+
try {
|
|
51709
|
+
return await deps.getValidAuthToken(anvilUrl, username);
|
|
51710
|
+
} catch (e) {
|
|
51711
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
51712
|
+
if (!interactive) throw errors_createAuthError.required(`Not logged in to ${anvilUrl}. Run 'anvil login ${anvilUrl}' and retry.`);
|
|
51713
|
+
const shouldLogin = await logger_logger.confirm(`Not logged in to ${anvilUrl}. Log in now?`, true);
|
|
51714
|
+
if (!shouldLogin) throw errors_createAuthError.required(`Not logged in to ${anvilUrl}. Run 'anvil login ${anvilUrl}' and retry.`);
|
|
51715
|
+
await deps.runInteractiveLoginFlow(anvilUrl);
|
|
51716
|
+
return deps.getValidAuthToken(anvilUrl, username);
|
|
51717
|
+
}
|
|
51718
|
+
}
|
|
51719
|
+
async function resolveCheckoutUsername(anvilUrl, explicitUsername, getAccounts = auth_getAccountsForUrl) {
|
|
51720
|
+
if (explicitUsername) return explicitUsername;
|
|
51721
|
+
const accounts = getAccounts(anvilUrl);
|
|
51722
|
+
if (0 === accounts.length) return;
|
|
51723
|
+
if (1 === accounts.length) {
|
|
51724
|
+
logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(accounts[0]));
|
|
51725
|
+
return accounts[0];
|
|
51726
|
+
}
|
|
51727
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
51728
|
+
if (!interactive) throw new Error(`Multiple accounts found for ${anvilUrl}. Use --user <USERNAME> to choose one.`);
|
|
51729
|
+
const choices = accounts.map((acct)=>({
|
|
51730
|
+
name: acct,
|
|
51731
|
+
value: acct
|
|
51732
|
+
}));
|
|
51733
|
+
choices.push({
|
|
51734
|
+
name: "Cancel",
|
|
51735
|
+
value: null
|
|
51736
|
+
});
|
|
51737
|
+
return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used for checkout?`, choices, accounts[0]);
|
|
51738
|
+
}
|
|
51739
|
+
async function executeCheckout(options, deps = defaultCheckoutDeps) {
|
|
51740
|
+
const parsed = parseCheckoutInput(options.input);
|
|
51741
|
+
const anvilUrl = await resolveCheckoutUrl(options.url, parsed.detectedUrl);
|
|
51742
|
+
if (!anvilUrl) return void logger_logger.info("Checkout cancelled.");
|
|
51743
|
+
const resolvedUsername = await resolveCheckoutUsername(anvilUrl, options.user);
|
|
51744
|
+
if (null === resolvedUsername) return void logger_logger.info("Checkout cancelled.");
|
|
51745
|
+
const authToken = await ensureCheckoutAuthToken(anvilUrl, resolvedUsername, deps);
|
|
51746
|
+
let checkoutUsername = resolvedUsername;
|
|
51747
|
+
if (!checkoutUsername) {
|
|
51748
|
+
const inferredUsername = await resolveCheckoutUsername(anvilUrl, void 0);
|
|
51749
|
+
if (null === inferredUsername) return void logger_logger.info("Checkout cancelled.");
|
|
51750
|
+
if (!inferredUsername) throw new Error(`Could not determine account for ${anvilUrl}. Use --user <USERNAME> so checkout can bind repository credentials.`);
|
|
51751
|
+
checkoutUsername = inferredUsername;
|
|
51752
|
+
}
|
|
51753
|
+
const validation = await deps.validateAppId(parsed.appId, anvilUrl, checkoutUsername);
|
|
51754
|
+
if (!validation.valid) throw new Error(validation.error || `App '${parsed.appId}' is not accessible on ${anvilUrl}.`);
|
|
51755
|
+
const destinationDir = options.directory || getDefaultDestinationDirectory(parsed.appId, validation.app_name);
|
|
51756
|
+
const destinationPath = external_path_default().resolve(process.cwd(), destinationDir);
|
|
51757
|
+
const destinationDisplay = formatPathForDisplay(destinationPath);
|
|
51758
|
+
await validateCheckoutDestination(destinationPath, options.force);
|
|
51759
|
+
const cloneUrl = getGitPushUrl(parsed.appId, authToken, anvilUrl);
|
|
51760
|
+
logger_logger.progress("checkout", `Checking out app ${parsed.appId} from ${anvilUrl}`);
|
|
51761
|
+
logger_logger.info(chalk_source.gray(` Destination directory: ${destinationDisplay}`));
|
|
51762
|
+
await deps.clone(cloneUrl, destinationPath, {
|
|
51763
|
+
branch: options.branch,
|
|
51764
|
+
depth: options.depth,
|
|
51765
|
+
singleBranch: options.singleBranch,
|
|
51766
|
+
origin: options.origin,
|
|
51767
|
+
quiet: options.quiet,
|
|
51768
|
+
verbose: options.verbose
|
|
51769
|
+
});
|
|
51770
|
+
const remoteName = options.origin || "origin";
|
|
51771
|
+
try {
|
|
51772
|
+
await deps.hardenCheckoutGitAuth({
|
|
51773
|
+
repoPath: destinationPath,
|
|
51774
|
+
appId: parsed.appId,
|
|
51775
|
+
anvilUrl,
|
|
51776
|
+
username: checkoutUsername,
|
|
51777
|
+
remoteName
|
|
51778
|
+
});
|
|
51779
|
+
} catch (e) {
|
|
51780
|
+
throw new Error(`Checkout clone succeeded, but failed to configure repository credentials: ${errors_getErrorMessage(e)}. The repository exists at ${destinationDisplay}, but Git auth bridge setup is incomplete.`);
|
|
51781
|
+
}
|
|
51782
|
+
logger_logger.progressEnd("checkout", `Checked out ${parsed.appId} into ${destinationDisplay}`);
|
|
51783
|
+
const preferredEditor = String(getConfig("preferredEditor") || "").trim();
|
|
51784
|
+
const preferredEditorCommand = preferredEditor ? getPreferredEditorCommand(preferredEditor) : "";
|
|
51785
|
+
if (options.open) if (preferredEditorCommand) await openInPreferredEditor(preferredEditorCommand, destinationPath);
|
|
51786
|
+
else {
|
|
51787
|
+
await node_modules_open(destinationPath);
|
|
51788
|
+
logger_logger.info(chalk_source.gray(`Opened ${destinationDisplay}`));
|
|
51789
|
+
}
|
|
51790
|
+
if (preferredEditorCommand && !options.open) logger_logger.info(chalk_source.cyan(`Next: ${preferredEditorCommand} ${destinationDisplay}`));
|
|
51791
|
+
}
|
|
51792
|
+
async function openInPreferredEditor(editorCommand, destinationPath) {
|
|
51793
|
+
await new Promise((resolve, reject)=>{
|
|
51794
|
+
const child = (0, external_child_process_.spawn)(editorCommand, [
|
|
51795
|
+
destinationPath
|
|
51796
|
+
], {
|
|
51797
|
+
shell: true,
|
|
51798
|
+
stdio: "inherit"
|
|
51799
|
+
});
|
|
51800
|
+
child.on("error", reject);
|
|
51801
|
+
child.on("exit", (code)=>{
|
|
51802
|
+
if (0 === code || null === code) resolve();
|
|
51803
|
+
else reject(new Error(`Editor command '${editorCommand}' exited with code ${code}`));
|
|
51804
|
+
});
|
|
51805
|
+
});
|
|
51806
|
+
}
|
|
51807
|
+
function registerCheckoutCommand(program) {
|
|
51808
|
+
const checkoutCommand = program.command("checkout <input> [directory]").description("Check out an Anvil app locally from editor URL, git URL, or app ID").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("-q, --quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").action(async (input, directory, options)=>{
|
|
51809
|
+
try {
|
|
51810
|
+
if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
|
|
51811
|
+
await executeCheckout({
|
|
51812
|
+
input,
|
|
51813
|
+
directory,
|
|
51814
|
+
open: options?.open,
|
|
51815
|
+
branch: options?.branch,
|
|
51816
|
+
depth: options?.depth,
|
|
51817
|
+
singleBranch: options?.singleBranch,
|
|
51818
|
+
origin: options?.origin,
|
|
51819
|
+
quiet: options?.quiet,
|
|
51820
|
+
verbose: options?.verbose,
|
|
51821
|
+
url: options?.url,
|
|
51822
|
+
user: options?.user,
|
|
51823
|
+
force: options?.force
|
|
51824
|
+
}, defaultCheckoutDeps);
|
|
51825
|
+
} catch (e) {
|
|
51826
|
+
logger_logger.error("Error: " + errors_getErrorMessage(e));
|
|
51827
|
+
process.exit(1);
|
|
51828
|
+
}
|
|
51829
|
+
});
|
|
51830
|
+
checkoutCommand.addHelpText("after", "\n" + chalk_source.bold("Examples:") + "\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n");
|
|
51831
|
+
}
|
|
51832
|
+
const defaultDeps = {
|
|
51833
|
+
getValidAuthToken: auth_getValidAuthToken,
|
|
51834
|
+
getAccountsForUrl: auth_getAccountsForUrl,
|
|
51835
|
+
getAppAuthBinding: getAppAuthBinding,
|
|
51836
|
+
cwd: ()=>process.cwd()
|
|
51837
|
+
};
|
|
51838
|
+
function parseGitCredentialRequest(rawInput) {
|
|
51839
|
+
const out = {};
|
|
51840
|
+
const lines = rawInput.split(/\r?\n/);
|
|
51841
|
+
for (const line of lines){
|
|
51842
|
+
if (!line) continue;
|
|
51843
|
+
const eqIdx = line.indexOf("=");
|
|
51844
|
+
if (eqIdx <= 0) continue;
|
|
51845
|
+
const key = line.slice(0, eqIdx);
|
|
51846
|
+
const value = line.slice(eqIdx + 1);
|
|
51847
|
+
if ("protocol" === key || "host" === key || "path" === key || "username" === key) out[key] = value;
|
|
51848
|
+
}
|
|
51849
|
+
return out;
|
|
51850
|
+
}
|
|
51851
|
+
async function buildGitCredentialResponse(operation, request, deps = defaultDeps) {
|
|
51852
|
+
if ("get" !== operation) return null;
|
|
51853
|
+
if (!request.protocol || !request.host) return null;
|
|
51854
|
+
const appId = parseAppIdFromGitPath(request.path);
|
|
51855
|
+
if (!appId) return null;
|
|
51856
|
+
const requestUrl = normalizeAnvilUrl(`${request.protocol}://${request.host}`);
|
|
51857
|
+
const binding = await deps.getAppAuthBinding(deps.cwd(), appId);
|
|
51858
|
+
const anvilUrl = binding.url ? normalizeAnvilUrl(binding.url) : requestUrl;
|
|
51859
|
+
let username = binding.username;
|
|
51860
|
+
if (!username) {
|
|
51861
|
+
const accounts = deps.getAccountsForUrl(anvilUrl);
|
|
51862
|
+
if (1 === accounts.length) username = accounts[0];
|
|
51863
|
+
else if (accounts.length > 1) throw new Error(`Multiple accounts found for ${anvilUrl}; bind one with anvil checkout --user.`);
|
|
51864
|
+
else throw new Error(`No account found for ${anvilUrl}. Run 'anvil login ${anvilUrl}'.`);
|
|
51865
|
+
}
|
|
51866
|
+
const token = await deps.getValidAuthToken(anvilUrl, username);
|
|
51867
|
+
return {
|
|
51868
|
+
username: "git",
|
|
51869
|
+
password: token
|
|
51870
|
+
};
|
|
51871
|
+
}
|
|
51872
|
+
async function readStdin() {
|
|
51873
|
+
const chunks = [];
|
|
51874
|
+
for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
51875
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
51876
|
+
}
|
|
51877
|
+
function writeGitCredentialResponse(response) {
|
|
51878
|
+
if (!response) return;
|
|
51879
|
+
const lines = [];
|
|
51880
|
+
for (const [k, v] of Object.entries(response))lines.push(`${k}=${v}`);
|
|
51881
|
+
process.stdout.write(lines.join("\n") + "\n\n");
|
|
51882
|
+
}
|
|
51883
|
+
async function executeGitCredentialOperation(operation, deps = defaultDeps) {
|
|
51884
|
+
const raw = await readStdin();
|
|
51885
|
+
const request = parseGitCredentialRequest(raw);
|
|
51886
|
+
const response = await buildGitCredentialResponse(operation, request, deps);
|
|
51887
|
+
writeGitCredentialResponse(response);
|
|
51888
|
+
}
|
|
51889
|
+
function registerGitCredentialCommand(program) {
|
|
51890
|
+
program.command("git-credential <operation>", {
|
|
51891
|
+
hidden: true
|
|
51892
|
+
}).description("Internal helper command for Git HTTPS authentication").action(async (operation)=>{
|
|
51893
|
+
try {
|
|
51894
|
+
await executeGitCredentialOperation(operation);
|
|
51895
|
+
} catch {
|
|
51896
|
+
process.exit(1);
|
|
51897
|
+
}
|
|
51898
|
+
});
|
|
51334
51899
|
}
|
|
51335
51900
|
function registerLoginCommand(program) {
|
|
51336
51901
|
const loginCommand = program.command("login [anvil-server-url]").description("Log in to Anvil using OAuth").alias("l").action(async (anvilUrl)=>{
|
|
@@ -51339,93 +51904,8 @@ var __webpack_exports__ = {};
|
|
|
51339
51904
|
anvilUrl = normalizeAnvilUrl(anvilUrl.trim());
|
|
51340
51905
|
setConfig("anvilUrl", anvilUrl);
|
|
51341
51906
|
} else anvilUrl = resolveAnvilUrl();
|
|
51342
|
-
const
|
|
51343
|
-
|
|
51344
|
-
const state = external_crypto_.randomBytes(16).toString("hex");
|
|
51345
|
-
const server = external_http_.createServer();
|
|
51346
|
-
await new Promise((resolve)=>server.listen(0, "127.0.0.1", resolve));
|
|
51347
|
-
const address = server.address();
|
|
51348
|
-
if (!address || "string" == typeof address) throw new Error("No address");
|
|
51349
|
-
const port = address.port;
|
|
51350
|
-
const redirectUri = `http://127.0.0.1:${port}/oauth-callback`;
|
|
51351
|
-
const SCOPES = "apps:read apps:write user:read";
|
|
51352
|
-
const authUrl = new URL(`${anvilUrl}/oauth/authorize`);
|
|
51353
|
-
authUrl.searchParams.set("response_type", "code");
|
|
51354
|
-
authUrl.searchParams.set("client_id", "anvil-sync");
|
|
51355
|
-
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
51356
|
-
authUrl.searchParams.set("scope", SCOPES);
|
|
51357
|
-
authUrl.searchParams.set("state", state);
|
|
51358
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
51359
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
51360
|
-
const codePromise = new Promise((resolve, reject)=>{
|
|
51361
|
-
server.on("request", (req, res)=>{
|
|
51362
|
-
if (!req.url) return;
|
|
51363
|
-
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
51364
|
-
if ("/oauth-callback" !== url.pathname) return;
|
|
51365
|
-
const code = url.searchParams.get("code") || void 0;
|
|
51366
|
-
const error = url.searchParams.get("error") || void 0;
|
|
51367
|
-
const recvState = url.searchParams.get("state");
|
|
51368
|
-
if (!recvState || !code && !error) {
|
|
51369
|
-
res.statusCode = 400;
|
|
51370
|
-
res.end("Missing code, error or state");
|
|
51371
|
-
reject(new Error("Missing code/state/error"));
|
|
51372
|
-
server.close();
|
|
51373
|
-
return;
|
|
51374
|
-
}
|
|
51375
|
-
res.statusCode = 200;
|
|
51376
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
51377
|
-
if (code) res.end(successPage);
|
|
51378
|
-
else res.end(errorPage(error || "unknown"));
|
|
51379
|
-
resolve({
|
|
51380
|
-
code,
|
|
51381
|
-
error,
|
|
51382
|
-
recvState
|
|
51383
|
-
});
|
|
51384
|
-
server.closeAllConnections();
|
|
51385
|
-
server.close();
|
|
51386
|
-
});
|
|
51387
|
-
});
|
|
51388
|
-
logger_logger.info(chalk_source.blue("Link to your Anvil account to continue."));
|
|
51389
|
-
console.log();
|
|
51390
|
-
logger_logger.info(chalk_source.gray(` ${authUrl}`));
|
|
51391
|
-
console.log();
|
|
51392
|
-
const { code, error, recvState } = await waitForOAuthWithPrompt(authUrl, codePromise);
|
|
51393
|
-
if (recvState !== state) {
|
|
51394
|
-
logger_logger.error("Invalid state received from OAuth callback");
|
|
51395
|
-
process.exit(1);
|
|
51396
|
-
}
|
|
51397
|
-
if (error) {
|
|
51398
|
-
logger_logger.error(`Error received from OAuth callback: ${error}`);
|
|
51399
|
-
process.exit(1);
|
|
51400
|
-
}
|
|
51401
|
-
const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
|
|
51402
|
-
method: "POST",
|
|
51403
|
-
headers: {
|
|
51404
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
51405
|
-
},
|
|
51406
|
-
body: new URLSearchParams({
|
|
51407
|
-
grant_type: "authorization_code",
|
|
51408
|
-
code: code,
|
|
51409
|
-
redirect_uri: redirectUri,
|
|
51410
|
-
client_id: "anvil-sync",
|
|
51411
|
-
code_verifier: codeVerifier
|
|
51412
|
-
})
|
|
51413
|
-
});
|
|
51414
|
-
if (tokenResponse.ok) {
|
|
51415
|
-
const tokenData = await tokenResponse.json();
|
|
51416
|
-
const result = await login(anvilUrl, {
|
|
51417
|
-
access_token: tokenData.access_token,
|
|
51418
|
-
refresh_token: tokenData.refresh_token,
|
|
51419
|
-
expires_in: tokenData.expires_in,
|
|
51420
|
-
scope: tokenData.scope
|
|
51421
|
-
});
|
|
51422
|
-
logger_logger.success("Logged in as " + chalk_source.bold(result.email));
|
|
51423
|
-
} else {
|
|
51424
|
-
logger_logger.error("Failed to exchange authorization code for token.");
|
|
51425
|
-
const errorText = await tokenResponse.text();
|
|
51426
|
-
logger_logger.error(` Server response: ${errorText}`);
|
|
51427
|
-
process.exit(1);
|
|
51428
|
-
}
|
|
51907
|
+
const result = await runInteractiveLoginFlow(anvilUrl);
|
|
51908
|
+
logger_logger.success("Logged in as " + chalk_source.bold(result.email));
|
|
51429
51909
|
} catch (e) {
|
|
51430
51910
|
logger_logger.error("Error: " + e.message);
|
|
51431
51911
|
process.exit(1);
|
|
@@ -51529,6 +52009,14 @@ var __webpack_exports__ = {};
|
|
|
51529
52009
|
});
|
|
51530
52010
|
logoutCommand.addHelpText("after", "\n" + chalk_source.bold("Examples:") + "\n anvil logout Logout from all accounts (or prompt if multiple)\n anvil logout anvil.works Logout from anvil.works\n anvil logout -u anvil.works Logout from anvil.works (using option)\n anvil logout -u anvil.works -U user@example.com Logout specific user\n");
|
|
51531
52011
|
}
|
|
52012
|
+
const defaultConfigResetDeps = {
|
|
52013
|
+
logout: logout,
|
|
52014
|
+
resetConfig: resetConfig
|
|
52015
|
+
};
|
|
52016
|
+
async function performConfigReset(deps = defaultConfigResetDeps) {
|
|
52017
|
+
await deps.logout();
|
|
52018
|
+
deps.resetConfig();
|
|
52019
|
+
}
|
|
51532
52020
|
function registerConfigCommand(program) {
|
|
51533
52021
|
const configCommand = program.command("config").description("Manage configuration").alias("c");
|
|
51534
52022
|
configCommand.command("get <key>").description("Get a configuration value").action(async (key)=>{
|
|
@@ -51547,8 +52035,7 @@ var __webpack_exports__ = {};
|
|
|
51547
52035
|
});
|
|
51548
52036
|
configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value)=>{
|
|
51549
52037
|
try {
|
|
51550
|
-
|
|
51551
|
-
if ("anvilUrl" === key && "string" == typeof typedValue) typedValue = typedValue.trim().replace(/\/+$/, "");
|
|
52038
|
+
const typedValue = parseConfigSetValue(key, value);
|
|
51552
52039
|
setConfig(key, typedValue);
|
|
51553
52040
|
logger_logger.success(`Set ${key} = ${typedValue}`);
|
|
51554
52041
|
} catch (error) {
|
|
@@ -51608,7 +52095,7 @@ var __webpack_exports__ = {};
|
|
|
51608
52095
|
});
|
|
51609
52096
|
configCommand.command("reset").description("Reset configuration to defaults").action(async ()=>{
|
|
51610
52097
|
try {
|
|
51611
|
-
|
|
52098
|
+
await performConfigReset();
|
|
51612
52099
|
logger_logger.success("Configuration reset to defaults.");
|
|
51613
52100
|
} catch (e) {
|
|
51614
52101
|
logger_logger.error("Error: " + e.message);
|
|
@@ -51624,6 +52111,136 @@ var __webpack_exports__ = {};
|
|
|
51624
52111
|
console.log();
|
|
51625
52112
|
});
|
|
51626
52113
|
}
|
|
52114
|
+
const onboard_defaultDeps = {
|
|
52115
|
+
getLatestVersion: getLatestVersion,
|
|
52116
|
+
getAccountsForUrl: auth_getAccountsForUrl,
|
|
52117
|
+
runInteractiveLoginFlow: runInteractiveLoginFlow,
|
|
52118
|
+
executeCheckout: executeCheckout
|
|
52119
|
+
};
|
|
52120
|
+
const DEFAULT_ANVIL_SERVER_URL_PROMPT = "Default Anvil server URL";
|
|
52121
|
+
async function getOnboardVersionStatus(currentVersion, latestVersionGetter = getLatestVersion) {
|
|
52122
|
+
const latestVersion = await latestVersionGetter();
|
|
52123
|
+
if (!latestVersion) return {
|
|
52124
|
+
currentVersion,
|
|
52125
|
+
latestVersion: null,
|
|
52126
|
+
isOutdated: null
|
|
52127
|
+
};
|
|
52128
|
+
if (!semver_default().valid(currentVersion) || !semver_default().valid(latestVersion)) return {
|
|
52129
|
+
currentVersion,
|
|
52130
|
+
latestVersion,
|
|
52131
|
+
isOutdated: null
|
|
52132
|
+
};
|
|
52133
|
+
return {
|
|
52134
|
+
currentVersion,
|
|
52135
|
+
latestVersion,
|
|
52136
|
+
isOutdated: semver_default().lt(currentVersion, latestVersion)
|
|
52137
|
+
};
|
|
52138
|
+
}
|
|
52139
|
+
function formatLoggedInAccounts(accounts) {
|
|
52140
|
+
if (0 === accounts.length) return "no account";
|
|
52141
|
+
if (1 === accounts.length) return accounts[0];
|
|
52142
|
+
return `${accounts.length} accounts`;
|
|
52143
|
+
}
|
|
52144
|
+
function isInteractiveSession() {
|
|
52145
|
+
return !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
52146
|
+
}
|
|
52147
|
+
async function runOnboardFlow(version, deps = onboard_defaultDeps) {
|
|
52148
|
+
if (!isInteractiveSession()) throw new Error("`anvil onboard` is interactive and requires a TTY terminal.");
|
|
52149
|
+
console.log();
|
|
52150
|
+
logger_logger.info(chalk_source.cyan.bold("Anvil Onboarding"));
|
|
52151
|
+
console.log();
|
|
52152
|
+
logger_logger.progress("onboard-version", "Checking CLI version...");
|
|
52153
|
+
const versionStatus = await getOnboardVersionStatus(version, deps.getLatestVersion);
|
|
52154
|
+
logger_logger.progressEnd("onboard-version");
|
|
52155
|
+
logger_logger.info(chalk_source.cyan("CLI version: ") + chalk_source.bold(versionStatus.currentVersion));
|
|
52156
|
+
if (versionStatus.latestVersion) {
|
|
52157
|
+
logger_logger.info(chalk_source.cyan("Latest version: ") + chalk_source.bold(versionStatus.latestVersion));
|
|
52158
|
+
if (versionStatus.isOutdated) {
|
|
52159
|
+
logger_logger.warn("Your anvil-cli is out of date.");
|
|
52160
|
+
logger_logger.info(chalk_source.gray("Run `anvil update` to see update commands."));
|
|
52161
|
+
const continueOnOldVersion = await logger_logger.confirm("Continue onboarding anyway?", true);
|
|
52162
|
+
if (!continueOnOldVersion) return void logger_logger.info("Onboarding cancelled.");
|
|
52163
|
+
}
|
|
52164
|
+
} else logger_logger.warn("Could not determine latest published version.");
|
|
52165
|
+
console.log();
|
|
52166
|
+
const currentUrl = resolveAnvilUrl();
|
|
52167
|
+
logger_logger.info(chalk_source.gray("Leave this blank unless you're using a dedicated enterprise/on-prem Anvil installation."));
|
|
52168
|
+
const urlAnswer = await logger_logger.prompt([
|
|
52169
|
+
{
|
|
52170
|
+
type: "input",
|
|
52171
|
+
name: "anvilUrl",
|
|
52172
|
+
message: DEFAULT_ANVIL_SERVER_URL_PROMPT,
|
|
52173
|
+
default: currentUrl
|
|
52174
|
+
}
|
|
52175
|
+
]);
|
|
52176
|
+
const configuredAnvilUrl = parseConfigSetValue("anvilUrl", urlAnswer.anvilUrl);
|
|
52177
|
+
setConfig("anvilUrl", configuredAnvilUrl);
|
|
52178
|
+
logger_logger.success(`Set default Anvil server URL (anvilUrl) = ${configuredAnvilUrl}`);
|
|
52179
|
+
const currentEditor = String(getConfig("preferredEditor") || "").trim().toLowerCase();
|
|
52180
|
+
const editorChoices = [
|
|
52181
|
+
{
|
|
52182
|
+
name: "None",
|
|
52183
|
+
value: ""
|
|
52184
|
+
},
|
|
52185
|
+
...preferredEditors.map((editor)=>({
|
|
52186
|
+
name: editor,
|
|
52187
|
+
value: editor
|
|
52188
|
+
}))
|
|
52189
|
+
];
|
|
52190
|
+
const selectedEditor = await logger_logger.select("Preferred editor", editorChoices, editorChoices.some((choice)=>choice.value === currentEditor) ? currentEditor : "");
|
|
52191
|
+
setConfig("preferredEditor", selectedEditor);
|
|
52192
|
+
if (selectedEditor) logger_logger.success(`Set preferredEditor = ${selectedEditor}`);
|
|
52193
|
+
else logger_logger.info("Left preferredEditor unset.");
|
|
52194
|
+
const currentVerbose = !!getConfig("verbose");
|
|
52195
|
+
const enableVerbose = await logger_logger.confirm("Enable verbose logging?", currentVerbose);
|
|
52196
|
+
setConfig("verbose", enableVerbose);
|
|
52197
|
+
logger_logger.success(`Set verbose = ${enableVerbose}`);
|
|
52198
|
+
console.log();
|
|
52199
|
+
const existingAccounts = deps.getAccountsForUrl(configuredAnvilUrl);
|
|
52200
|
+
if (existingAccounts.length > 0) logger_logger.success(`Already logged in to ${configuredAnvilUrl} as ${formatLoggedInAccounts(existingAccounts)}.`);
|
|
52201
|
+
else {
|
|
52202
|
+
const shouldLogin = await logger_logger.confirm(`You're not logged in to ${configuredAnvilUrl}. Log in now?`, true);
|
|
52203
|
+
if (shouldLogin) {
|
|
52204
|
+
const loginResult = await deps.runInteractiveLoginFlow(configuredAnvilUrl);
|
|
52205
|
+
logger_logger.success("Logged in as " + chalk_source.bold(loginResult.email));
|
|
52206
|
+
} else logger_logger.warn("Skipping login.");
|
|
52207
|
+
}
|
|
52208
|
+
console.log();
|
|
52209
|
+
const shouldCheckout = await logger_logger.confirm("Check out an app now?", true);
|
|
52210
|
+
if (shouldCheckout) {
|
|
52211
|
+
const checkoutAnswer = await logger_logger.prompt([
|
|
52212
|
+
{
|
|
52213
|
+
type: "input",
|
|
52214
|
+
name: "input",
|
|
52215
|
+
message: "App ID or app URL to checkout"
|
|
52216
|
+
}
|
|
52217
|
+
]);
|
|
52218
|
+
const checkoutInput = checkoutAnswer.input.trim();
|
|
52219
|
+
if (checkoutInput) {
|
|
52220
|
+
const preferredEditorCommand = getPreferredEditorCommand(selectedEditor);
|
|
52221
|
+
let openAfterCheckout = false;
|
|
52222
|
+
if (preferredEditorCommand) openAfterCheckout = await logger_logger.confirm(`Open checked out app in ${preferredEditorCommand} after checkout?`, true);
|
|
52223
|
+
else logger_logger.info("No preferred editor is configured, so open-in-editor is skipped.");
|
|
52224
|
+
await deps.executeCheckout({
|
|
52225
|
+
input: checkoutInput,
|
|
52226
|
+
url: configuredAnvilUrl,
|
|
52227
|
+
open: openAfterCheckout
|
|
52228
|
+
});
|
|
52229
|
+
} else logger_logger.info("No app provided; skipping checkout.");
|
|
52230
|
+
} else logger_logger.info("Skipping checkout.");
|
|
52231
|
+
console.log();
|
|
52232
|
+
logger_logger.success("Onboarding complete.");
|
|
52233
|
+
}
|
|
52234
|
+
function registerOnboardCommand(program, version) {
|
|
52235
|
+
program.command("onboard").description("Guided setup for configuration, login, and optional app checkout").action(async ()=>{
|
|
52236
|
+
try {
|
|
52237
|
+
await runOnboardFlow(version, onboard_defaultDeps);
|
|
52238
|
+
} catch (e) {
|
|
52239
|
+
logger_logger.error("Error: " + errors_getErrorMessage(e));
|
|
52240
|
+
process.exit(1);
|
|
52241
|
+
}
|
|
52242
|
+
});
|
|
52243
|
+
}
|
|
51627
52244
|
const packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
|
|
51628
52245
|
const VERSION = packageJson.version;
|
|
51629
52246
|
setLogger(new CLILogger({
|
|
@@ -51687,14 +52304,17 @@ var __webpack_exports__ = {};
|
|
|
51687
52304
|
});
|
|
51688
52305
|
if (!opts.json) {
|
|
51689
52306
|
const commandName = actionCommand?.name();
|
|
51690
|
-
if ("update" !== commandName) checkVersionAndWarn();
|
|
52307
|
+
if ("update" !== commandName && "git-credential" !== commandName) checkVersionAndWarn();
|
|
51691
52308
|
}
|
|
51692
52309
|
});
|
|
51693
52310
|
const cli_watchCommand = registerWatchCommand(cli_program);
|
|
52311
|
+
registerCheckoutCommand(cli_program);
|
|
52312
|
+
registerGitCredentialCommand(cli_program);
|
|
51694
52313
|
registerLoginCommand(cli_program);
|
|
51695
52314
|
registerLogoutCommand(cli_program);
|
|
51696
52315
|
registerConfigCommand(cli_program);
|
|
51697
52316
|
registerVersionCommand(cli_program, VERSION);
|
|
52317
|
+
registerOnboardCommand(cli_program, VERSION);
|
|
51698
52318
|
cli_program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
|
|
51699
52319
|
await handleUpdateCommand();
|
|
51700
52320
|
});
|
package/dist/index.js
CHANGED
|
@@ -20042,7 +20042,8 @@ var __webpack_exports__ = {};
|
|
|
20042
20042
|
}
|
|
20043
20043
|
const configDefaults = {
|
|
20044
20044
|
devMode: false,
|
|
20045
|
-
verbose: false
|
|
20045
|
+
verbose: false,
|
|
20046
|
+
preferredEditor: ""
|
|
20046
20047
|
};
|
|
20047
20048
|
function isTestMode() {
|
|
20048
20049
|
return "test" === process.env.NODE_ENV || !!process.env.RSTEST;
|
|
@@ -22721,6 +22722,7 @@ var __webpack_exports__ = {};
|
|
|
22721
22722
|
});
|
|
22722
22723
|
this.ws.on("error", (error)=>{
|
|
22723
22724
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
|
|
22725
|
+
if (this.isClosing) return;
|
|
22724
22726
|
if (error.message.includes("502")) logger_logger.error(chalk_source.red("WebSocket connection failed (502 Bad Gateway) - likely a temporary server issue"));
|
|
22725
22727
|
else logger_logger.error(chalk_source.red(`WebSocket error: ${error.message}`));
|
|
22726
22728
|
this.emit("error", {
|
|
@@ -22821,15 +22823,20 @@ var __webpack_exports__ = {};
|
|
|
22821
22823
|
}
|
|
22822
22824
|
teardownSocket() {
|
|
22823
22825
|
this.stopHeartbeat();
|
|
22824
|
-
|
|
22826
|
+
const socket = this.ws;
|
|
22827
|
+
this.ws = null;
|
|
22828
|
+
if (!socket) return;
|
|
22825
22829
|
try {
|
|
22826
22830
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
|
|
22827
|
-
|
|
22828
|
-
|
|
22831
|
+
if (socket.readyState === ws_wrapper.CONNECTING) {
|
|
22832
|
+
socket.once("error", ()=>{});
|
|
22833
|
+
socket.terminate();
|
|
22834
|
+
return;
|
|
22835
|
+
}
|
|
22836
|
+
if (socket.readyState === ws_wrapper.OPEN) return void socket.close();
|
|
22829
22837
|
} catch (e) {
|
|
22830
22838
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
|
|
22831
22839
|
}
|
|
22832
|
-
this.ws = null;
|
|
22833
22840
|
}
|
|
22834
22841
|
}
|
|
22835
22842
|
async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
|