@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.
- package/README.md +54 -0
- package/dist/WatchSession.d.ts.map +1 -1
- package/dist/api.d.ts +3 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/cli.js +1251 -216
- package/dist/commands/appDetection.d.ts +68 -1
- package/dist/commands/appDetection.d.ts.map +1 -1
- package/dist/commands/checkout.d.ts +1 -0
- package/dist/commands/checkout.d.ts.map +1 -1
- package/dist/commands/create.d.ts +52 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/deps.d.ts.map +1 -1
- package/dist/commands/env.d.ts +3 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/repl.d.ts.map +1 -1
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/test.d.ts +26 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/watch.d.ts +1 -25
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/formTemplateValidation.d.ts.map +1 -1
- package/dist/index.js +225 -22
- package/dist/program.d.ts.map +1 -1
- package/dist/services/anvil-api.d.ts +20 -0
- package/dist/services/anvil-api.d.ts.map +1 -1
- package/dist/services/db.d.ts +2 -0
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/deps.d.ts +5 -2
- package/dist/services/deps.d.ts.map +1 -1
- package/dist/services/environment.d.ts +4 -0
- package/dist/services/environment.d.ts.map +1 -1
- package/dist/services/path-open.d.ts +1 -0
- package/dist/services/path-open.d.ts.map +1 -1
- package/dist/services/repl.d.ts +2 -0
- package/dist/services/repl.d.ts.map +1 -1
- package/dist/services/secret.d.ts +43 -0
- package/dist/services/secret.d.ts.map +1 -0
- package/dist/services/server-tests.d.ts +47 -0
- package/dist/services/server-tests.d.ts.map +1 -0
- package/dist/validateFormTemplateHtml.d.ts.map +1 -1
- 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
|
|
12879
|
-
if (
|
|
12880
|
-
if (
|
|
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(/(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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: "
|
|
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
|
|
21359
|
-
action: "
|
|
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
|
|
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
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
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
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
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
|
-
|
|
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("-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|