@anvil-works/anvil-cli 0.8.0-dev.0 → 0.8.0-dev.1

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 (47) hide show
  1. package/README.md +54 -0
  2. package/dist/WatchSession.d.ts.map +1 -1
  3. package/dist/api.d.ts +3 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/cli.js +1251 -216
  6. package/dist/commands/appDetection.d.ts +68 -1
  7. package/dist/commands/appDetection.d.ts.map +1 -1
  8. package/dist/commands/checkout.d.ts +1 -0
  9. package/dist/commands/checkout.d.ts.map +1 -1
  10. package/dist/commands/create.d.ts +52 -0
  11. package/dist/commands/create.d.ts.map +1 -0
  12. package/dist/commands/db.d.ts.map +1 -1
  13. package/dist/commands/deps.d.ts.map +1 -1
  14. package/dist/commands/env.d.ts +3 -1
  15. package/dist/commands/env.d.ts.map +1 -1
  16. package/dist/commands/index.d.ts +3 -0
  17. package/dist/commands/index.d.ts.map +1 -1
  18. package/dist/commands/repl.d.ts.map +1 -1
  19. package/dist/commands/secret.d.ts +3 -0
  20. package/dist/commands/secret.d.ts.map +1 -0
  21. package/dist/commands/test.d.ts +26 -0
  22. package/dist/commands/test.d.ts.map +1 -0
  23. package/dist/commands/watch.d.ts +1 -25
  24. package/dist/commands/watch.d.ts.map +1 -1
  25. package/dist/config.d.ts +3 -1
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/formTemplateValidation.d.ts.map +1 -1
  28. package/dist/index.js +225 -22
  29. package/dist/program.d.ts.map +1 -1
  30. package/dist/services/anvil-api.d.ts +20 -0
  31. package/dist/services/anvil-api.d.ts.map +1 -1
  32. package/dist/services/db.d.ts +2 -0
  33. package/dist/services/db.d.ts.map +1 -1
  34. package/dist/services/deps.d.ts +5 -2
  35. package/dist/services/deps.d.ts.map +1 -1
  36. package/dist/services/environment.d.ts +4 -0
  37. package/dist/services/environment.d.ts.map +1 -1
  38. package/dist/services/path-open.d.ts +1 -0
  39. package/dist/services/path-open.d.ts.map +1 -1
  40. package/dist/services/repl.d.ts +2 -0
  41. package/dist/services/repl.d.ts.map +1 -1
  42. package/dist/services/secret.d.ts +43 -0
  43. package/dist/services/secret.d.ts.map +1 -0
  44. package/dist/services/server-tests.d.ts +47 -0
  45. package/dist/services/server-tests.d.ts.map +1 -0
  46. package/dist/validateFormTemplateHtml.d.ts.map +1 -1
  47. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -12875,12 +12875,25 @@ var __webpack_exports__ = {};
12875
12875
  if (!urlInput || "string" != typeof urlInput) return urlInput;
12876
12876
  let url = urlInput.trim();
12877
12877
  url = url.replace(/\/+$/, "");
12878
- if ("localhost" === url || url.startsWith("localhost:")) return "http://localhost:3000";
12879
- if ("http://localhost" === url || url.startsWith("http://localhost:")) return "http://localhost:3000";
12880
- if ("https://localhost" === url || url.startsWith("https://localhost:")) return "http://localhost:3000";
12881
- if (/^https?:\/\//.test(url)) return url;
12878
+ if ("localhost" === url) return "http://localhost:3000";
12879
+ if (url.startsWith("localhost:")) return `http://${url}`;
12880
+ if (/^https?:\/\//i.test(url)) return url;
12882
12881
  return `https://${url}`;
12883
12882
  }
12883
+ function normalizeAnvilOrigin(input) {
12884
+ try {
12885
+ const raw = input.trim();
12886
+ const normalized = normalizeAnvilUrl(raw);
12887
+ const url = new URL(raw.includes("://") ? raw : normalized);
12888
+ if (![
12889
+ "http:",
12890
+ "https:"
12891
+ ].includes(url.protocol) || url.username || url.password || "/" !== url.pathname || url.search || url.hash) throw new Error("Invalid origin");
12892
+ return normalized;
12893
+ } catch {
12894
+ throw new Error("Server URL must be an HTTP(S) origin without credentials, a path, query, or fragment.");
12895
+ }
12896
+ }
12884
12897
  function resolveAnvilUrl() {
12885
12898
  const fromConfig = getConfig("anvilUrl");
12886
12899
  if ("string" == typeof fromConfig && fromConfig.trim()) return fromConfig.trim();
@@ -12904,6 +12917,9 @@ var __webpack_exports__ = {};
12904
12917
  function getGlobalOutputConfig() {
12905
12918
  return globalOutputConfig;
12906
12919
  }
12920
+ function canPrompt() {
12921
+ return !globalOutputConfig.jsonMode && !!(process.stdin.isTTY && process.stdout.isTTY);
12922
+ }
12907
12923
  function assertCanPrompt(action = "This command", explicitAlternative = "Pass explicit flags instead.") {
12908
12924
  if (globalOutputConfig.jsonMode) throw new Error(`${action} requires interactive input, but --json disables prompts. ${explicitAlternative}`);
12909
12925
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error(`${action} requires interactive input, but stdin/stdout is not a TTY. ${explicitAlternative}`);
@@ -13214,8 +13230,6 @@ var __webpack_exports__ = {};
13214
13230
  return answer.value;
13215
13231
  }
13216
13232
  }
13217
- const external_readline_namespaceObject = require("readline");
13218
- var external_readline_default = /*#__PURE__*/ __webpack_require__.n(external_readline_namespaceObject);
13219
13233
  const external_js_yaml_namespaceObject = require("js-yaml");
13220
13234
  var external_js_yaml_default = /*#__PURE__*/ __webpack_require__.n(external_js_yaml_namespaceObject);
13221
13235
  class EditorYamlManager {
@@ -14748,9 +14762,50 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14748
14762
  function parseBranchList(output) {
14749
14763
  return output.split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
14750
14764
  }
14765
+ function formatHttpError(prefix, status, body) {
14766
+ if (!body) return `${prefix}: ${status}`;
14767
+ try {
14768
+ const parsed = JSON.parse(body);
14769
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14770
+ } catch {}
14771
+ return `${prefix}: ${status} ${body}`;
14772
+ }
14751
14773
  function anvil_api_getDefaultAnvilUrl() {
14752
14774
  return resolveAnvilUrl();
14753
14775
  }
14776
+ async function createAnvilApp(options) {
14777
+ let response;
14778
+ try {
14779
+ response = await fetch(`${options.anvilUrl}/ide/api/_/apps`, {
14780
+ method: "POST",
14781
+ headers: {
14782
+ Authorization: `Bearer ${options.authToken}`,
14783
+ "Content-Type": "application/json"
14784
+ },
14785
+ body: JSON.stringify({
14786
+ name: options.name,
14787
+ tags: options.tags
14788
+ })
14789
+ });
14790
+ } catch (error) {
14791
+ throw errors_createNetworkError.network(error.message);
14792
+ }
14793
+ const data = await response.json().catch(()=>({}));
14794
+ if (!response.ok) {
14795
+ if (401 === response.status || 403 === response.status) throw errors_createAuthError.invalid(data.error || "Not authorized to create an app");
14796
+ throw new Error(data.error || `Failed to create app: ${response.status} ${response.statusText}`);
14797
+ }
14798
+ if ("string" != typeof data.id || !data.id) throw new Error("App creation response did not contain an app ID.");
14799
+ return {
14800
+ id: data.id,
14801
+ ..."string" == typeof data.name ? {
14802
+ name: data.name
14803
+ } : {},
14804
+ ...data.tags ? {
14805
+ tags: data.tags
14806
+ } : {}
14807
+ };
14808
+ }
14754
14809
  async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
14755
14810
  const authToken = await auth_getValidAuthToken(anvilUrl, username);
14756
14811
  try {
@@ -14836,6 +14891,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14836
14891
  return null;
14837
14892
  }
14838
14893
  }
14894
+ async function getTestRunSnapshot(appId, runId, options = {}) {
14895
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
14896
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
14897
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/test-runs/${encodeURIComponent(runId)}`, {
14898
+ headers: {
14899
+ Authorization: `Bearer ${token}`
14900
+ }
14901
+ });
14902
+ if (!response.ok) throw new Error(formatHttpError("Failed to get the test run's results", response.status, await response.text()));
14903
+ return response.json();
14904
+ }
14839
14905
  function filterCandidates(candidates, explicitUrl, explicitUsername) {
14840
14906
  let filtered = candidates;
14841
14907
  if (explicitUrl) {
@@ -14873,14 +14939,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14873
14939
  try {
14874
14940
  const remotes = await git.getRemotes(true);
14875
14941
  for (const remote of remotes){
14876
- const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
14942
+ const httpMatch = remote.refs.fetch?.match(/(https?):\/\/(?:[^@\/]+@)?([^\/]+)\/git\/([A-Z0-9]+)\.git/);
14877
14943
  if (httpMatch) {
14878
- const [, host, detectedAppId] = httpMatch;
14944
+ const [, scheme, host, detectedAppId] = httpMatch;
14945
+ const origin = new URL(`${scheme}://${host}`).origin;
14879
14946
  out.push({
14880
14947
  appId: detectedAppId,
14881
14948
  source: "remote",
14882
14949
  description: `Git remote '${remote.name}'`,
14883
- detectedUrl: normalizeAnvilUrl(host)
14950
+ detectedUrl: normalizeAnvilUrl(origin)
14884
14951
  });
14885
14952
  continue;
14886
14953
  }
@@ -14988,14 +15055,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14988
15055
  html: htmlContent
14989
15056
  };
14990
15057
  }
14991
- function formatHttpError(prefix, status, body) {
14992
- if (!body) return `${prefix}: ${status}`;
14993
- try {
14994
- const parsed = JSON.parse(body);
14995
- if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14996
- } catch {}
14997
- return `${prefix}: ${status} ${body}`;
14998
- }
14999
15058
  const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
15000
15059
  async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
15001
15060
  const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
@@ -15013,12 +15072,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15013
15072
  }
15014
15073
  const ANVIL_DEPS_DIR = ".anvil/deps";
15015
15074
  const ANVIL_DEPS_MANIFEST = "manifest.json";
15016
- const DEPS_MANIFEST_VERSION = 2;
15075
+ const DEPS_MANIFEST_VERSION = 3;
15017
15076
  const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
15018
15077
  async function fetchAllDependencies(appId, anvilUrl, options = {}) {
15019
15078
  const { commit, includeDocs = true, knownDependencies = [] } = options;
15020
15079
  anvilUrl ??= await resolveAuthAnvilUrl();
15021
- const token = await auth_getValidAuthToken(anvilUrl);
15080
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
15022
15081
  const body = {
15023
15082
  include_docs: includeDocs,
15024
15083
  known_dependencies: knownDependencies
@@ -15042,9 +15101,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15042
15101
  auth_setRepoContext(projectRoot);
15043
15102
  anvilUrl ??= await resolveAuthAnvilUrl();
15044
15103
  const includeDocs = options.includeDocs ?? true;
15104
+ const git = new GitService(projectRoot);
15105
+ const commit = options.commit ?? await git.getCommitId();
15106
+ const sourceYaml = await git.show(`${commit}:anvil.yaml`).catch((error)=>{
15107
+ if (options.commit) return null;
15108
+ throw error;
15109
+ });
15110
+ const sourceDependencies = null === sourceYaml ? null : normalizeAnvilYamlDependenciesFromContent(sourceYaml);
15045
15111
  const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
15046
15112
  const response = await fetchAllDependencies(appId, anvilUrl, {
15047
- commit: options.commit,
15113
+ username: options.username,
15114
+ commit,
15048
15115
  includeDocs,
15049
15116
  knownDependencies
15050
15117
  });
@@ -15057,7 +15124,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15057
15124
  }
15058
15125
  const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
15059
15126
  await writeAllDepsToCache(projectRoot, response, {
15060
- includeDocs
15127
+ includeDocs,
15128
+ sourceDependencies
15061
15129
  });
15062
15130
  return {
15063
15131
  response,
@@ -15083,7 +15151,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15083
15151
  function isDepsCacheManifest(value) {
15084
15152
  if (!value || "object" != typeof value) return false;
15085
15153
  const manifest = value;
15086
- return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && Array.isArray(manifest.source_dependencies) && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
15154
+ return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && (null === manifest.source_dependencies || Array.isArray(manifest.source_dependencies)) && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
15087
15155
  }
15088
15156
  async function readDepsCacheManifest(projectRoot) {
15089
15157
  try {
@@ -15295,7 +15363,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15295
15363
  const newManifest = {
15296
15364
  version: DEPS_MANIFEST_VERSION,
15297
15365
  includeDocs,
15298
- source_dependencies: await readAnvilYamlDependencies(projectRoot),
15366
+ source_dependencies: void 0 === options.sourceDependencies ? await readAnvilYamlDependencies(projectRoot) : options.sourceDependencies,
15299
15367
  resolved_dependencies: response.resolved_dependencies,
15300
15368
  cached_dependencies: cachedDependencies
15301
15369
  };
@@ -16382,8 +16450,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16382
16450
  }
16383
16451
  if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
16384
16452
  if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
16453
+ if (void 0 !== value.help_text && "string" != typeof value.help_text) formTemplateValidation_pushIssue(issues, path("help_text"), "must be a string");
16385
16454
  if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
16386
- if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16455
+ if (void 0 !== value.item_type.table_name) validateNonEmptyString(value.item_type.table_name, path("item_type.table_name"), issues);
16456
+ else if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16387
16457
  } else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
16388
16458
  return issues;
16389
16459
  }
@@ -16456,7 +16526,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16456
16526
  "toolbox_item",
16457
16527
  "layout_metadata",
16458
16528
  "item_type",
16459
- "slots"
16529
+ "slots",
16530
+ "help_text"
16460
16531
  ]);
16461
16532
  const FRONTMATTER_DISALLOWED_KEYS = new Set([
16462
16533
  "container",
@@ -19737,6 +19808,7 @@ print(json.dumps({"issues": issues}))
19737
19808
  try {
19738
19809
  const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
19739
19810
  const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
19811
+ username: this.username,
19740
19812
  includeDocs: true
19741
19813
  });
19742
19814
  if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
@@ -20245,6 +20317,8 @@ print(json.dumps({"issues": issues}))
20245
20317
  }
20246
20318
  }
20247
20319
  const external_http_namespaceObject = require("http");
20320
+ const external_readline_namespaceObject = require("readline");
20321
+ var external_readline_default = /*#__PURE__*/ __webpack_require__.n(external_readline_namespaceObject);
20248
20322
  const external_open_namespaceObject = require("open");
20249
20323
  var external_open_default = /*#__PURE__*/ __webpack_require__.n(external_open_namespaceObject);
20250
20324
  const successPage = `<!DOCTYPE html>
@@ -21276,7 +21350,7 @@ print(json.dumps({"issues": issues}))
21276
21350
  value: null
21277
21351
  });
21278
21352
  return logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0], {
21279
- action: "`anvil checkout` URL selection",
21353
+ action: "Anvil server selection",
21280
21354
  nonInteractiveHint: `Use --url <ANVIL_URL>. Available URLs: ${decision.urls.join(", ")}.`
21281
21355
  });
21282
21356
  }
@@ -21355,8 +21429,8 @@ print(json.dumps({"issues": issues}))
21355
21429
  name: "Cancel",
21356
21430
  value: null
21357
21431
  });
21358
- return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used for checkout?`, choices, accounts[0], {
21359
- action: "`anvil checkout` account selection",
21432
+ return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used?`, choices, accounts[0], {
21433
+ action: "Anvil account selection",
21360
21434
  nonInteractiveHint: `Use --user <USERNAME>. Available accounts: ${accounts.join(", ")}.`
21361
21435
  });
21362
21436
  }
@@ -21375,7 +21449,7 @@ print(json.dumps({"issues": issues}))
21375
21449
  logger_logger.verbose(external_chalk_default().cyan("Resolved app ID: ") + external_chalk_default().bold(parsed.appId));
21376
21450
  const anvilUrl = await resolveCheckoutUrlForApi(options.anvilUrl, parsed.detectedUrl);
21377
21451
  if (!anvilUrl) throw new Error("Anvil URL is required. Use --url <ANVIL_URL> or pass an input URL containing the host.");
21378
- const gitUrl = options.gitUrl ?? anvilUrl;
21452
+ const gitUrl = void 0 === options.gitUrl ? anvilUrl : normalizeAnvilOrigin(options.gitUrl);
21379
21453
  const resolvedUsername = await resolveCheckoutUsernameForApi(anvilUrl, options.user);
21380
21454
  if (resolvedUsername) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(resolvedUsername));
21381
21455
  else logger_logger.verbose(external_chalk_default().cyan("No account preselected; resolving after auth token lookup."));
@@ -21486,7 +21560,7 @@ print(json.dumps({"issues": issues}))
21486
21560
  await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
21487
21561
  }
21488
21562
  function registerCheckoutCommand(program) {
21489
- const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").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("--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").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
21563
+ const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").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("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("--git-url <GIT_URL>", "Git server URL, if separate from the Anvil server").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
21490
21564
  try {
21491
21565
  const globalOptions = command?.optsWithGlobals() || options || {};
21492
21566
  if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
@@ -21501,6 +21575,7 @@ print(json.dumps({"issues": issues}))
21501
21575
  quiet: options?.quiet,
21502
21576
  verbose: globalOptions.verbose,
21503
21577
  anvilUrl: options?.url,
21578
+ gitUrl: options?.gitUrl,
21504
21579
  user: options?.user,
21505
21580
  force: options?.force,
21506
21581
  query: options?.query
@@ -21512,6 +21587,232 @@ print(json.dumps({"issues": issues}))
21512
21587
  });
21513
21588
  checkoutCommand.addHelpText("after", "\n" + external_chalk_default().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 anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
21514
21589
  }
21590
+ class CreateCheckoutError extends Error {
21591
+ created;
21592
+ constructor(message, created, options){
21593
+ super(message, options), this.created = created;
21594
+ this.name = "CreateCheckoutError";
21595
+ }
21596
+ }
21597
+ const create_defaultDeps = {
21598
+ getAccountsForUrl: auth_getAccountsForUrl,
21599
+ getValidAuthToken: auth_getValidAuthToken,
21600
+ createApp: createAnvilApp,
21601
+ checkout: executeCheckout
21602
+ };
21603
+ function validateCheckoutOptions(options) {
21604
+ if (false === options.checkout && (void 0 !== options.directory || void 0 !== options.gitUrl || options.open)) throw new Error("--no-checkout cannot be combined with a destination argument, --directory, --git-url, or --open.");
21605
+ }
21606
+ async function validateCreateInput(options) {
21607
+ validateCheckoutOptions(options);
21608
+ const name = options.name.trim();
21609
+ if (!name) throw new Error("App name is required.");
21610
+ const tags = void 0 === options.tags ? void 0 : [
21611
+ ...new Set(options.tags.map((tag)=>tag.trim().toLowerCase()))
21612
+ ];
21613
+ if (tags && (tags.length > 20 || tags.some((tag)=>tag.length > 50 || !/^[a-z0-9_-]+$/.test(tag)))) throw new Error("Use at most 20 unique tags, each 1–50 characters: letters, numbers, hyphens, or underscores.");
21614
+ if (false === options.checkout) return {
21615
+ name,
21616
+ tags,
21617
+ destinationPath: void 0
21618
+ };
21619
+ const directory = options.directory ?? sanitizeDirectoryName(name);
21620
+ if (!directory) throw new Error("App name cannot be used as a directory. Pass --directory <PATH>.");
21621
+ const destinationPath = external_path_default().resolve(directory);
21622
+ await validateCheckoutDestination(destinationPath);
21623
+ return {
21624
+ name,
21625
+ tags,
21626
+ destinationPath
21627
+ };
21628
+ }
21629
+ async function executeCreate(options, deps = create_defaultDeps) {
21630
+ const { name, tags, destinationPath } = await validateCreateInput(options);
21631
+ const anvilUrl = normalizeAnvilOrigin(options.anvilUrl ?? await resolveCheckoutUrlForApi() ?? "");
21632
+ const gitUrl = void 0 === options.gitUrl ? void 0 : normalizeAnvilOrigin(options.gitUrl);
21633
+ const accounts = options.user ? [] : await deps.getAccountsForUrl(anvilUrl);
21634
+ const username = options.user ?? accounts[0];
21635
+ if (!username) throw new Error(`Not logged in to ${anvilUrl}. Run 'anvil login ${anvilUrl}' and retry.`);
21636
+ if (!options.user && accounts.length > 1) throw new Error(`Multiple accounts found for ${anvilUrl}: ${accounts.join(", ")}. Use --user <USERNAME>.`);
21637
+ const authToken = await deps.getValidAuthToken(anvilUrl, username);
21638
+ const created = await deps.createApp({
21639
+ name,
21640
+ anvilUrl,
21641
+ authToken,
21642
+ ...tags ? {
21643
+ tags
21644
+ } : {}
21645
+ });
21646
+ if (void 0 === destinationPath) return {
21647
+ appId: created.id,
21648
+ appName: created.name,
21649
+ anvilUrl,
21650
+ username,
21651
+ tags: created.tags ?? tags ?? [],
21652
+ editorUrl: `${anvilUrl}/build/apps/${encodeURIComponent(created.id)}`
21653
+ };
21654
+ try {
21655
+ const checkout = await deps.checkout({
21656
+ input: created.id,
21657
+ directory: destinationPath,
21658
+ anvilUrl,
21659
+ ...gitUrl ? {
21660
+ gitUrl
21661
+ } : {},
21662
+ user: username
21663
+ });
21664
+ return {
21665
+ ...checkout,
21666
+ tags: created.tags ?? tags ?? []
21667
+ };
21668
+ } catch (error) {
21669
+ const details = {
21670
+ appId: created.id,
21671
+ appName: created.name,
21672
+ anvilUrl,
21673
+ gitUrl,
21674
+ destinationPath,
21675
+ username,
21676
+ tags: created.tags ?? tags ?? []
21677
+ };
21678
+ const retry = buildShellCommandLine([
21679
+ "anvil",
21680
+ "checkout",
21681
+ created.id,
21682
+ destinationPath,
21683
+ "--url",
21684
+ anvilUrl,
21685
+ "--user",
21686
+ username,
21687
+ ...gitUrl ? [
21688
+ "--git-url",
21689
+ gitUrl
21690
+ ] : []
21691
+ ]);
21692
+ const remainingFiles = await isDirectoryNonEmpty(destinationPath).catch(()=>true);
21693
+ const recovery = remainingFiles ? `Inspect the existing checkout at ${destinationPath} before retrying. For Git authentication problems, run ${buildShellCommandLine([
21694
+ "anvil",
21695
+ "git-auth",
21696
+ "doctor",
21697
+ "--repo",
21698
+ destinationPath
21699
+ ])}.` : `Retry checkout with: ${retry}`;
21700
+ const message = errors_getErrorMessage(error).replaceAll(authToken, "[REDACTED]").replaceAll(encodeURIComponent(authToken), "[REDACTED]");
21701
+ throw new CreateCheckoutError(`Created app ${created.id} at ${anvilUrl}, but checkout failed: ${message}. Do not rerun create. ${recovery}`, details, {
21702
+ cause: error
21703
+ });
21704
+ }
21705
+ }
21706
+ async function resolveCreateAccountInteractive(anvilUrl, options) {
21707
+ const user = await resolveCheckoutUsername(anvilUrl, options.user);
21708
+ if (null === user) return null;
21709
+ try {
21710
+ await auth_getValidAuthToken(anvilUrl, user);
21711
+ return user;
21712
+ } catch (error) {
21713
+ const loginNeeded = isAnvilError(error) && ([
21714
+ "auth_required",
21715
+ "auth_expired",
21716
+ "auth_invalid"
21717
+ ].includes(error.type) || "token_refresh_failed" === error.type && [
21718
+ 400,
21719
+ 401,
21720
+ 403
21721
+ ].includes(error.status ?? 0));
21722
+ if (!loginNeeded) throw error;
21723
+ if (!canPrompt()) throw new Error(`${errors_getErrorMessage(error)} Run 'anvil login ${anvilUrl}' and retry.`, {
21724
+ cause: error
21725
+ });
21726
+ if (!await logger_logger.confirm(`${errors_getErrorMessage(error)} Log in to ${anvilUrl} now?`, true)) return null;
21727
+ const login = await (options.login ?? runInteractiveLoginFlow)(anvilUrl);
21728
+ if (user && login.email !== user) throw new Error(`Logged in as ${login.email}, but the selected account is ${user}. No app was created.`);
21729
+ return login.email;
21730
+ }
21731
+ }
21732
+ function registerCreateCommand(program, deps = {
21733
+ executeCreate
21734
+ }) {
21735
+ program.command("create [name] [directory]").description("Create a new Anvil app and check it out locally").option("--no-checkout", "Create the app without a local checkout").option("-d, --directory <PATH>", "Local directory for the new app").option("--tag <TAG...>", "App tags (space-separated or repeated; place after name and directory)", (tag, tags)=>[
21736
+ ...tags,
21737
+ tag
21738
+ ], []).option("-u, --url <ANVIL_URL>", "Anvil server URL").option("-U, --user <USERNAME>", "Logged-in account to create the app").option("--git-url <GIT_URL>", "Git server URL, if separate from the Anvil server").option("-O, --open", "Open the new app directory after checkout").action(async (name, directory, options)=>{
21739
+ try {
21740
+ if (void 0 !== directory && void 0 !== options.directory) throw new Error("Pass the directory once, either as an argument or with --directory.");
21741
+ validateCheckoutOptions({
21742
+ ...options,
21743
+ directory: directory ?? options.directory
21744
+ });
21745
+ if (void 0 === name) {
21746
+ assertCanPrompt("`anvil create` without a name", "Pass a name: anvil create <NAME>.");
21747
+ const answer = await logger_logger.prompt([
21748
+ {
21749
+ type: "input",
21750
+ name: "name",
21751
+ message: "App name",
21752
+ validate: (value)=>value.trim() ? true : "App name is required."
21753
+ }
21754
+ ]);
21755
+ name = answer.name;
21756
+ }
21757
+ const input = await validateCreateInput({
21758
+ name,
21759
+ directory: directory ?? options.directory,
21760
+ tags: options.tag,
21761
+ checkout: options.checkout
21762
+ });
21763
+ const explicitUrl = void 0 === options.url ? void 0 : normalizeAnvilOrigin(options.url);
21764
+ const gitUrl = void 0 === options.gitUrl ? void 0 : normalizeAnvilOrigin(options.gitUrl);
21765
+ const selectedUrl = await resolveCheckoutUrl(explicitUrl);
21766
+ if (!selectedUrl) return void logger_logger.info("Create cancelled.");
21767
+ const anvilUrl = normalizeAnvilOrigin(selectedUrl);
21768
+ const user = await resolveCreateAccountInteractive(anvilUrl, {
21769
+ user: options.user,
21770
+ login: deps.login
21771
+ });
21772
+ if (null === user) return void logger_logger.info("Create cancelled.");
21773
+ const result = await deps.executeCreate({
21774
+ name: input.name,
21775
+ directory: input.destinationPath,
21776
+ tags: input.tags,
21777
+ anvilUrl,
21778
+ ...false === options.checkout ? {
21779
+ checkout: false
21780
+ } : {},
21781
+ ...gitUrl ? {
21782
+ gitUrl
21783
+ } : {},
21784
+ user
21785
+ });
21786
+ if (options.open && "destinationPath" in result) {
21787
+ const preferredEditor = String(getConfig("preferredEditor") || "").trim();
21788
+ try {
21789
+ await (deps.openPath ?? checkout_openPathInEditorOrDefault)(result.destinationPath, preferredEditor ? getPreferredEditorCommand(preferredEditor) : void 0);
21790
+ } catch (error) {
21791
+ logger_logger.warn(`App created and checked out at ${result.destinationPath}, but opening it failed: ${errors_getErrorMessage(error)}`);
21792
+ }
21793
+ }
21794
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
21795
+ data: result
21796
+ });
21797
+ else {
21798
+ logger_logger.success("destinationDisplay" in result ? `Created ${result.appName ?? result.appId} (${result.appId}) in ${result.destinationDisplay}` : `Created ${result.appName ?? result.appId} (${result.appId}) at ${result.editorUrl}`);
21799
+ if ("destinationDisplay" in result) logger_logger.info(`${result.anvilUrl}/build/apps/${encodeURIComponent(result.appId)}`);
21800
+ }
21801
+ } catch (error) {
21802
+ const message = errors_getErrorMessage(error);
21803
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
21804
+ error: message,
21805
+ ...error instanceof CreateCheckoutError ? {
21806
+ data: {
21807
+ created: error.created
21808
+ }
21809
+ } : {}
21810
+ });
21811
+ else logger_logger.error(`Error: ${message}`);
21812
+ process.exitCode = 1;
21813
+ }
21814
+ });
21815
+ }
21515
21816
  const defaultGitAuthDoctorDeps = {
21516
21817
  getAppAuthBinding: getAppAuthBinding,
21517
21818
  getAccountsForUrl: auth_getAccountsForUrl,
@@ -22206,36 +22507,6 @@ print(json.dumps({"issues": issues}))
22206
22507
  const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
22207
22508
  return selectedUrl;
22208
22509
  }
22209
- const defaultConfigureWatchGitAuthDeps = {
22210
- configureCredentialHelperForUrl: configureCredentialHelperForUrl,
22211
- setAppAuthBinding: setAppAuthBinding
22212
- };
22213
- const defaultSyncStartDeps = {
22214
- pushToAnvil,
22215
- recreateSessionAndValidate,
22216
- recheckSyncStatus,
22217
- startWatchingWithEventHandlers
22218
- };
22219
- function resolveWatchOpenPath(repoPath) {
22220
- return external_path_default().resolve(repoPath);
22221
- }
22222
- async function openWatchPath(targetPath, deps) {
22223
- const preferredEditor = String(getConfig("preferredEditor") || "").trim();
22224
- const preferredEditorCommand = preferredEditor ? getPreferredEditorCommand(preferredEditor) : "";
22225
- await openPathInEditorOrDefault(targetPath, preferredEditorCommand, deps);
22226
- logger_logger.info(external_chalk_default().gray(`Opened ${targetPath}`));
22227
- }
22228
- async function configureWatchGitAuth(options, deps = defaultConfigureWatchGitAuthDeps) {
22229
- await deps.configureCredentialHelperForUrl(options.repoPath, options.anvilUrl);
22230
- await deps.setAppAuthBinding(options.repoPath, options.appId, {
22231
- url: options.anvilUrl,
22232
- username: options.username
22233
- });
22234
- }
22235
- function isStaleRemotePushError(message) {
22236
- const normalized = message.toLowerCase();
22237
- return normalized.includes("fetch first") || normalized.includes("non-fast-forward") || normalized.includes("failed to push some refs") || normalized.includes("remote contains work that you do") || normalized.includes("[rejected]");
22238
- }
22239
22510
  async function selectAppId(candidates) {
22240
22511
  if (0 === candidates.length) {
22241
22512
  logger_logger.warn("Could not auto-detect app ID from repository.");
@@ -22313,6 +22584,169 @@ print(json.dumps({"issues": issues}))
22313
22584
  return null;
22314
22585
  }
22315
22586
  }
22587
+ class AppAccountCancelledError extends Error {
22588
+ constructor(){
22589
+ super("Operation cancelled.");
22590
+ }
22591
+ }
22592
+ class NoAppIdError extends Error {
22593
+ constructor(){
22594
+ super("No app ID provided. Cannot continue without an app ID.");
22595
+ }
22596
+ }
22597
+ async function resolveAppAccount(repoPath, options = {}) {
22598
+ const { appId: explicitAppId, url: explicitUrl, user: explicitUsername, useFirst = false } = options;
22599
+ auth_setRepoContext(repoPath);
22600
+ const detectedFromAllRemotes = await detectAppIdsFromAllRemotes(repoPath);
22601
+ let filteredCandidates = filterCandidates(detectedFromAllRemotes, explicitUrl, explicitUsername);
22602
+ logger_logger.verbose(external_chalk_default().cyan(`Detected ${detectedFromAllRemotes.length} app ID(s) from git remotes`));
22603
+ if (filteredCandidates.length !== detectedFromAllRemotes.length) logger_logger.verbose(external_chalk_default().cyan(`After filtering: ${filteredCandidates.length} candidate(s)`));
22604
+ let finalAppId;
22605
+ let anvilUrl;
22606
+ let username = explicitUsername;
22607
+ let fallbackUrl;
22608
+ let shouldPersistUsernameBinding = false;
22609
+ if (explicitAppId) {
22610
+ finalAppId = explicitAppId;
22611
+ const binding = await getAppAuthBinding(repoPath, explicitAppId);
22612
+ if (binding.url && !explicitUrl) {
22613
+ anvilUrl = normalizeAnvilUrl(binding.url);
22614
+ logger_logger.verbose(external_chalk_default().cyan("Resolved URL from binding for app ID: ") + external_chalk_default().bold(anvilUrl));
22615
+ }
22616
+ if (binding.username && !explicitUsername) {
22617
+ username = binding.username;
22618
+ logger_logger.verbose(external_chalk_default().cyan("Resolved username from binding for app ID: ") + external_chalk_default().bold(username));
22619
+ }
22620
+ const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
22621
+ if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
22622
+ anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
22623
+ logger_logger.verbose(external_chalk_default().cyan("Resolved URL from remote for app ID: ") + external_chalk_default().bold(anvilUrl));
22624
+ }
22625
+ if (remoteInfo.detectedUsername && !explicitUsername && !username) {
22626
+ username = remoteInfo.detectedUsername;
22627
+ logger_logger.verbose(external_chalk_default().cyan("Resolved username from remote for app ID: ") + external_chalk_default().bold(username));
22628
+ }
22629
+ } else {
22630
+ logger_logger.verbose(external_chalk_default().cyan("No app ID provided, attempting auto-detection..."));
22631
+ if (0 === filteredCandidates.length) {
22632
+ logger_logger.verbose(external_chalk_default().gray("No app IDs found in git remotes."));
22633
+ const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
22634
+ if (null === resolvedFallbackUrl) throw new AppAccountCancelledError();
22635
+ fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22636
+ const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
22637
+ if (null === lookupDecision) throw new AppAccountCancelledError();
22638
+ username = lookupDecision.username;
22639
+ if (lookupDecision.shouldContinue) {
22640
+ logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
22641
+ const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
22642
+ anvilUrl: fallbackUrl,
22643
+ username,
22644
+ includeRemotes: false
22645
+ });
22646
+ logger_logger.progressEnd("detect");
22647
+ for (const c of reverseLookupCandidates)filteredCandidates.push({
22648
+ ...c,
22649
+ detectedUrl: fallbackUrl
22650
+ });
22651
+ }
22652
+ }
22653
+ if (filteredCandidates.length > 0) {
22654
+ for (const c of filteredCandidates)logger_logger.verbose(external_chalk_default().gray(` Found: ${formatCandidateLabel(c)}`));
22655
+ if (filteredCandidates.length > 1) logger_logger.verbose(external_chalk_default().yellow(`Found ${filteredCandidates.length} potential app IDs`));
22656
+ }
22657
+ if (useFirst && filteredCandidates.length > 0) {
22658
+ const selected = filteredCandidates[0];
22659
+ finalAppId = selected.appId;
22660
+ if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22661
+ if (selected.detectedUsername && !username) username = selected.detectedUsername;
22662
+ logger_logger.success("Auto-selected first app ID: " + external_chalk_default().bold(finalAppId));
22663
+ } else {
22664
+ const selected = await selectAppId(filteredCandidates);
22665
+ if (!selected) throw new NoAppIdError();
22666
+ finalAppId = selected.appId;
22667
+ if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22668
+ if (selected.detectedUsername && !username) username = selected.detectedUsername;
22669
+ }
22670
+ }
22671
+ const binding = await getAppAuthBinding(repoPath, finalAppId);
22672
+ if (binding.url && !explicitUrl) {
22673
+ anvilUrl = normalizeAnvilUrl(binding.url);
22674
+ logger_logger.verbose(external_chalk_default().cyan("Using app binding URL: ") + external_chalk_default().bold(anvilUrl));
22675
+ }
22676
+ if (binding.username && !explicitUsername) {
22677
+ username = binding.username;
22678
+ logger_logger.verbose(external_chalk_default().cyan("Using app binding username: ") + external_chalk_default().bold(username));
22679
+ }
22680
+ shouldPersistUsernameBinding = !binding.username && !explicitUsername;
22681
+ if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
22682
+ else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
22683
+ else {
22684
+ const resolvedFallbackUrl = await resolveUrlForFallback();
22685
+ if (null === resolvedFallbackUrl) throw new AppAccountCancelledError();
22686
+ anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22687
+ }
22688
+ anvilUrl = normalizeAnvilUrl(anvilUrl);
22689
+ logger_logger.verbose(external_chalk_default().green("Using app ID: ") + external_chalk_default().bold(finalAppId));
22690
+ logger_logger.verbose(external_chalk_default().cyan("Using Anvil URL: ") + external_chalk_default().bold(anvilUrl));
22691
+ const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
22692
+ if (null === resolvedUsername) throw new AppAccountCancelledError();
22693
+ username = resolvedUsername;
22694
+ if (shouldPersistUsernameBinding && username && (await auth_getAccountsForUrl(anvilUrl)).length > 1) {
22695
+ await setAppAuthBinding(repoPath, finalAppId, {
22696
+ url: anvilUrl,
22697
+ username
22698
+ });
22699
+ logger_logger.verbose(external_chalk_default().cyan("Saved app account binding for future non-interactive use."));
22700
+ }
22701
+ if (username) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(username));
22702
+ return {
22703
+ appId: finalAppId,
22704
+ anvilUrl,
22705
+ ...username ? {
22706
+ username
22707
+ } : {}
22708
+ };
22709
+ }
22710
+ function addAppAccountOptions(command) {
22711
+ return command.option("-A, --appid <APP_ID>", "Specify app ID directly").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use");
22712
+ }
22713
+ function appAccountOptions(options) {
22714
+ return {
22715
+ appId: options.appid,
22716
+ url: options.url,
22717
+ user: options.user
22718
+ };
22719
+ }
22720
+ const defaultConfigureWatchGitAuthDeps = {
22721
+ configureCredentialHelperForUrl: configureCredentialHelperForUrl,
22722
+ setAppAuthBinding: setAppAuthBinding
22723
+ };
22724
+ const defaultSyncStartDeps = {
22725
+ pushToAnvil,
22726
+ recreateSessionAndValidate,
22727
+ recheckSyncStatus,
22728
+ startWatchingWithEventHandlers
22729
+ };
22730
+ function resolveWatchOpenPath(repoPath) {
22731
+ return external_path_default().resolve(repoPath);
22732
+ }
22733
+ async function openWatchPath(targetPath, deps) {
22734
+ const preferredEditor = String(getConfig("preferredEditor") || "").trim();
22735
+ const preferredEditorCommand = preferredEditor ? getPreferredEditorCommand(preferredEditor) : "";
22736
+ await openPathInEditorOrDefault(targetPath, preferredEditorCommand, deps);
22737
+ logger_logger.info(external_chalk_default().gray(`Opened ${targetPath}`));
22738
+ }
22739
+ async function configureWatchGitAuth(options, deps = defaultConfigureWatchGitAuthDeps) {
22740
+ await deps.configureCredentialHelperForUrl(options.repoPath, options.anvilUrl);
22741
+ await deps.setAppAuthBinding(options.repoPath, options.appId, {
22742
+ url: options.anvilUrl,
22743
+ username: options.username
22744
+ });
22745
+ }
22746
+ function isStaleRemotePushError(message) {
22747
+ const normalized = message.toLowerCase();
22748
+ return normalized.includes("fetch first") || normalized.includes("non-fast-forward") || normalized.includes("failed to push some refs") || normalized.includes("remote contains work that you do") || normalized.includes("[rejected]");
22749
+ }
22316
22750
  async function pushToAnvil(options) {
22317
22751
  try {
22318
22752
  const authToken = await auth_getValidAuthToken(options.anvilUrl, options.username);
@@ -22808,123 +23242,26 @@ print(json.dumps({"issues": issues}))
22808
23242
  try {
22809
23243
  const validationResult = await validateAnvilApp(repoPath);
22810
23244
  if (validationResult.appName) logger_logger.info(external_chalk_default().green("Anvil app: ") + external_chalk_default().bold(validationResult.appName));
22811
- const detectedFromAllRemotes = await detectAppIdsFromAllRemotes(repoPath);
22812
- let filteredCandidates = filterCandidates(detectedFromAllRemotes, explicitUrl, explicitUsername);
22813
- logger_logger.verbose(external_chalk_default().cyan(`Detected ${detectedFromAllRemotes.length} app ID(s) from git remotes`));
22814
- if (filteredCandidates.length !== detectedFromAllRemotes.length) logger_logger.verbose(external_chalk_default().cyan(`After filtering: ${filteredCandidates.length} candidate(s)`));
22815
- let finalAppId;
22816
- let anvilUrl;
22817
- let username = explicitUsername;
22818
- let fallbackUrl;
22819
- let shouldPersistUsernameBinding = false;
22820
- if (explicitAppId) {
22821
- finalAppId = explicitAppId;
22822
- const binding = await getAppAuthBinding(repoPath, explicitAppId);
22823
- if (binding.url && !explicitUrl) {
22824
- anvilUrl = normalizeAnvilUrl(binding.url);
22825
- logger_logger.verbose(external_chalk_default().cyan("Resolved URL from binding for app ID: ") + external_chalk_default().bold(anvilUrl));
22826
- }
22827
- if (binding.username && !explicitUsername) {
22828
- username = binding.username;
22829
- logger_logger.verbose(external_chalk_default().cyan("Resolved username from binding for app ID: ") + external_chalk_default().bold(username));
22830
- }
22831
- const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
22832
- if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
22833
- anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
22834
- logger_logger.verbose(external_chalk_default().cyan("Resolved URL from remote for app ID: ") + external_chalk_default().bold(anvilUrl));
22835
- }
22836
- if (remoteInfo.detectedUsername && !explicitUsername && !username) {
22837
- username = remoteInfo.detectedUsername;
22838
- logger_logger.verbose(external_chalk_default().cyan("Resolved username from remote for app ID: ") + external_chalk_default().bold(username));
22839
- }
22840
- } else {
22841
- logger_logger.verbose(external_chalk_default().cyan("No app ID provided, attempting auto-detection..."));
22842
- if (0 === filteredCandidates.length) {
22843
- logger_logger.verbose(external_chalk_default().gray("No app IDs found in git remotes."));
22844
- const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
22845
- if (null === resolvedFallbackUrl) {
22846
- logger_logger.warn("Operation cancelled.");
22847
- process.exit(0);
22848
- }
22849
- fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22850
- const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
22851
- if (null === lookupDecision) {
22852
- logger_logger.warn("Operation cancelled.");
22853
- process.exit(0);
22854
- }
22855
- username = lookupDecision.username;
22856
- if (lookupDecision.shouldContinue) {
22857
- logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
22858
- const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
22859
- anvilUrl: fallbackUrl,
22860
- username,
22861
- includeRemotes: false
22862
- });
22863
- logger_logger.progressEnd("detect");
22864
- for (const c of reverseLookupCandidates)filteredCandidates.push({
22865
- ...c,
22866
- detectedUrl: fallbackUrl
22867
- });
22868
- }
22869
- }
22870
- if (filteredCandidates.length > 0) {
22871
- for (const c of filteredCandidates)logger_logger.verbose(external_chalk_default().gray(` Found: ${formatCandidateLabel(c)}`));
22872
- if (filteredCandidates.length > 1) logger_logger.verbose(external_chalk_default().yellow(`Found ${filteredCandidates.length} potential app IDs`));
22873
- }
22874
- if (useFirst && filteredCandidates.length > 0) {
22875
- const selected = filteredCandidates[0];
22876
- finalAppId = selected.appId;
22877
- if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22878
- if (selected.detectedUsername && !username) username = selected.detectedUsername;
22879
- logger_logger.success("Auto-selected first app ID: " + external_chalk_default().bold(finalAppId));
22880
- } else {
22881
- const selected = await selectAppId(filteredCandidates);
22882
- if (!selected) {
22883
- logger_logger.error("No app ID provided. Cannot continue without an app ID.");
22884
- process.exit(1);
22885
- }
22886
- finalAppId = selected.appId;
22887
- if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22888
- if (selected.detectedUsername && !username) username = selected.detectedUsername;
22889
- }
22890
- }
22891
- const binding = await getAppAuthBinding(repoPath, finalAppId);
22892
- if (binding.url && !explicitUrl) {
22893
- anvilUrl = normalizeAnvilUrl(binding.url);
22894
- logger_logger.verbose(external_chalk_default().cyan("Using app binding URL: ") + external_chalk_default().bold(anvilUrl));
22895
- }
22896
- if (binding.username && !explicitUsername) {
22897
- username = binding.username;
22898
- logger_logger.verbose(external_chalk_default().cyan("Using app binding username: ") + external_chalk_default().bold(username));
22899
- }
22900
- shouldPersistUsernameBinding = !binding.username && !explicitUsername;
22901
- if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
22902
- else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
22903
- else {
22904
- const resolvedFallbackUrl = await resolveUrlForFallback();
22905
- if (null === resolvedFallbackUrl) {
23245
+ let account;
23246
+ try {
23247
+ account = await resolveAppAccount(repoPath, {
23248
+ appId: explicitAppId,
23249
+ url: explicitUrl,
23250
+ user: explicitUsername,
23251
+ useFirst
23252
+ });
23253
+ } catch (e) {
23254
+ if (e instanceof AppAccountCancelledError) {
22906
23255
  logger_logger.warn("Operation cancelled.");
22907
23256
  process.exit(0);
22908
23257
  }
22909
- anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22910
- }
22911
- anvilUrl = normalizeAnvilUrl(anvilUrl);
22912
- logger_logger.verbose(external_chalk_default().green("Using app ID: ") + external_chalk_default().bold(finalAppId));
22913
- logger_logger.verbose(external_chalk_default().cyan("Using Anvil URL: ") + external_chalk_default().bold(anvilUrl));
22914
- const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
22915
- if (null === resolvedUsername) {
22916
- logger_logger.warn("Operation cancelled.");
22917
- process.exit(0);
22918
- }
22919
- username = resolvedUsername;
22920
- if (shouldPersistUsernameBinding && username && (await auth_getAccountsForUrl(anvilUrl)).length > 1) {
22921
- await setAppAuthBinding(repoPath, finalAppId, {
22922
- url: anvilUrl,
22923
- username
22924
- });
22925
- logger_logger.verbose(external_chalk_default().cyan("Saved app account binding for future non-interactive use."));
23258
+ if (e instanceof NoAppIdError) {
23259
+ logger_logger.error(e.message);
23260
+ process.exit(1);
23261
+ }
23262
+ throw e;
22926
23263
  }
22927
- if (username) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(username));
23264
+ const { appId: finalAppId, anvilUrl, username } = account;
22928
23265
  if (!await hasTokensForUrl(anvilUrl, username)) {
22929
23266
  if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
22930
23267
  else logger_logger.error(`Not logged in to ${anvilUrl}`);
@@ -22981,7 +23318,7 @@ print(json.dumps({"issues": issues}))
22981
23318
  }
22982
23319
  }
22983
23320
  function registerWatchCommand(program) {
22984
- const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("--no-dependency-watch", "Do not watch and refresh .anvil/deps dependencies").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
23321
+ const watchCommand = addAppAccountOptions(program.command("watch [path]")).description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("--no-dependency-watch", "Do not watch and refresh .anvil/deps dependencies").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").action(async (path, options, command)=>{
22985
23322
  const globalOptions = command.optsWithGlobals();
22986
23323
  if (void 0 !== globalOptions.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(globalOptions.verbose);
22987
23324
  await handleWatchCommand({
@@ -23595,33 +23932,19 @@ print(json.dumps({"issues": issues}))
23595
23932
  }
23596
23933
  });
23597
23934
  }
23598
- async function resolvePrimaryAppId(projectRoot, anvilUrl) {
23599
- const remoteCandidates = await detectAppIdsFromAllRemotes(projectRoot);
23600
- if (remoteCandidates.length > 0) return remoteCandidates[0].appId;
23601
- const lookupCandidates = await detectAppIdsByCommitLookup(projectRoot, {
23602
- anvilUrl,
23603
- includeRemotes: false
23604
- });
23605
- return lookupCandidates[0]?.appId ?? null;
23606
- }
23607
23935
  function registerDepsCommand(program) {
23608
23936
  const deps = program.command("deps").description("Manage app dependencies");
23609
- deps.command("fetch").description("Fetch dependencies into .anvil/deps/").option("--commit <commit>", "Fetch deps at specific commit").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
23937
+ addAppAccountOptions(deps.command("fetch")).description("Fetch dependencies for checkout HEAD into .anvil/deps/").option("--commit <commit>", "Fetch deps at a specific app commit instead of checkout HEAD").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
23610
23938
  try {
23611
23939
  const projectRoot = process.cwd();
23612
- auth_setRepoContext(projectRoot);
23613
- const anvilUrl = await resolveAuthAnvilUrl();
23614
- const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23615
- if (!mainAppId) {
23616
- logger_logger.error("No Anvil app found in current directory");
23617
- logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
23618
- process.exit(1);
23619
- }
23940
+ const { appId: mainAppId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
23620
23941
  logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
23621
23942
  const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
23943
+ username,
23622
23944
  commit: options.commit,
23623
23945
  includeDocs: !options.excludeDocs
23624
23946
  });
23947
+ if (await getDependencyCacheWarning(projectRoot)) logger_logger.warn("Dependency cache does not match local anvil.yaml. Fetch uses committed dependency declarations; sync dependency changes to Anvil before fetching again.");
23625
23948
  if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
23626
23949
  if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
23627
23950
  if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
@@ -23658,7 +23981,7 @@ print(json.dumps({"issues": issues}))
23658
23981
  auth_setRepoContext(projectRoot);
23659
23982
  const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23660
23983
  const appYaml = external_js_yaml_default().load(await external_fs_.promises.readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf8"));
23661
- const token = await auth_getValidAuthToken(anvilUrl);
23984
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
23662
23985
  const response = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db/infer-from-env/apply-schema`, {
23663
23986
  method: "PUT",
23664
23987
  headers: {
@@ -23681,16 +24004,14 @@ print(json.dumps({"issues": issues}))
23681
24004
  function registerDbCommand(program) {
23682
24005
  if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23683
24006
  const db = program.command("db").description("Manage app databases");
23684
- db.command("apply-schema").description("Apply the app schema to the database for the current environment").option("-f, --force", "Apply the schema without asking for confirmation").action(async (options)=>{
24007
+ addAppAccountOptions(db.command("apply-schema")).description("Apply the app schema to the database for the current environment").option("-f, --force", "Apply the schema without asking for confirmation").action(async (options)=>{
23685
24008
  try {
23686
24009
  const projectRoot = process.cwd();
23687
- auth_setRepoContext(projectRoot);
23688
- const anvilUrl = await resolveAuthAnvilUrl();
23689
- const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23690
- if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
24010
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
23691
24011
  if (!await confirmSchemaApplication(options.force)) return void logger_logger.info("Schema application cancelled.");
23692
24012
  const response = await applyDatabaseSchema(appId, {
23693
24013
  anvilUrl,
24014
+ username,
23694
24015
  projectRoot
23695
24016
  });
23696
24017
  if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
@@ -23709,7 +24030,7 @@ print(json.dumps({"issues": issues}))
23709
24030
  }
23710
24031
  async function ensureEnvironmentUrl(appId, envPid, options = {}) {
23711
24032
  const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23712
- const token = await auth_getValidAuthToken(anvilUrl);
24033
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
23713
24034
  const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-url`, {
23714
24035
  method: "POST",
23715
24036
  headers: {
@@ -23725,7 +24046,7 @@ print(json.dumps({"issues": issues}))
23725
24046
  }
23726
24047
  async function getTemporaryUplinkKey(appId, envPid, options = {}) {
23727
24048
  const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23728
- const token = await auth_getValidAuthToken(anvilUrl);
24049
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
23729
24050
  const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-uplink-key`, {
23730
24051
  method: "POST",
23731
24052
  headers: {
@@ -23740,28 +24061,29 @@ print(json.dumps({"issues": issues}))
23740
24061
  expires_at
23741
24062
  };
23742
24063
  }
23743
- async function resolveEnvironmentContext() {
24064
+ async function resolveEnvironmentContext(options = {}) {
23744
24065
  const projectRoot = process.cwd();
23745
24066
  const envPid = await readEnvironmentPid(projectRoot);
23746
24067
  if (!envPid) throw new Error("No app environment associated with the current directory.");
23747
- auth_setRepoContext(projectRoot);
23748
- const anvilUrl = await resolveAuthAnvilUrl();
23749
- const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23750
- if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
24068
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, options);
23751
24069
  return {
23752
24070
  envPid,
23753
24071
  anvilUrl,
23754
- appId
24072
+ appId,
24073
+ ...username ? {
24074
+ username
24075
+ } : {}
23755
24076
  };
23756
24077
  }
23757
24078
  function registerEnvCommand(program) {
23758
24079
  if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23759
24080
  const env = program.command("env").description("Manage app environments");
23760
- env.command("url").description("Get a temporary private app URL for the current environment").action(async ()=>{
24081
+ addAppAccountOptions(env.command("url")).description("Get a temporary private app URL for the current environment").action(async (options)=>{
23761
24082
  try {
23762
- const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
24083
+ const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
23763
24084
  const { url, expires_at } = await ensureEnvironmentUrl(appId, envPid, {
23764
- anvilUrl
24085
+ anvilUrl,
24086
+ username
23765
24087
  });
23766
24088
  if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23767
24089
  data: {
@@ -23779,11 +24101,12 @@ print(json.dumps({"issues": issues}))
23779
24101
  process.exit(1);
23780
24102
  }
23781
24103
  });
23782
- env.command("uplink").description("Get a temporary uplink key for the current environment").action(async ()=>{
24104
+ addAppAccountOptions(env.command("uplink")).description("Get a temporary uplink key for the current environment").action(async (options)=>{
23783
24105
  try {
23784
- const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
24106
+ const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
23785
24107
  const { key, url, expires_at } = await getTemporaryUplinkKey(appId, envPid, {
23786
- anvilUrl
24108
+ anvilUrl,
24109
+ username
23787
24110
  });
23788
24111
  if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23789
24112
  data: {
@@ -23833,7 +24156,7 @@ print(json.dumps({"issues": issues}))
23833
24156
  async function runRepl(appId, envPid, options) {
23834
24157
  const interrupted = ()=>new Error("REPL interrupted.");
23835
24158
  const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23836
- const token = await auth_getValidAuthToken(anvilUrl);
24159
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
23837
24160
  const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/ws`);
23838
24161
  url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
23839
24162
  if (options.signal?.aborted) throw interrupted();
@@ -23982,16 +24305,16 @@ print(json.dumps({"issues": issues}))
23982
24305
  }
23983
24306
  function registerReplCommand(program) {
23984
24307
  if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23985
- program.command("repl <code>").description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
24308
+ addAppAccountOptions(program.command("repl <code>")).description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
23986
24309
  Examples:
23987
24310
  anvil repl '1 + 2'
23988
24311
  anvil --json repl '1 + 2'
23989
24312
  anvil repl - < script.py
23990
- `).action(async (code)=>{
24313
+ `).action(async (code, options)=>{
23991
24314
  const controller = new AbortController();
23992
24315
  const onInterrupt = ()=>controller.abort();
23993
24316
  try {
23994
- const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
24317
+ const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
23995
24318
  if ("-" === code) {
23996
24319
  const chunks = [];
23997
24320
  for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -24001,6 +24324,7 @@ Examples:
24001
24324
  const response = await runRepl(appId, envPid, {
24002
24325
  code,
24003
24326
  anvilUrl,
24327
+ username,
24004
24328
  signal: controller.signal,
24005
24329
  onOutput: getGlobalOutputConfig().jsonMode ? writeJsonReplOutput : (output)=>{
24006
24330
  process.stdout.write(output);
@@ -24029,6 +24353,714 @@ Examples:
24029
24353
  }
24030
24354
  });
24031
24355
  }
24356
+ const external_yaml_namespaceObject = require("yaml");
24357
+ async function encryptSecret(appId, secretName, options = {}) {
24358
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24359
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24360
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/encrypt`, {
24361
+ method: "POST",
24362
+ headers: {
24363
+ Authorization: `Bearer ${token}`,
24364
+ "Content-Type": "application/json"
24365
+ },
24366
+ body: JSON.stringify({
24367
+ name: secretName,
24368
+ ...void 0 === options.secret ? {} : {
24369
+ secret: options.secret
24370
+ }
24371
+ })
24372
+ });
24373
+ if (!response.ok) throw new Error(formatHttpError("Failed to set secret", response.status, await response.text()));
24374
+ return await response.json();
24375
+ }
24376
+ async function generateSecretKey(appId, keyName, options = {}) {
24377
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24378
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24379
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/generate-key`, {
24380
+ method: "POST",
24381
+ headers: {
24382
+ Authorization: `Bearer ${token}`,
24383
+ "Content-Type": "application/json"
24384
+ },
24385
+ body: JSON.stringify({
24386
+ name: keyName
24387
+ })
24388
+ });
24389
+ if (!response.ok) throw new Error(formatHttpError("Failed to generate key", response.status, await response.text()));
24390
+ return await response.json();
24391
+ }
24392
+ function secret_anvilYamlPath(projectRoot) {
24393
+ return external_path_default().join(projectRoot, "anvil.yaml");
24394
+ }
24395
+ function checkSecretTypeInDoc(doc, secretName, type) {
24396
+ const existingType = doc.getIn([
24397
+ "secrets",
24398
+ secretName,
24399
+ "type"
24400
+ ]);
24401
+ if (void 0 !== existingType && existingType !== type) throw new Error(`'${secretName}' is a ${existingType} in anvil.yaml, not a ${type}`);
24402
+ }
24403
+ function secretExistsInDoc(doc, secretName, appId) {
24404
+ return doc.hasIn([
24405
+ "secrets",
24406
+ secretName,
24407
+ "value",
24408
+ appId
24409
+ ]);
24410
+ }
24411
+ async function findSecretInAnvilYaml(projectRoot, secretName, appId) {
24412
+ const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(secret_anvilYamlPath(projectRoot), "utf8"));
24413
+ if (!doc.hasIn([
24414
+ "secrets",
24415
+ secretName
24416
+ ])) return;
24417
+ return {
24418
+ type: doc.getIn([
24419
+ "secrets",
24420
+ secretName,
24421
+ "type"
24422
+ ]),
24423
+ existsForApp: secretExistsInDoc(doc, secretName, appId)
24424
+ };
24425
+ }
24426
+ async function writeSecretToAnvilYaml(projectRoot, secretName, options) {
24427
+ const { appId, encrypted, overwrite, type } = options;
24428
+ const filePath = secret_anvilYamlPath(projectRoot);
24429
+ const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(filePath, "utf8"));
24430
+ checkSecretTypeInDoc(doc, secretName, type);
24431
+ if (secretExistsInDoc(doc, secretName, appId)) {
24432
+ if (!overwrite) throw new Error(`'${secretName}' already exists for app ${appId} in anvil.yaml`);
24433
+ if (!doc.hasIn([
24434
+ "secrets",
24435
+ secretName,
24436
+ "value",
24437
+ appId,
24438
+ "all"
24439
+ ])) throw new Error(`'${secretName}' for app ${appId} in anvil.yaml cannot be overridden`);
24440
+ }
24441
+ if (doc.hasIn([
24442
+ "secrets",
24443
+ secretName
24444
+ ])) {
24445
+ if (!(0, external_yaml_namespaceObject.isMap)(doc.getIn([
24446
+ "secrets",
24447
+ secretName,
24448
+ "value"
24449
+ ]))) doc.setIn([
24450
+ "secrets",
24451
+ secretName,
24452
+ "value"
24453
+ ], doc.createNode({}));
24454
+ } else doc.setIn([
24455
+ "secrets",
24456
+ secretName
24457
+ ], doc.createNode({
24458
+ type,
24459
+ value: {}
24460
+ }));
24461
+ const value = new external_yaml_namespaceObject.Scalar(encrypted);
24462
+ value.type = external_yaml_namespaceObject.Scalar.QUOTE_DOUBLE;
24463
+ if ((0, external_yaml_namespaceObject.isMap)(doc.getIn([
24464
+ "secrets",
24465
+ secretName,
24466
+ "value",
24467
+ appId
24468
+ ]))) doc.setIn([
24469
+ "secrets",
24470
+ secretName,
24471
+ "value",
24472
+ appId,
24473
+ "all"
24474
+ ], value);
24475
+ else doc.setIn([
24476
+ "secrets",
24477
+ secretName,
24478
+ "value",
24479
+ appId
24480
+ ], doc.createNode({
24481
+ all: value
24482
+ }, {
24483
+ flow: true
24484
+ }));
24485
+ await external_fs_.promises.writeFile(filePath, doc.toString(), "utf8");
24486
+ }
24487
+ function registerSecretCommand(program) {
24488
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
24489
+ const secret = program.command("secret").description("Manage app secrets");
24490
+ addAppAccountOptions(secret.command("set")).description("Set an app secret. The value is read from stdin, or prompted for if stdin is a terminal.").argument("<name>", "Name of the secret").option("--generate", "Have the server autogenerate the secret value instead of reading it").option("-f, --force", "Overwrite the secret if it is already set for this app").action(async (name, options)=>{
24491
+ try {
24492
+ const projectRoot = process.cwd();
24493
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24494
+ const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
24495
+ appId,
24496
+ type: "secret",
24497
+ force: options.force
24498
+ });
24499
+ const value = options.generate ? void 0 : await readSecretValue(name);
24500
+ const response = await encryptSecret(appId, name, {
24501
+ anvilUrl,
24502
+ username,
24503
+ secret: value
24504
+ });
24505
+ await writeSecretToAnvilYaml(projectRoot, name, {
24506
+ appId,
24507
+ encrypted: response.encrypted,
24508
+ overwrite,
24509
+ type: "secret"
24510
+ });
24511
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24512
+ data: {
24513
+ encrypted: response.encrypted
24514
+ }
24515
+ });
24516
+ else logger_logger.info(`Secret '${name}' written to anvil.yaml`);
24517
+ } catch (error) {
24518
+ const message = errors_getErrorMessage(error);
24519
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24520
+ error: message
24521
+ });
24522
+ else logger_logger.error(message);
24523
+ process.exit(1);
24524
+ }
24525
+ });
24526
+ addAppAccountOptions(secret.command("generate-key")).description("Generate an app encryption key. The key is always generated by the server, and its value is never shown.").argument("<name>", "Name of the key").option("-f, --force", "Replace the key if it is already set for this app").action(async (name, options)=>{
24527
+ try {
24528
+ const projectRoot = process.cwd();
24529
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24530
+ const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
24531
+ appId,
24532
+ type: "key",
24533
+ force: options.force
24534
+ });
24535
+ const response = await generateSecretKey(appId, name, {
24536
+ anvilUrl,
24537
+ username
24538
+ });
24539
+ await writeSecretToAnvilYaml(projectRoot, name, {
24540
+ appId,
24541
+ encrypted: response.encrypted,
24542
+ overwrite,
24543
+ type: "key"
24544
+ });
24545
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24546
+ data: {
24547
+ encrypted: response.encrypted
24548
+ }
24549
+ });
24550
+ else logger_logger.info(`Key '${name}' written to anvil.yaml`);
24551
+ } catch (error) {
24552
+ const message = errors_getErrorMessage(error);
24553
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24554
+ error: message
24555
+ });
24556
+ else logger_logger.error(message);
24557
+ process.exit(1);
24558
+ }
24559
+ });
24560
+ }
24561
+ async function confirmOverwriteIfExists(projectRoot, name, options) {
24562
+ const { appId, type, force = false } = options;
24563
+ const existing = await findSecretInAnvilYaml(projectRoot, name, appId);
24564
+ if (existing?.type && existing.type !== type) throw new Error(`'${name}' is a ${existing.type} in anvil.yaml, not a ${type}`);
24565
+ if (!existing?.existsForApp) return false;
24566
+ if (force) return true;
24567
+ const message = `'${name}' already exists for app ${appId} in anvil.yaml`;
24568
+ if (!process.stdin.isTTY) throw new Error(`${message}. Use --force to overwrite it.`);
24569
+ if (!await logger_logger.confirm(`${message}. Overwrite it?`, false)) throw new Error(`Not overwriting '${name}'`);
24570
+ return true;
24571
+ }
24572
+ async function readSecretValue(name) {
24573
+ if (process.stdin.isTTY) {
24574
+ const { secret } = await logger_logger.prompt([
24575
+ {
24576
+ type: "password",
24577
+ name: "secret",
24578
+ message: `Value for secret '${name}':`
24579
+ }
24580
+ ]);
24581
+ if (!secret) throw new Error("No secret value entered");
24582
+ return secret;
24583
+ }
24584
+ {
24585
+ const chunks = [];
24586
+ for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
24587
+ const secret = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
24588
+ if (!secret) throw new Error("No secret value received on stdin");
24589
+ return secret;
24590
+ }
24591
+ }
24592
+ const REORDER_WAIT_MS = 200;
24593
+ const LATE_OUTPUT_QUIET_MS = 500;
24594
+ const LATE_OUTPUT_MAX_MS = 3000;
24595
+ const CANCEL_WAIT_MS = 5000;
24596
+ class ServerTestsError extends Error {
24597
+ }
24598
+ class InterruptedError extends Error {
24599
+ }
24600
+ function serverErrorMessage(error) {
24601
+ if (error && "object" == typeof error) {
24602
+ const value = error;
24603
+ return String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value));
24604
+ }
24605
+ return String(error);
24606
+ }
24607
+ class OrderedLog {
24608
+ write;
24609
+ nextSeq = 1;
24610
+ held = new Map();
24611
+ timer;
24612
+ constructor(write){
24613
+ this.write = write;
24614
+ }
24615
+ add(seq, text) {
24616
+ if ("number" != typeof seq || seq < this.nextSeq) return void this.write(text);
24617
+ this.held.set(seq, text);
24618
+ while(this.held.has(this.nextSeq)){
24619
+ const next = this.held.get(this.nextSeq);
24620
+ this.held.delete(this.nextSeq);
24621
+ this.nextSeq++;
24622
+ this.write(next);
24623
+ }
24624
+ if (0 === this.held.size) {
24625
+ clearTimeout(this.timer);
24626
+ this.timer = void 0;
24627
+ } else this.timer ??= setTimeout(()=>{
24628
+ this.timer = void 0;
24629
+ this.flush();
24630
+ }, REORDER_WAIT_MS);
24631
+ }
24632
+ flush() {
24633
+ clearTimeout(this.timer);
24634
+ this.timer = void 0;
24635
+ for (const seq of [
24636
+ ...this.held.keys()
24637
+ ].sort((a, b)=>a - b)){
24638
+ this.write(this.held.get(seq));
24639
+ this.nextSeq = seq + 1;
24640
+ }
24641
+ this.held.clear();
24642
+ }
24643
+ }
24644
+ async function runServerTests(appId, options) {
24645
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24646
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24647
+ const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/ws`);
24648
+ url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
24649
+ if (options.signal?.aborted) throw new InterruptedError("Interrupted.");
24650
+ const ws = new (external_ws_default())(url, {
24651
+ headers: {
24652
+ Authorization: `Bearer ${token}`
24653
+ },
24654
+ handshakeTimeout: 30000
24655
+ });
24656
+ const controller = new AbortController();
24657
+ const replies = new Map();
24658
+ let nextId = 1;
24659
+ let runId;
24660
+ let sessionId;
24661
+ let launchMessage = "";
24662
+ let cancelId;
24663
+ let keepalive;
24664
+ let pongTimeout;
24665
+ let closeTimeout;
24666
+ const unmatchedLogs = new Map();
24667
+ const runLog = new OrderedLog((text)=>{
24668
+ if (text) options.onOutput?.(text);
24669
+ });
24670
+ let onRunLogEvent;
24671
+ const fail = (error)=>controller.abort(error);
24672
+ const connectionLost = (detail)=>runId ? new Error(`Lost connection to Anvil. The test run (${runId}) may still be running.`) : new Error(`Lost connection to Anvil (${detail}).`);
24673
+ const markResponsive = ()=>{
24674
+ clearTimeout(pongTimeout);
24675
+ pongTimeout = void 0;
24676
+ };
24677
+ function addRunLog(message) {
24678
+ runLog.add(message.seq, "print" === message.type && "string" == typeof message.log_text ? message.log_text : void 0);
24679
+ }
24680
+ function setSession(id) {
24681
+ if (sessionId || "string" != typeof id || !id) return;
24682
+ sessionId = id;
24683
+ for (const message of unmatchedLogs.get(id) ?? [])addRunLog(message);
24684
+ unmatchedLogs.clear();
24685
+ }
24686
+ function handleMessage(raw) {
24687
+ markResponsive();
24688
+ let message;
24689
+ try {
24690
+ message = JSON.parse(raw.toString());
24691
+ if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
24692
+ } catch {
24693
+ fail(new Error("Invalid message from Anvil."));
24694
+ return;
24695
+ }
24696
+ if ("number" == typeof message.id) {
24697
+ if (message.error && message.id !== cancelId) return void fail(new ServerTestsError(serverErrorMessage(message.error)));
24698
+ const resolve = replies.get(message.id);
24699
+ replies.delete(message.id);
24700
+ resolve?.(message);
24701
+ return;
24702
+ }
24703
+ switch(message.event){
24704
+ case "LOG_EVENT":
24705
+ if ("string" != typeof message.session_id) return;
24706
+ if (sessionId) {
24707
+ if (message.session_id !== sessionId) return;
24708
+ addRunLog(message);
24709
+ onRunLogEvent?.();
24710
+ } else {
24711
+ const logs = unmatchedLogs.get(message.session_id) ?? [];
24712
+ logs.push(message);
24713
+ unmatchedLogs.set(message.session_id, logs);
24714
+ }
24715
+ return;
24716
+ case "TEST_EVENT":
24717
+ {
24718
+ if (!runId || message.run_id !== runId) return;
24719
+ setSession(message.session_id);
24720
+ const testEvent = message.test_event;
24721
+ if (testEvent?.type === "test_run_update" && "string" == typeof testEvent.launch_message) {
24722
+ const text = testEvent.launch_message;
24723
+ const added = text.startsWith(launchMessage) ? text.slice(launchMessage.length) : text;
24724
+ launchMessage = text;
24725
+ if (added) options.onLaunchMessage?.(added);
24726
+ }
24727
+ return;
24728
+ }
24729
+ }
24730
+ }
24731
+ function send(message) {
24732
+ const id = nextId++;
24733
+ ws.send(JSON.stringify({
24734
+ id,
24735
+ ...message
24736
+ }), (error)=>{
24737
+ if (error) fail(connectionLost(`send failed: ${error.message}`));
24738
+ });
24739
+ return id;
24740
+ }
24741
+ function reply(id) {
24742
+ const signal = controller.signal;
24743
+ return new Promise((resolve, reject)=>{
24744
+ if (signal.aborted) return void reject(signal.reason);
24745
+ const onAbort = ()=>{
24746
+ replies.delete(id);
24747
+ reject(signal.reason);
24748
+ };
24749
+ signal.addEventListener("abort", onAbort, {
24750
+ once: true
24751
+ });
24752
+ replies.set(id, (message)=>{
24753
+ signal.removeEventListener("abort", onAbort);
24754
+ resolve(message);
24755
+ });
24756
+ });
24757
+ }
24758
+ const request = (message)=>reply(send(message));
24759
+ async function cancel(waited) {
24760
+ let cancelTimeout;
24761
+ let confirmed = false;
24762
+ try {
24763
+ cancelId = send({
24764
+ cmd: "CANCEL_SERVER_TEST_RUN",
24765
+ run_id: runId
24766
+ });
24767
+ const answer = await Promise.race([
24768
+ reply(cancelId),
24769
+ waited,
24770
+ new Promise((_, reject)=>{
24771
+ cancelTimeout = setTimeout(()=>reject(new Error("Timed out")), CANCEL_WAIT_MS);
24772
+ })
24773
+ ]);
24774
+ confirmed = !answer.error;
24775
+ } catch {} finally{
24776
+ clearTimeout(cancelTimeout);
24777
+ }
24778
+ runLog.flush();
24779
+ throw new InterruptedError(confirmed ? `Cancelled the test run (${runId}).` : `Couldn't confirm that the test run (${runId}) was cancelled. It may still be running.`);
24780
+ }
24781
+ async function readLateOutput() {
24782
+ await new Promise((resolve)=>{
24783
+ let quietTimeout;
24784
+ const done = ()=>{
24785
+ clearTimeout(quietTimeout);
24786
+ clearTimeout(maxTimeout);
24787
+ onRunLogEvent = void 0;
24788
+ controller.signal.removeEventListener("abort", done);
24789
+ options.signal?.removeEventListener("abort", done);
24790
+ resolve();
24791
+ };
24792
+ const resetQuiet = ()=>{
24793
+ clearTimeout(quietTimeout);
24794
+ quietTimeout = setTimeout(done, LATE_OUTPUT_QUIET_MS);
24795
+ };
24796
+ const maxTimeout = setTimeout(done, LATE_OUTPUT_MAX_MS);
24797
+ onRunLogEvent = resetQuiet;
24798
+ resetQuiet();
24799
+ controller.signal.addEventListener("abort", done, {
24800
+ once: true
24801
+ });
24802
+ options.signal?.addEventListener("abort", done, {
24803
+ once: true
24804
+ });
24805
+ });
24806
+ runLog.flush();
24807
+ }
24808
+ const onAbort = ()=>fail(new InterruptedError("Interrupted."));
24809
+ ws.on("pong", markResponsive);
24810
+ ws.on("unexpected-response", (request, response)=>{
24811
+ response.resume();
24812
+ fail(new Error(`Couldn't connect to Anvil (${response.statusCode} ${response.statusMessage}).`));
24813
+ request.destroy();
24814
+ });
24815
+ ws.on("error", (error)=>fail(connectionLost(error.message)));
24816
+ ws.on("close", (code, reason)=>{
24817
+ clearTimeout(closeTimeout);
24818
+ fail(connectionLost(`code ${code}${reason.length ? `: ${reason}` : ""}`));
24819
+ });
24820
+ ws.on("message", handleMessage);
24821
+ try {
24822
+ options.signal?.addEventListener("abort", onAbort, {
24823
+ once: true
24824
+ });
24825
+ await new Promise((resolve, reject)=>{
24826
+ ws.once("open", ()=>resolve());
24827
+ controller.signal.addEventListener("abort", ()=>reject(controller.signal.reason), {
24828
+ once: true
24829
+ });
24830
+ });
24831
+ keepalive = setInterval(()=>{
24832
+ pongTimeout ??= setTimeout(()=>fail(connectionLost("heartbeat timed out")), 10000);
24833
+ ws.ping();
24834
+ }, 20000);
24835
+ await request({
24836
+ cmd: "WATCH_APP",
24837
+ app: appId,
24838
+ only_branch: options.branch
24839
+ });
24840
+ const launched = await request({
24841
+ cmd: "RUN_SERVER_TESTS",
24842
+ commit_id: options.commitId,
24843
+ ...options.selection ? {
24844
+ selection: options.selection
24845
+ } : {},
24846
+ ...options.collectOnly ? {
24847
+ collect_only: true
24848
+ } : {}
24849
+ });
24850
+ if ("string" != typeof launched.run_id || !launched.run_id) throw new Error("Invalid response from Anvil: missing test run ID.");
24851
+ runId = launched.run_id;
24852
+ options.onRunCreated?.(runId);
24853
+ options.signal?.removeEventListener("abort", onAbort);
24854
+ const interrupted = new Promise((resolve)=>{
24855
+ if (options.signal?.aborted) resolve();
24856
+ options.signal?.addEventListener("abort", ()=>resolve(), {
24857
+ once: true
24858
+ });
24859
+ });
24860
+ const watchId = send({
24861
+ cmd: "WATCH_APP",
24862
+ app: appId,
24863
+ only_branch: options.branch,
24864
+ test_run_ids: [
24865
+ runId
24866
+ ]
24867
+ });
24868
+ const waitId = send({
24869
+ cmd: "WAIT_FOR_SERVER_TEST_RUN",
24870
+ run_id: runId
24871
+ });
24872
+ reply(watchId).catch(()=>{});
24873
+ const waited = reply(waitId);
24874
+ const done = await Promise.race([
24875
+ waited,
24876
+ interrupted.then(()=>void 0)
24877
+ ]);
24878
+ if (!done) return await cancel(waited);
24879
+ setSession(done.session_id);
24880
+ if (!sessionId) unmatchedLogs.clear();
24881
+ await readLateOutput();
24882
+ return {
24883
+ runId,
24884
+ ...sessionId ? {
24885
+ sessionId
24886
+ } : {},
24887
+ state: done.state ?? {}
24888
+ };
24889
+ } catch (error) {
24890
+ if (error instanceof InterruptedError) throw error;
24891
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
24892
+ throw failure instanceof Error ? failure : new Error(String(failure));
24893
+ } finally{
24894
+ clearInterval(keepalive);
24895
+ clearTimeout(pongTimeout);
24896
+ runLog.flush();
24897
+ options.signal?.removeEventListener("abort", onAbort);
24898
+ controller.abort();
24899
+ if (ws.readyState === external_ws_default().OPEN) ws.close();
24900
+ else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
24901
+ if (ws.readyState !== external_ws_default().CLOSED) {
24902
+ closeTimeout = setTimeout(()=>ws.terminate(), 1000);
24903
+ closeTimeout.unref();
24904
+ }
24905
+ }
24906
+ }
24907
+ const PYTEST_EXIT_CODES = {
24908
+ OK: 0,
24909
+ TESTS_FAILED: 1,
24910
+ INTERRUPTED: 2,
24911
+ INTERNAL_ERROR: 3,
24912
+ USAGE_ERROR: 4,
24913
+ NO_TESTS_COLLECTED: 5
24914
+ };
24915
+ class UsageError extends Error {
24916
+ }
24917
+ function toServerTestTarget(target, options) {
24918
+ const separator = target.indexOf("::");
24919
+ const targetPath = -1 === separator ? target : target.slice(0, separator);
24920
+ const suffix = -1 === separator ? "" : target.slice(separator);
24921
+ const absolutePath = external_path_default().resolve(options.cwd, targetPath);
24922
+ const serverTestsPath = external_path_default().relative(external_path_default().join(options.appRoot, "tests", "server_tests"), absolutePath);
24923
+ if (external_path_default().isAbsolute(serverTestsPath) || ".." === serverTestsPath || serverTestsPath.startsWith(`..${external_path_default().sep}`)) throw new UsageError(`${target} isn't in tests/server_tests.`);
24924
+ return external_path_default().relative(external_path_default().join(options.appRoot, "tests"), absolutePath).split(external_path_default().sep).join("/") + suffix;
24925
+ }
24926
+ async function checkCommitOnAnvil(git, options) {
24927
+ const notOnAnvil = ()=>new Error(`Commit ${options.commitId.slice(0, 7)} isn't on Anvil. Push ${options.branch} first.`);
24928
+ try {
24929
+ await git.fetch(options.fetchUrl, `refs/heads/${options.branch}`);
24930
+ } catch (error) {
24931
+ const message = errors_getErrorMessage(error);
24932
+ if (/couldn't find remote ref/i.test(message)) throw notOnAnvil();
24933
+ throw new Error(message.replace(/\/\/git:[^@\s]*@/g, "//git:***@"));
24934
+ }
24935
+ if (!await git.isAncestor(options.commitId, "FETCH_HEAD")) throw notOnAnvil();
24936
+ }
24937
+ async function getBranch(git) {
24938
+ try {
24939
+ return await git.getCurrentBranch();
24940
+ } catch {
24941
+ throw new Error("HEAD isn't on a branch. Check out the branch you've pushed to Anvil.");
24942
+ }
24943
+ }
24944
+ async function runTestCommand(targets, options) {
24945
+ const jsonMode = getGlobalOutputConfig().jsonMode;
24946
+ const controller = new AbortController();
24947
+ let runId;
24948
+ let interrupted = false;
24949
+ let outputEndsLine = true;
24950
+ const writeOutput = (text)=>{
24951
+ process.stdout.write(text);
24952
+ outputEndsLine = text.endsWith("\n");
24953
+ };
24954
+ const writeJsonResult = (...args)=>{
24955
+ if (!outputEndsLine) writeOutput("\n");
24956
+ logJsonResult(...args);
24957
+ };
24958
+ const onInterrupt = ()=>{
24959
+ if (interrupted) process.exit(130);
24960
+ interrupted = true;
24961
+ if (runId) process.stderr.write("\nCancelling test run...\n");
24962
+ controller.abort();
24963
+ };
24964
+ try {
24965
+ const projectRoot = process.cwd();
24966
+ const git = new GitService(projectRoot);
24967
+ let appRoot;
24968
+ try {
24969
+ appRoot = external_path_default().resolve(projectRoot, (await git.getGit().revparse([
24970
+ "--show-cdup"
24971
+ ])).trim());
24972
+ } catch {
24973
+ throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
24974
+ }
24975
+ const testTargets = targets.map((target)=>toServerTestTarget(target, {
24976
+ appRoot,
24977
+ cwd: projectRoot
24978
+ }));
24979
+ const selection = {
24980
+ ...testTargets.length > 0 ? {
24981
+ test_targets: testTargets
24982
+ } : {},
24983
+ ...void 0 !== options.k ? {
24984
+ keyword_expression: options.k
24985
+ } : {}
24986
+ };
24987
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24988
+ const branch = await getBranch(git);
24989
+ const commitId = await git.getCommitId();
24990
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
24991
+ await checkCommitOnAnvil(git, {
24992
+ fetchUrl: getGitFetchUrl(appId, authToken, anvilUrl),
24993
+ branch,
24994
+ commitId
24995
+ });
24996
+ if (await git.hasUncommittedChanges()) process.stderr.write(`You have uncommitted changes, which won't be included in the test run.\n`);
24997
+ process.on("SIGINT", onInterrupt);
24998
+ const { runId: completedRunId, state } = await runServerTests(appId, {
24999
+ commitId,
25000
+ branch,
25001
+ ...Object.keys(selection).length > 0 ? {
25002
+ selection
25003
+ } : {},
25004
+ collectOnly: options.collectOnly,
25005
+ anvilUrl,
25006
+ username,
25007
+ signal: controller.signal,
25008
+ onOutput: writeOutput,
25009
+ onLaunchMessage: (text)=>{
25010
+ process.stderr.write(text);
25011
+ },
25012
+ onRunCreated: (id)=>{
25013
+ runId = id;
25014
+ }
25015
+ });
25016
+ const exitCode = state.exit_code ? PYTEST_EXIT_CODES[state.exit_code] ?? 1 : 1;
25017
+ if (state.error?.message) process.stderr.write(`${state.error.message}\n`);
25018
+ else if (!state.exit_code) process.stderr.write("The test run ended without a result.\n");
25019
+ if (jsonMode) {
25020
+ let snapshot;
25021
+ try {
25022
+ snapshot = await getTestRunSnapshot(appId, completedRunId, {
25023
+ anvilUrl,
25024
+ username
25025
+ });
25026
+ } catch (error) {
25027
+ writeJsonResult(false, {
25028
+ error: errors_getErrorMessage(error)
25029
+ });
25030
+ process.exitCode = 1;
25031
+ return;
25032
+ }
25033
+ const success = "OK" === state.exit_code || !!options.collectOnly && "NO_TESTS_COLLECTED" === state.exit_code;
25034
+ writeJsonResult(success, {
25035
+ data: snapshot
25036
+ });
25037
+ }
25038
+ process.exitCode = exitCode;
25039
+ } catch (error) {
25040
+ const message = errors_getErrorMessage(error);
25041
+ if (jsonMode) writeJsonResult(false, {
25042
+ error: message
25043
+ });
25044
+ else process.stderr.write(`${message}\n`);
25045
+ process.exitCode = controller.signal.aborted ? 130 : error instanceof UsageError ? PYTEST_EXIT_CODES.USAGE_ERROR : 1;
25046
+ } finally{
25047
+ process.removeListener("SIGINT", onInterrupt);
25048
+ }
25049
+ }
25050
+ function registerTestCommand(program) {
25051
+ addAppAccountOptions(program.command("test")).description("Run the app's tests on Anvil, against the commit you have checked out").argument("[targets...]", "Test paths or pytest node IDs, relative to your current directory").option("-k <expression>", "Only run tests that match the pytest keyword expression").option("--collect-only", "List the tests without running them").addHelpText("after", `
25052
+ The commit you have checked out must already be pushed to Anvil.
25053
+ The exit code is the same as pytest: 0 if every test passed, 1 if any failed.
25054
+
25055
+ Examples:
25056
+ anvil test
25057
+ anvil test tests/server_tests/test_orders.py::test_refund
25058
+ anvil test -k "login and not slow"
25059
+ anvil --json test
25060
+ `).action(async (targets, options)=>{
25061
+ await runTestCommand(targets, options);
25062
+ });
25063
+ }
24032
25064
  const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
24033
25065
  const VERSION = program_packageJson.version;
24034
25066
  setLogger(new CLILogger({
@@ -24440,6 +25472,7 @@ Examples:
24440
25472
  });
24441
25473
  const watchCommand = registerWatchCommand(program);
24442
25474
  registerCheckoutCommand(program);
25475
+ registerCreateCommand(program);
24443
25476
  registerGitAuthCommand(program);
24444
25477
  registerGitCredentialCommand(program);
24445
25478
  registerLoginCommand(program);
@@ -24452,6 +25485,8 @@ Examples:
24452
25485
  registerDbCommand(program);
24453
25486
  registerEnvCommand(program);
24454
25487
  registerReplCommand(program);
25488
+ registerSecretCommand(program);
25489
+ registerTestCommand(program);
24455
25490
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
24456
25491
  await handleUpdateCommand();
24457
25492
  });