@anvil-works/anvil-cli 0.4.2 → 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.
Files changed (4) hide show
  1. package/README.md +208 -145
  2. package/dist/cli.js +847 -141
  3. package/dist/index.js +12 -5
  4. package/package.json +1 -1
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
- if (!this.ws) return;
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
- this.ws.removeAllListeners();
47691
- if (this.ws.readyState === ws_wrapper.OPEN || this.ws.readyState === ws_wrapper.CONNECTING) this.ws.terminate();
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,13 +50002,199 @@ var __webpack_exports__ = {};
49956
50002
  session.hasUncommittedChanges = hasUncommittedChanges;
49957
50003
  return session;
49958
50004
  }
49959
- function resolveUrlForFallback(explicitUrl) {
49960
- if (explicitUrl) return normalizeAnvilUrl(explicitUrl);
49961
- const availableUrls = getAvailableAnvilUrls();
49962
- if (availableUrls.length > 0) return availableUrls[0];
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
+ }
50120
+ function decideFallbackUrl(explicitUrl, availableUrls = getAvailableAnvilUrls()) {
50121
+ if (explicitUrl) return {
50122
+ source: "explicit",
50123
+ url: normalizeAnvilUrl(explicitUrl)
50124
+ };
50125
+ if (availableUrls.length > 1) return {
50126
+ source: "available-multiple",
50127
+ urls: availableUrls
50128
+ };
50129
+ if (1 === availableUrls.length) return {
50130
+ source: "available-single",
50131
+ url: availableUrls[0]
50132
+ };
49963
50133
  const fromConfig = getConfig("anvilUrl");
49964
- if ("string" == typeof fromConfig && fromConfig.trim()) return fromConfig.trim();
49965
- return isDevMode() ? "http://localhost:3000" : "https://anvil.works";
50134
+ if ("string" == typeof fromConfig && fromConfig.trim()) return {
50135
+ source: "config",
50136
+ url: normalizeAnvilUrl(fromConfig.trim())
50137
+ };
50138
+ return {
50139
+ source: "default",
50140
+ url: isDevMode() ? "http://localhost:3000" : "https://anvil.works"
50141
+ };
50142
+ }
50143
+ function decideUsernameForUrl(accounts) {
50144
+ if (0 === accounts.length) return {
50145
+ source: "none"
50146
+ };
50147
+ if (1 === accounts.length) return {
50148
+ source: "single",
50149
+ username: accounts[0]
50150
+ };
50151
+ return {
50152
+ source: "multiple",
50153
+ usernames: [
50154
+ ...accounts
50155
+ ]
50156
+ };
50157
+ }
50158
+ async function resolveUsernameForUrl(anvilUrl, explicitUsername, promptMessage = "Multiple accounts found. Which account owns this app?") {
50159
+ if (explicitUsername) return explicitUsername;
50160
+ const decision = decideUsernameForUrl(auth_getAccountsForUrl(anvilUrl));
50161
+ if ("none" === decision.source) return;
50162
+ if ("single" === decision.source) {
50163
+ logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(decision.username));
50164
+ return decision.username;
50165
+ }
50166
+ const choices = decision.usernames.map((acct)=>({
50167
+ name: acct,
50168
+ value: acct
50169
+ }));
50170
+ choices.push({
50171
+ name: "Cancel",
50172
+ value: null
50173
+ });
50174
+ return logger_logger.select(promptMessage, choices, decision.usernames[0]);
50175
+ }
50176
+ async function confirmReverseLookupWithResolvedUser(anvilUrl, username) {
50177
+ const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username, `Multiple accounts found for ${anvilUrl}. Which account should be used for app lookup?`);
50178
+ if (null === resolvedUsername) return null;
50179
+ const shouldContinue = await logger_logger.confirm(`Search ${anvilUrl} ${resolvedUsername ? `for ${resolvedUsername}` : ""} for matching app IDs? (slower)`, true);
50180
+ return {
50181
+ username: resolvedUsername,
50182
+ shouldContinue
50183
+ };
50184
+ }
50185
+ async function resolveUrlForFallback(explicitUrl) {
50186
+ const decision = decideFallbackUrl(explicitUrl);
50187
+ if ("available-multiple" !== decision.source) return decision.url;
50188
+ const choices = decision.urls.map((url)=>({
50189
+ name: url,
50190
+ value: url
50191
+ }));
50192
+ choices.push({
50193
+ name: "Cancel",
50194
+ value: null
50195
+ });
50196
+ const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
50197
+ return selectedUrl;
49966
50198
  }
49967
50199
  async function selectAppId(candidates) {
49968
50200
  if (0 === candidates.length) {
@@ -50474,14 +50706,25 @@ var __webpack_exports__ = {};
50474
50706
  let finalAppId;
50475
50707
  let anvilUrl;
50476
50708
  let username = explicitUsername;
50709
+ let fallbackUrl;
50710
+ let shouldPersistUsernameBinding = false;
50477
50711
  if (explicitAppId) {
50478
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
+ }
50479
50722
  const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
50480
- if (remoteInfo.detectedUrl && !explicitUrl) {
50723
+ if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
50481
50724
  anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
50482
50725
  logger_logger.verbose(chalk_source.cyan("Resolved URL from remote for app ID: ") + chalk_source.bold(anvilUrl));
50483
50726
  }
50484
- if (remoteInfo.detectedUsername && !explicitUsername) {
50727
+ if (remoteInfo.detectedUsername && !explicitUsername && !username) {
50485
50728
  username = remoteInfo.detectedUsername;
50486
50729
  logger_logger.verbose(chalk_source.cyan("Resolved username from remote for app ID: ") + chalk_source.bold(username));
50487
50730
  }
@@ -50489,13 +50732,23 @@ var __webpack_exports__ = {};
50489
50732
  logger_logger.verbose(chalk_source.cyan("No app ID provided, attempting auto-detection..."));
50490
50733
  if (0 === filteredCandidates.length) {
50491
50734
  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...`);
50735
+ const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
50736
+ if (null === resolvedFallbackUrl) {
50737
+ logger_logger.warn("Operation cancelled.");
50738
+ process.exit(0);
50739
+ }
50740
+ fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
50741
+ const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
50742
+ if (null === lookupDecision) {
50743
+ logger_logger.warn("Operation cancelled.");
50744
+ process.exit(0);
50745
+ }
50746
+ username = lookupDecision.username;
50747
+ if (lookupDecision.shouldContinue) {
50748
+ logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
50496
50749
  const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
50497
50750
  anvilUrl: fallbackUrl,
50498
- username: explicitUsername,
50751
+ username,
50499
50752
  includeRemotes: false
50500
50753
  });
50501
50754
  logger_logger.progressEnd("detect");
@@ -50526,32 +50779,41 @@ var __webpack_exports__ = {};
50526
50779
  if (selected.detectedUsername && !username) username = selected.detectedUsername;
50527
50780
  }
50528
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;
50529
50792
  if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
50530
- else if (!anvilUrl) anvilUrl = resolveUrlForFallback();
50793
+ else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
50794
+ else {
50795
+ const resolvedFallbackUrl = await resolveUrlForFallback();
50796
+ if (null === resolvedFallbackUrl) {
50797
+ logger_logger.warn("Operation cancelled.");
50798
+ process.exit(0);
50799
+ }
50800
+ anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
50801
+ }
50531
50802
  anvilUrl = normalizeAnvilUrl(anvilUrl);
50532
50803
  logger_logger.verbose(chalk_source.green("Using app ID: ") + chalk_source.bold(finalAppId));
50533
50804
  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
- }
50805
+ const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
50806
+ if (null === resolvedUsername) {
50807
+ logger_logger.warn("Operation cancelled.");
50808
+ process.exit(0);
50809
+ }
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."));
50555
50817
  }
50556
50818
  if (username) logger_logger.verbose(chalk_source.cyan("Using account: ") + chalk_source.bold(username));
50557
50819
  if (!hasTokensForUrl(anvilUrl, username)) {
@@ -50611,7 +50873,6 @@ var __webpack_exports__ = {};
50611
50873
  });
50612
50874
  return watchCommand;
50613
50875
  }
50614
- var external_http_ = __webpack_require__("http");
50615
50876
  const external_node_url_namespaceObject = require("node:url");
50616
50877
  var external_node_child_process_ = __webpack_require__("node:child_process");
50617
50878
  let isDockerCached;
@@ -51074,6 +51335,7 @@ var __webpack_exports__ = {};
51074
51335
  defineLazyProperty(open_apps, 'browser', ()=>'browser');
51075
51336
  defineLazyProperty(open_apps, 'browserPrivate', ()=>'browserPrivate');
51076
51337
  const node_modules_open = open_open;
51338
+ var external_http_ = __webpack_require__("http");
51077
51339
  const successPage = `<!DOCTYPE html>
51078
51340
  <html lang="en">
51079
51341
  <head>
@@ -51231,6 +51493,9 @@ var __webpack_exports__ = {};
51231
51493
  </body>
51232
51494
  </html>`;
51233
51495
  }
51496
+ function oauth_login_base64url(buf) {
51497
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
51498
+ }
51234
51499
  async function waitForOAuthWithPrompt(authUrl, oauthPromise) {
51235
51500
  const rl = external_readline_namespaceObject.createInterface({
51236
51501
  input: process.stdin,
@@ -51255,8 +51520,382 @@ var __webpack_exports__ = {};
51255
51520
  process.stdout.write("\r" + " ".repeat(60) + "\r");
51256
51521
  return result.oauth;
51257
51522
  }
51258
- function login_base64url(buf) {
51259
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
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
+ });
51260
51899
  }
51261
51900
  function registerLoginCommand(program) {
51262
51901
  const loginCommand = program.command("login [anvil-server-url]").description("Log in to Anvil using OAuth").alias("l").action(async (anvilUrl)=>{
@@ -51265,93 +51904,8 @@ var __webpack_exports__ = {};
51265
51904
  anvilUrl = normalizeAnvilUrl(anvilUrl.trim());
51266
51905
  setConfig("anvilUrl", anvilUrl);
51267
51906
  } else anvilUrl = resolveAnvilUrl();
51268
- const codeVerifier = external_crypto_.randomBytes(48).toString("hex");
51269
- const codeChallenge = login_base64url(external_crypto_.createHash("sha256").update(codeVerifier, "ascii").digest());
51270
- const state = external_crypto_.randomBytes(16).toString("hex");
51271
- const server = external_http_.createServer();
51272
- await new Promise((resolve)=>server.listen(0, "127.0.0.1", resolve));
51273
- const address = server.address();
51274
- if (!address || "string" == typeof address) throw new Error("No address");
51275
- const port = address.port;
51276
- const redirectUri = `http://127.0.0.1:${port}/oauth-callback`;
51277
- const SCOPES = "apps:read apps:write user:read";
51278
- const authUrl = new URL(`${anvilUrl}/oauth/authorize`);
51279
- authUrl.searchParams.set("response_type", "code");
51280
- authUrl.searchParams.set("client_id", "anvil-sync");
51281
- authUrl.searchParams.set("redirect_uri", redirectUri);
51282
- authUrl.searchParams.set("scope", SCOPES);
51283
- authUrl.searchParams.set("state", state);
51284
- authUrl.searchParams.set("code_challenge", codeChallenge);
51285
- authUrl.searchParams.set("code_challenge_method", "S256");
51286
- const codePromise = new Promise((resolve, reject)=>{
51287
- server.on("request", (req, res)=>{
51288
- if (!req.url) return;
51289
- const url = new URL(req.url, `http://127.0.0.1:${port}`);
51290
- if ("/oauth-callback" !== url.pathname) return;
51291
- const code = url.searchParams.get("code") || void 0;
51292
- const error = url.searchParams.get("error") || void 0;
51293
- const recvState = url.searchParams.get("state");
51294
- if (!recvState || !code && !error) {
51295
- res.statusCode = 400;
51296
- res.end("Missing code, error or state");
51297
- reject(new Error("Missing code/state/error"));
51298
- server.close();
51299
- return;
51300
- }
51301
- res.statusCode = 200;
51302
- res.setHeader("Content-Type", "text/html; charset=utf-8");
51303
- if (code) res.end(successPage);
51304
- else res.end(errorPage(error || "unknown"));
51305
- resolve({
51306
- code,
51307
- error,
51308
- recvState
51309
- });
51310
- server.closeAllConnections();
51311
- server.close();
51312
- });
51313
- });
51314
- logger_logger.info(chalk_source.blue("Link to your Anvil account to continue."));
51315
- console.log();
51316
- logger_logger.info(chalk_source.gray(` ${authUrl}`));
51317
- console.log();
51318
- const { code, error, recvState } = await waitForOAuthWithPrompt(authUrl, codePromise);
51319
- if (recvState !== state) {
51320
- logger_logger.error("Invalid state received from OAuth callback");
51321
- process.exit(1);
51322
- }
51323
- if (error) {
51324
- logger_logger.error(`Error received from OAuth callback: ${error}`);
51325
- process.exit(1);
51326
- }
51327
- const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
51328
- method: "POST",
51329
- headers: {
51330
- "Content-Type": "application/x-www-form-urlencoded"
51331
- },
51332
- body: new URLSearchParams({
51333
- grant_type: "authorization_code",
51334
- code: code,
51335
- redirect_uri: redirectUri,
51336
- client_id: "anvil-sync",
51337
- code_verifier: codeVerifier
51338
- })
51339
- });
51340
- if (tokenResponse.ok) {
51341
- const tokenData = await tokenResponse.json();
51342
- const result = await login(anvilUrl, {
51343
- access_token: tokenData.access_token,
51344
- refresh_token: tokenData.refresh_token,
51345
- expires_in: tokenData.expires_in,
51346
- scope: tokenData.scope
51347
- });
51348
- logger_logger.success("Logged in as " + chalk_source.bold(result.email));
51349
- } else {
51350
- logger_logger.error("Failed to exchange authorization code for token.");
51351
- const errorText = await tokenResponse.text();
51352
- logger_logger.error(` Server response: ${errorText}`);
51353
- process.exit(1);
51354
- }
51907
+ const result = await runInteractiveLoginFlow(anvilUrl);
51908
+ logger_logger.success("Logged in as " + chalk_source.bold(result.email));
51355
51909
  } catch (e) {
51356
51910
  logger_logger.error("Error: " + e.message);
51357
51911
  process.exit(1);
@@ -51359,6 +51913,13 @@ var __webpack_exports__ = {};
51359
51913
  });
51360
51914
  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
51915
  }
51916
+ function getTotalLoggedInAccounts(urls) {
51917
+ return urls.reduce((total, url)=>total + auth_getAccountsForUrl(url).length, 0);
51918
+ }
51919
+ function formatMultiAccountLogoutPrompt(totalAccounts, urlCount) {
51920
+ if (urlCount <= 1) return `You have ${totalAccounts} logged-in accounts. What would you like to do?`;
51921
+ return `You have ${totalAccounts} logged-in accounts across ${urlCount} URLs. What would you like to do?`;
51922
+ }
51362
51923
  function registerLogoutCommand(program) {
51363
51924
  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
51925
  try {
@@ -51372,8 +51933,9 @@ var __webpack_exports__ = {};
51372
51933
  else logger_logger.info(result.message || `Not logged in to ${normalized}. No action needed.`);
51373
51934
  } else {
51374
51935
  const availableUrls = getAvailableAnvilUrls();
51375
- if (0 === availableUrls.length) logger_logger.warn("No logged-in accounts found.");
51376
- else if (1 === availableUrls.length) {
51936
+ const totalAccounts = getTotalLoggedInAccounts(availableUrls);
51937
+ if (0 === totalAccounts) logger_logger.warn("No logged-in accounts found.");
51938
+ else if (1 === totalAccounts) {
51377
51939
  const result = await logout();
51378
51940
  if (result.loggedOut) logger_logger.success("Logged out.");
51379
51941
  } else {
@@ -51391,7 +51953,7 @@ var __webpack_exports__ = {};
51391
51953
  value: "cancel"
51392
51954
  }
51393
51955
  ];
51394
- const action = await logger_logger.select(`You have ${availableUrls.length} logged-in accounts. What would you like to do?`, choices, "one");
51956
+ const action = await logger_logger.select(formatMultiAccountLogoutPrompt(totalAccounts, availableUrls.length), choices, "one");
51395
51957
  if ("cancel" === action) return void logger_logger.info("Logout cancelled.");
51396
51958
  if ("all" === action) {
51397
51959
  const result = await logout();
@@ -51447,6 +52009,14 @@ var __webpack_exports__ = {};
51447
52009
  });
51448
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");
51449
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
+ }
51450
52020
  function registerConfigCommand(program) {
51451
52021
  const configCommand = program.command("config").description("Manage configuration").alias("c");
51452
52022
  configCommand.command("get <key>").description("Get a configuration value").action(async (key)=>{
@@ -51465,8 +52035,7 @@ var __webpack_exports__ = {};
51465
52035
  });
51466
52036
  configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value)=>{
51467
52037
  try {
51468
- let typedValue = coerceConfigValue(key, value);
51469
- if ("anvilUrl" === key && "string" == typeof typedValue) typedValue = typedValue.trim().replace(/\/+$/, "");
52038
+ const typedValue = parseConfigSetValue(key, value);
51470
52039
  setConfig(key, typedValue);
51471
52040
  logger_logger.success(`Set ${key} = ${typedValue}`);
51472
52041
  } catch (error) {
@@ -51490,17 +52059,21 @@ var __webpack_exports__ = {};
51490
52059
  let totalAccounts = 0;
51491
52060
  for (const url of urls){
51492
52061
  const urlData = authTokens[url];
51493
- if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) totalAccounts += Object.keys(urlData).length;
52062
+ if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
52063
+ const usernames = Object.keys(urlData);
52064
+ totalAccounts += usernames.filter((username)=>hasTokensForUrl(url, username)).length;
52065
+ }
51494
52066
  }
51495
- console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} account(s) across ${urls.length} URL(s)`)}`);
52067
+ console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} logged-in account(s) across ${urls.length} URL(s)`)}`);
51496
52068
  for (const url of urls){
51497
52069
  const urlData = authTokens[url];
51498
52070
  if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
51499
52071
  const usernames = Object.keys(urlData).sort();
51500
52072
  if (usernames.length > 0) for (const username of usernames){
51501
52073
  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)");
52074
+ const hasConfigTokens = !!(accountData?.authToken || accountData?.refreshToken);
52075
+ const hasAnyTokens = hasTokensForUrl(url, username);
52076
+ const status = hasConfigTokens ? chalk_source.green("✓ logged in (config)") : hasAnyTokens ? chalk_source.green("✓ logged in (keychain)") : chalk_source.gray("(no tokens)");
51504
52077
  console.log(chalk_source.gray(` ${url}: ${username} ${status}`));
51505
52078
  }
51506
52079
  }
@@ -51522,7 +52095,7 @@ var __webpack_exports__ = {};
51522
52095
  });
51523
52096
  configCommand.command("reset").description("Reset configuration to defaults").action(async ()=>{
51524
52097
  try {
51525
- resetConfig();
52098
+ await performConfigReset();
51526
52099
  logger_logger.success("Configuration reset to defaults.");
51527
52100
  } catch (e) {
51528
52101
  logger_logger.error("Error: " + e.message);
@@ -51538,6 +52111,136 @@ var __webpack_exports__ = {};
51538
52111
  console.log();
51539
52112
  });
51540
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
+ }
51541
52244
  const packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
51542
52245
  const VERSION = packageJson.version;
51543
52246
  setLogger(new CLILogger({
@@ -51601,14 +52304,17 @@ var __webpack_exports__ = {};
51601
52304
  });
51602
52305
  if (!opts.json) {
51603
52306
  const commandName = actionCommand?.name();
51604
- if ("update" !== commandName) checkVersionAndWarn();
52307
+ if ("update" !== commandName && "git-credential" !== commandName) checkVersionAndWarn();
51605
52308
  }
51606
52309
  });
51607
52310
  const cli_watchCommand = registerWatchCommand(cli_program);
52311
+ registerCheckoutCommand(cli_program);
52312
+ registerGitCredentialCommand(cli_program);
51608
52313
  registerLoginCommand(cli_program);
51609
52314
  registerLogoutCommand(cli_program);
51610
52315
  registerConfigCommand(cli_program);
51611
52316
  registerVersionCommand(cli_program, VERSION);
52317
+ registerOnboardCommand(cli_program, VERSION);
51612
52318
  cli_program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
51613
52319
  await handleUpdateCommand();
51614
52320
  });