@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.
Files changed (3) hide show
  1. package/README.md +35 -30
  2. package/dist/cli.js +126 -40
  3. 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
- > **Note:** `anvil sync` still works but is deprecated. Use `anvil watch` instead.
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. **User Prompt**: If multiple accounts are available, prompts you to select one
161
- 4. **Global Config**: Falls back to `anvilUrl` in config file
162
- 5. **Default**: Uses `https://anvil.works` (or `http://localhost:3000` in dev mode)
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 accounts, you'll be prompted to select
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
- # When syncing, you'll be prompted to select which one to use
273
- # Or specify explicitly:
274
- anvil watch --url anvil.company-a.com
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. User prompt (if multiple accounts available)
282
- 4. `anvilUrl` in config file
283
- 5. Default based on `NODE_ENV`:
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
- **Multiple accounts:** If you have multiple accounts and run `anvil logout` without specifying a URL, you'll be prompted to:
312
-
313
- - Logout from one account (then select which one)
314
- - Logout from all accounts
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 resolveUrlForFallback(explicitUrl) {
49960
- if (explicitUrl) return normalizeAnvilUrl(explicitUrl);
49961
- const availableUrls = getAvailableAnvilUrls();
49962
- if (availableUrls.length > 0) return availableUrls[0];
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 fromConfig.trim();
49965
- return isDevMode() ? "http://localhost:3000" : "https://anvil.works";
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 fallbackUrl = resolveUrlForFallback(explicitUrl);
50493
- const shouldContinue = await logger_logger.confirm(`Search ${fallbackUrl} for matching app IDs? (slower)`, true);
50494
- if (shouldContinue) {
50495
- logger_logger.progress("detect", `Searching ${fallbackUrl} ${explicitUsername ? `for ${explicitUsername}` : ''} for matching app IDs...`);
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: explicitUsername,
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 = resolveUrlForFallback();
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
- if (!username) {
50535
- const accounts = auth_getAccountsForUrl(anvilUrl);
50536
- if (1 === accounts.length) {
50537
- username = accounts[0];
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
- if (0 === availableUrls.length) logger_logger.warn("No logged-in accounts found.");
51376
- else if (1 === availableUrls.length) {
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(`You have ${availableUrls.length} logged-in accounts. What would you like to do?`, choices, "one");
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)) totalAccounts += Object.keys(urlData).length;
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 hasToken = !!(accountData?.authToken || accountData?.refreshToken);
51503
- const status = hasToken ? chalk_source.green("✓ logged in") : chalk_source.gray("(no tokens)");
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",