@anvil-works/anvil-cli 0.4.2 → 0.4.3
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 +35 -30
- package/dist/cli.js +126 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,9 +85,7 @@ Or use the short form:
|
|
|
85
85
|
anvil w
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
You can specify the app ID explicitly:
|
|
88
|
+
You can specify the app ID explicitly:
|
|
91
89
|
|
|
92
90
|
```bash
|
|
93
91
|
anvil watch -A YOUR_APP_ID
|
|
@@ -126,7 +124,6 @@ The diagram shows bidirectional sync:
|
|
|
126
124
|
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
127
125
|
| `anvil watch [path]` | Watch directory for changes and sync to Anvil |
|
|
128
126
|
| `anvil watch -V` or `--verbose` | Watch with verbose logging (detailed output) |
|
|
129
|
-
| `anvil sync [path]` | Deprecated alias for `watch` |
|
|
130
127
|
| `anvil w [path]` | Short form for watch |
|
|
131
128
|
| `anvil login [anvil-server-url]` | Authenticate with Anvil using OAuth (supports smart URL handling) |
|
|
132
129
|
| `anvil l [anvil-server-url]` | Short form for login |
|
|
@@ -153,13 +150,15 @@ anvil watch -A YOUR_APP_ID
|
|
|
153
150
|
|
|
154
151
|
### Anvil URL Detection
|
|
155
152
|
|
|
156
|
-
anvil-cli automatically detects which Anvil server to use:
|
|
157
|
-
|
|
158
|
-
1. **Explicit URL**: If you specify `--url` or `-u` flag
|
|
159
|
-
2. **Git Remotes**: Automatically extracts URL from git remotes in your repository
|
|
160
|
-
3. **
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
anvil-cli automatically detects which Anvil server to use:
|
|
154
|
+
|
|
155
|
+
1. **Explicit URL**: If you specify `--url` or `-u` flag
|
|
156
|
+
2. **Git Remotes**: Automatically extracts URL from git remotes in your repository
|
|
157
|
+
3. **Logged-in URLs**:
|
|
158
|
+
- If one logged-in URL is available, uses it automatically
|
|
159
|
+
- If multiple logged-in URLs are available, prompts you to select one
|
|
160
|
+
4. **Global Config**: Falls back to `anvilUrl` in config file
|
|
161
|
+
5. **Default**: Uses `https://anvil.works` (or `http://localhost:3000` in dev mode)
|
|
163
162
|
|
|
164
163
|
**Examples:**
|
|
165
164
|
|
|
@@ -172,12 +171,12 @@ anvil watch --url anvil.works
|
|
|
172
171
|
anvil watch --url localhost:3000
|
|
173
172
|
anvil watch --url anvil.mycompany.com
|
|
174
173
|
|
|
175
|
-
# If multiple
|
|
176
|
-
anvil watch
|
|
177
|
-
# ? Multiple Anvil installations found. Which one would you like to use?
|
|
178
|
-
# ❯ https://anvil.works
|
|
179
|
-
# https://anvil.company.com
|
|
180
|
-
# Cancel
|
|
174
|
+
# If multiple logged-in URLs, you'll be prompted to select
|
|
175
|
+
anvil watch
|
|
176
|
+
# ? Multiple Anvil installations found. Which one would you like to use?
|
|
177
|
+
# ❯ https://anvil.works
|
|
178
|
+
# https://anvil.company.com
|
|
179
|
+
# Cancel
|
|
181
180
|
```
|
|
182
181
|
|
|
183
182
|
## Configuration
|
|
@@ -269,18 +268,21 @@ anvil login anvil.works
|
|
|
269
268
|
anvil login anvil.company-a.com
|
|
270
269
|
anvil login anvil.company-b.com
|
|
271
270
|
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
# If URL is not resolved from git remotes and multiple logged-in URLs exist,
|
|
272
|
+
# you'll be prompted to select which one to use
|
|
273
|
+
# Or specify explicitly:
|
|
274
|
+
anvil watch --url anvil.company-a.com
|
|
275
|
+
```
|
|
276
276
|
|
|
277
277
|
**URL Resolution Priority:**
|
|
278
278
|
|
|
279
|
-
1. Explicit `--url` flag
|
|
280
|
-
2. Git remote detection (from current repository)
|
|
281
|
-
3.
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
1. Explicit `--url` flag
|
|
280
|
+
2. Git remote detection (from current repository)
|
|
281
|
+
3. Logged-in URL selection:
|
|
282
|
+
- One URL: auto-select
|
|
283
|
+
- Multiple URLs: prompt to select
|
|
284
|
+
4. `anvilUrl` in config file
|
|
285
|
+
5. Default based on `NODE_ENV`:
|
|
284
286
|
- Production: `https://anvil.works`
|
|
285
287
|
- Development: `http://localhost:3000`
|
|
286
288
|
|
|
@@ -308,10 +310,13 @@ anvil logout anvil.mycompany.com
|
|
|
308
310
|
anvil logout --url localhost:3000
|
|
309
311
|
```
|
|
310
312
|
|
|
311
|
-
**
|
|
312
|
-
|
|
313
|
-
-
|
|
314
|
-
-
|
|
313
|
+
**Without URL:** `anvil logout` uses total logged-in account count across all URLs:
|
|
314
|
+
|
|
315
|
+
- If you have **one account total**, it logs out immediately
|
|
316
|
+
- If you have **multiple accounts total**, you'll be prompted to:
|
|
317
|
+
|
|
318
|
+
- Logout from one account (then select which one)
|
|
319
|
+
- Logout from all accounts
|
|
315
320
|
- Cancel
|
|
316
321
|
|
|
317
322
|
## Troubleshooting
|
package/dist/cli.js
CHANGED
|
@@ -49956,13 +49956,84 @@ var __webpack_exports__ = {};
|
|
|
49956
49956
|
session.hasUncommittedChanges = hasUncommittedChanges;
|
|
49957
49957
|
return session;
|
|
49958
49958
|
}
|
|
49959
|
-
function
|
|
49960
|
-
if (explicitUrl) return
|
|
49961
|
-
|
|
49962
|
-
|
|
49959
|
+
function decideFallbackUrl(explicitUrl, availableUrls = getAvailableAnvilUrls()) {
|
|
49960
|
+
if (explicitUrl) return {
|
|
49961
|
+
source: "explicit",
|
|
49962
|
+
url: normalizeAnvilUrl(explicitUrl)
|
|
49963
|
+
};
|
|
49964
|
+
if (availableUrls.length > 1) return {
|
|
49965
|
+
source: "available-multiple",
|
|
49966
|
+
urls: availableUrls
|
|
49967
|
+
};
|
|
49968
|
+
if (1 === availableUrls.length) return {
|
|
49969
|
+
source: "available-single",
|
|
49970
|
+
url: availableUrls[0]
|
|
49971
|
+
};
|
|
49963
49972
|
const fromConfig = getConfig("anvilUrl");
|
|
49964
|
-
if ("string" == typeof fromConfig && fromConfig.trim()) return
|
|
49965
|
-
|
|
49973
|
+
if ("string" == typeof fromConfig && fromConfig.trim()) return {
|
|
49974
|
+
source: "config",
|
|
49975
|
+
url: normalizeAnvilUrl(fromConfig.trim())
|
|
49976
|
+
};
|
|
49977
|
+
return {
|
|
49978
|
+
source: "default",
|
|
49979
|
+
url: isDevMode() ? "http://localhost:3000" : "https://anvil.works"
|
|
49980
|
+
};
|
|
49981
|
+
}
|
|
49982
|
+
function decideUsernameForUrl(accounts) {
|
|
49983
|
+
if (0 === accounts.length) return {
|
|
49984
|
+
source: "none"
|
|
49985
|
+
};
|
|
49986
|
+
if (1 === accounts.length) return {
|
|
49987
|
+
source: "single",
|
|
49988
|
+
username: accounts[0]
|
|
49989
|
+
};
|
|
49990
|
+
return {
|
|
49991
|
+
source: "multiple",
|
|
49992
|
+
usernames: [
|
|
49993
|
+
...accounts
|
|
49994
|
+
]
|
|
49995
|
+
};
|
|
49996
|
+
}
|
|
49997
|
+
async function resolveUsernameForUrl(anvilUrl, explicitUsername, promptMessage = "Multiple accounts found. Which account owns this app?") {
|
|
49998
|
+
if (explicitUsername) return explicitUsername;
|
|
49999
|
+
const decision = decideUsernameForUrl(auth_getAccountsForUrl(anvilUrl));
|
|
50000
|
+
if ("none" === decision.source) return;
|
|
50001
|
+
if ("single" === decision.source) {
|
|
50002
|
+
logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(decision.username));
|
|
50003
|
+
return decision.username;
|
|
50004
|
+
}
|
|
50005
|
+
const choices = decision.usernames.map((acct)=>({
|
|
50006
|
+
name: acct,
|
|
50007
|
+
value: acct
|
|
50008
|
+
}));
|
|
50009
|
+
choices.push({
|
|
50010
|
+
name: "Cancel",
|
|
50011
|
+
value: null
|
|
50012
|
+
});
|
|
50013
|
+
return logger_logger.select(promptMessage, choices, decision.usernames[0]);
|
|
50014
|
+
}
|
|
50015
|
+
async function confirmReverseLookupWithResolvedUser(anvilUrl, username) {
|
|
50016
|
+
const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username, `Multiple accounts found for ${anvilUrl}. Which account should be used for app lookup?`);
|
|
50017
|
+
if (null === resolvedUsername) return null;
|
|
50018
|
+
const shouldContinue = await logger_logger.confirm(`Search ${anvilUrl} ${resolvedUsername ? `for ${resolvedUsername}` : ""} for matching app IDs? (slower)`, true);
|
|
50019
|
+
return {
|
|
50020
|
+
username: resolvedUsername,
|
|
50021
|
+
shouldContinue
|
|
50022
|
+
};
|
|
50023
|
+
}
|
|
50024
|
+
async function resolveUrlForFallback(explicitUrl) {
|
|
50025
|
+
const decision = decideFallbackUrl(explicitUrl);
|
|
50026
|
+
if ("available-multiple" !== decision.source) return decision.url;
|
|
50027
|
+
const choices = decision.urls.map((url)=>({
|
|
50028
|
+
name: url,
|
|
50029
|
+
value: url
|
|
50030
|
+
}));
|
|
50031
|
+
choices.push({
|
|
50032
|
+
name: "Cancel",
|
|
50033
|
+
value: null
|
|
50034
|
+
});
|
|
50035
|
+
const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
|
|
50036
|
+
return selectedUrl;
|
|
49966
50037
|
}
|
|
49967
50038
|
async function selectAppId(candidates) {
|
|
49968
50039
|
if (0 === candidates.length) {
|
|
@@ -50474,6 +50545,7 @@ var __webpack_exports__ = {};
|
|
|
50474
50545
|
let finalAppId;
|
|
50475
50546
|
let anvilUrl;
|
|
50476
50547
|
let username = explicitUsername;
|
|
50548
|
+
let fallbackUrl;
|
|
50477
50549
|
if (explicitAppId) {
|
|
50478
50550
|
finalAppId = explicitAppId;
|
|
50479
50551
|
const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
|
|
@@ -50489,13 +50561,23 @@ var __webpack_exports__ = {};
|
|
|
50489
50561
|
logger_logger.verbose(chalk_source.cyan("No app ID provided, attempting auto-detection..."));
|
|
50490
50562
|
if (0 === filteredCandidates.length) {
|
|
50491
50563
|
logger_logger.verbose(chalk_source.gray("No app IDs found in git remotes."));
|
|
50492
|
-
const
|
|
50493
|
-
|
|
50494
|
-
|
|
50495
|
-
|
|
50564
|
+
const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
|
|
50565
|
+
if (null === resolvedFallbackUrl) {
|
|
50566
|
+
logger_logger.warn("Operation cancelled.");
|
|
50567
|
+
process.exit(0);
|
|
50568
|
+
}
|
|
50569
|
+
fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
|
|
50570
|
+
const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
|
|
50571
|
+
if (null === lookupDecision) {
|
|
50572
|
+
logger_logger.warn("Operation cancelled.");
|
|
50573
|
+
process.exit(0);
|
|
50574
|
+
}
|
|
50575
|
+
username = lookupDecision.username;
|
|
50576
|
+
if (lookupDecision.shouldContinue) {
|
|
50577
|
+
logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
|
|
50496
50578
|
const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
|
|
50497
50579
|
anvilUrl: fallbackUrl,
|
|
50498
|
-
username
|
|
50580
|
+
username,
|
|
50499
50581
|
includeRemotes: false
|
|
50500
50582
|
});
|
|
50501
50583
|
logger_logger.progressEnd("detect");
|
|
@@ -50527,32 +50609,24 @@ var __webpack_exports__ = {};
|
|
|
50527
50609
|
}
|
|
50528
50610
|
}
|
|
50529
50611
|
if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
|
|
50530
|
-
else if (!anvilUrl) anvilUrl =
|
|
50612
|
+
else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
|
|
50613
|
+
else {
|
|
50614
|
+
const resolvedFallbackUrl = await resolveUrlForFallback();
|
|
50615
|
+
if (null === resolvedFallbackUrl) {
|
|
50616
|
+
logger_logger.warn("Operation cancelled.");
|
|
50617
|
+
process.exit(0);
|
|
50618
|
+
}
|
|
50619
|
+
anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
|
|
50620
|
+
}
|
|
50531
50621
|
anvilUrl = normalizeAnvilUrl(anvilUrl);
|
|
50532
50622
|
logger_logger.verbose(chalk_source.green("Using app ID: ") + chalk_source.bold(finalAppId));
|
|
50533
50623
|
logger_logger.verbose(chalk_source.cyan("Using Anvil URL: ") + chalk_source.bold(anvilUrl));
|
|
50534
|
-
|
|
50535
|
-
|
|
50536
|
-
|
|
50537
|
-
|
|
50538
|
-
logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(username));
|
|
50539
|
-
} else if (accounts.length > 1) {
|
|
50540
|
-
const choices = accounts.map((acct)=>({
|
|
50541
|
-
name: acct,
|
|
50542
|
-
value: acct
|
|
50543
|
-
}));
|
|
50544
|
-
choices.push({
|
|
50545
|
-
name: "Cancel",
|
|
50546
|
-
value: null
|
|
50547
|
-
});
|
|
50548
|
-
const selected = await logger_logger.select("Multiple accounts found. Which account owns this app?", choices, accounts[0]);
|
|
50549
|
-
if (null === selected) {
|
|
50550
|
-
logger_logger.warn("Operation cancelled.");
|
|
50551
|
-
process.exit(0);
|
|
50552
|
-
}
|
|
50553
|
-
username = selected;
|
|
50554
|
-
}
|
|
50624
|
+
const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
|
|
50625
|
+
if (null === resolvedUsername) {
|
|
50626
|
+
logger_logger.warn("Operation cancelled.");
|
|
50627
|
+
process.exit(0);
|
|
50555
50628
|
}
|
|
50629
|
+
username = resolvedUsername;
|
|
50556
50630
|
if (username) logger_logger.verbose(chalk_source.cyan("Using account: ") + chalk_source.bold(username));
|
|
50557
50631
|
if (!hasTokensForUrl(anvilUrl, username)) {
|
|
50558
50632
|
if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
|
|
@@ -51359,6 +51433,13 @@ var __webpack_exports__ = {};
|
|
|
51359
51433
|
});
|
|
51360
51434
|
loginCommand.addHelpText("after", "\n" + chalk_source.bold("Examples:") + "\n anvil login Log in to default Anvil server\n anvil login anvil.works Log in to anvil.works\n anvil login localhost Log in to local Anvil server\n");
|
|
51361
51435
|
}
|
|
51436
|
+
function getTotalLoggedInAccounts(urls) {
|
|
51437
|
+
return urls.reduce((total, url)=>total + auth_getAccountsForUrl(url).length, 0);
|
|
51438
|
+
}
|
|
51439
|
+
function formatMultiAccountLogoutPrompt(totalAccounts, urlCount) {
|
|
51440
|
+
if (urlCount <= 1) return `You have ${totalAccounts} logged-in accounts. What would you like to do?`;
|
|
51441
|
+
return `You have ${totalAccounts} logged-in accounts across ${urlCount} URLs. What would you like to do?`;
|
|
51442
|
+
}
|
|
51362
51443
|
function registerLogoutCommand(program) {
|
|
51363
51444
|
const logoutCommand = program.command("logout [anvil-server-url]").description("Log out from Anvil (optionally specify URL to logout from specific installation)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL to logout from").option("-U, --user <USERNAME>", "Specify which user account to logout from").alias("lo").action(async (anvilUrl, options)=>{
|
|
51364
51445
|
try {
|
|
@@ -51372,8 +51453,9 @@ var __webpack_exports__ = {};
|
|
|
51372
51453
|
else logger_logger.info(result.message || `Not logged in to ${normalized}. No action needed.`);
|
|
51373
51454
|
} else {
|
|
51374
51455
|
const availableUrls = getAvailableAnvilUrls();
|
|
51375
|
-
|
|
51376
|
-
|
|
51456
|
+
const totalAccounts = getTotalLoggedInAccounts(availableUrls);
|
|
51457
|
+
if (0 === totalAccounts) logger_logger.warn("No logged-in accounts found.");
|
|
51458
|
+
else if (1 === totalAccounts) {
|
|
51377
51459
|
const result = await logout();
|
|
51378
51460
|
if (result.loggedOut) logger_logger.success("Logged out.");
|
|
51379
51461
|
} else {
|
|
@@ -51391,7 +51473,7 @@ var __webpack_exports__ = {};
|
|
|
51391
51473
|
value: "cancel"
|
|
51392
51474
|
}
|
|
51393
51475
|
];
|
|
51394
|
-
const action = await logger_logger.select(
|
|
51476
|
+
const action = await logger_logger.select(formatMultiAccountLogoutPrompt(totalAccounts, availableUrls.length), choices, "one");
|
|
51395
51477
|
if ("cancel" === action) return void logger_logger.info("Logout cancelled.");
|
|
51396
51478
|
if ("all" === action) {
|
|
51397
51479
|
const result = await logout();
|
|
@@ -51490,17 +51572,21 @@ var __webpack_exports__ = {};
|
|
|
51490
51572
|
let totalAccounts = 0;
|
|
51491
51573
|
for (const url of urls){
|
|
51492
51574
|
const urlData = authTokens[url];
|
|
51493
|
-
if (urlData && "object" == typeof urlData && !Array.isArray(urlData))
|
|
51575
|
+
if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
|
|
51576
|
+
const usernames = Object.keys(urlData);
|
|
51577
|
+
totalAccounts += usernames.filter((username)=>hasTokensForUrl(url, username)).length;
|
|
51578
|
+
}
|
|
51494
51579
|
}
|
|
51495
|
-
console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} account(s) across ${urls.length} URL(s)`)}`);
|
|
51580
|
+
console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} logged-in account(s) across ${urls.length} URL(s)`)}`);
|
|
51496
51581
|
for (const url of urls){
|
|
51497
51582
|
const urlData = authTokens[url];
|
|
51498
51583
|
if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
|
|
51499
51584
|
const usernames = Object.keys(urlData).sort();
|
|
51500
51585
|
if (usernames.length > 0) for (const username of usernames){
|
|
51501
51586
|
const accountData = urlData[username];
|
|
51502
|
-
const
|
|
51503
|
-
const
|
|
51587
|
+
const hasConfigTokens = !!(accountData?.authToken || accountData?.refreshToken);
|
|
51588
|
+
const hasAnyTokens = hasTokensForUrl(url, username);
|
|
51589
|
+
const status = hasConfigTokens ? chalk_source.green("✓ logged in (config)") : hasAnyTokens ? chalk_source.green("✓ logged in (keychain)") : chalk_source.gray("(no tokens)");
|
|
51504
51590
|
console.log(chalk_source.gray(` ${url}: ${username} ${status}`));
|
|
51505
51591
|
}
|
|
51506
51592
|
}
|