@anvil-works/anvil-cli 0.8.0-canary.9 → 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 +2 -0
- package/dist/WatchSession.d.ts.map +1 -1
- package/dist/api.d.ts +8 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/cli.js +2799 -1249
- 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/configure.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 +3 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/deps.d.ts.map +1 -1
- package/dist/commands/env.d.ts +10 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/index.d.ts +6 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/modelToken.d.ts +33 -0
- package/dist/commands/modelToken.d.ts.map +1 -0
- package/dist/commands/repl.d.ts +3 -0
- package/dist/commands/repl.d.ts.map +1 -0
- 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/errors.d.ts +11 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/formTemplateValidation.d.ts.map +1 -1
- package/dist/index.js +1466 -994
- 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/auth.d.ts +42 -3
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/db.d.ts +9 -0
- package/dist/services/db.d.ts.map +1 -0
- package/dist/services/deps.d.ts +5 -2
- package/dist/services/deps.d.ts.map +1 -1
- package/dist/services/environment.d.ts +20 -0
- package/dist/services/environment.d.ts.map +1 -0
- package/dist/services/git-auth.d.ts +35 -0
- package/dist/services/git-auth.d.ts.map +1 -1
- package/dist/services/git.d.ts +1 -0
- package/dist/services/git.d.ts.map +1 -1
- package/dist/services/http.d.ts +2 -0
- package/dist/services/http.d.ts.map +1 -0
- 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 +20 -0
- package/dist/services/repl.d.ts.map +1 -0
- 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/dist/validatePython.d.ts.map +1 -1
- package/dist/watch/SaveProcessor.d.ts +2 -0
- package/dist/watch/SaveProcessor.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/commands/tables.d.ts +0 -7
- package/dist/commands/tables.d.ts.map +0 -1
- package/dist/services/tables.d.ts +0 -27
- package/dist/services/tables.d.ts.map +0 -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 {
|
|
@@ -13384,9 +13398,10 @@ var __webpack_exports__ = {};
|
|
|
13384
13398
|
type: "auth_invalid",
|
|
13385
13399
|
message
|
|
13386
13400
|
}),
|
|
13387
|
-
refreshFailed: (message)=>({
|
|
13401
|
+
refreshFailed: (message, status)=>({
|
|
13388
13402
|
type: "token_refresh_failed",
|
|
13389
|
-
message
|
|
13403
|
+
message,
|
|
13404
|
+
status
|
|
13390
13405
|
})
|
|
13391
13406
|
};
|
|
13392
13407
|
const createAppError = {
|
|
@@ -13600,6 +13615,19 @@ var __webpack_exports__ = {};
|
|
|
13600
13615
|
return "An unknown error occurred";
|
|
13601
13616
|
}
|
|
13602
13617
|
}
|
|
13618
|
+
function getErrorCauseMessage(error) {
|
|
13619
|
+
if ("object" != typeof error || null === error || !("cause" in error)) return null;
|
|
13620
|
+
const cause = error.cause;
|
|
13621
|
+
if (null == cause) return null;
|
|
13622
|
+
const message = errors_getErrorMessage(cause);
|
|
13623
|
+
const code = "object" == typeof cause && null !== cause && "code" in cause && "string" == typeof cause.code ? cause.code : null;
|
|
13624
|
+
if (code && !message.includes(code)) return `${message} (${code})`;
|
|
13625
|
+
return message;
|
|
13626
|
+
}
|
|
13627
|
+
function logErrorCause(error, ui) {
|
|
13628
|
+
const causeMessage = getErrorCauseMessage(error);
|
|
13629
|
+
if (causeMessage) ui.verbose(`Cause: ${causeMessage}`);
|
|
13630
|
+
}
|
|
13603
13631
|
const external_module_namespaceObject = require("module");
|
|
13604
13632
|
const requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
|
|
13605
13633
|
const GIT_CREDENTIAL_HELPER_FILENAME = "anvil-credential-helper.cjs";
|
|
@@ -13820,6 +13848,76 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13820
13848
|
function getAuthFilePath(gitDir) {
|
|
13821
13849
|
return external_path_default().join(gitDir, "anvil-auth.json");
|
|
13822
13850
|
}
|
|
13851
|
+
const AUTH_FILE_LOCK_DEFAULTS = {
|
|
13852
|
+
staleMs: 30000,
|
|
13853
|
+
timeoutMs: 60000,
|
|
13854
|
+
retryDelayMs: 100,
|
|
13855
|
+
heartbeatMs: 5000
|
|
13856
|
+
};
|
|
13857
|
+
function git_auth_delay(ms) {
|
|
13858
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
13859
|
+
}
|
|
13860
|
+
async function acquireAuthFileLock(lockPath, options) {
|
|
13861
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
13862
|
+
while(true){
|
|
13863
|
+
try {
|
|
13864
|
+
const handle = await external_fs_.promises.open(lockPath, "wx", 384);
|
|
13865
|
+
try {
|
|
13866
|
+
await handle.writeFile(JSON.stringify({
|
|
13867
|
+
pid: process.pid,
|
|
13868
|
+
acquiredAt: new Date().toISOString()
|
|
13869
|
+
}));
|
|
13870
|
+
} finally{
|
|
13871
|
+
await handle.close();
|
|
13872
|
+
}
|
|
13873
|
+
return;
|
|
13874
|
+
} catch (e) {
|
|
13875
|
+
if ("EEXIST" !== e.code) throw e;
|
|
13876
|
+
}
|
|
13877
|
+
try {
|
|
13878
|
+
const lockStat = await external_fs_.promises.stat(lockPath);
|
|
13879
|
+
if (Date.now() - lockStat.mtimeMs > options.staleMs) {
|
|
13880
|
+
await external_fs_.promises.rm(lockPath, {
|
|
13881
|
+
force: true
|
|
13882
|
+
});
|
|
13883
|
+
continue;
|
|
13884
|
+
}
|
|
13885
|
+
} catch {
|
|
13886
|
+
continue;
|
|
13887
|
+
}
|
|
13888
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the Anvil auth file lock at ${lockPath}`);
|
|
13889
|
+
await git_auth_delay(options.retryDelayMs);
|
|
13890
|
+
}
|
|
13891
|
+
}
|
|
13892
|
+
async function withRepoAuthFileLock(repoPath, fn, options) {
|
|
13893
|
+
const gitDir = await getRepositoryGitDir(repoPath);
|
|
13894
|
+
const lockPath = `${getAuthFilePath(gitDir)}.lock`;
|
|
13895
|
+
const lockOptions = {
|
|
13896
|
+
...AUTH_FILE_LOCK_DEFAULTS,
|
|
13897
|
+
...options
|
|
13898
|
+
};
|
|
13899
|
+
await external_fs_.promises.mkdir(external_path_default().dirname(lockPath), {
|
|
13900
|
+
recursive: true
|
|
13901
|
+
});
|
|
13902
|
+
await acquireAuthFileLock(lockPath, lockOptions);
|
|
13903
|
+
const heartbeat = startAuthFileLockHeartbeat(lockPath, lockOptions.heartbeatMs);
|
|
13904
|
+
try {
|
|
13905
|
+
return await fn();
|
|
13906
|
+
} finally{
|
|
13907
|
+
clearInterval(heartbeat);
|
|
13908
|
+
await external_fs_.promises.rm(lockPath, {
|
|
13909
|
+
force: true
|
|
13910
|
+
});
|
|
13911
|
+
}
|
|
13912
|
+
}
|
|
13913
|
+
function startAuthFileLockHeartbeat(lockPath, heartbeatMs) {
|
|
13914
|
+
const heartbeat = setInterval(()=>{
|
|
13915
|
+
const now = new Date();
|
|
13916
|
+
external_fs_.promises.utimes(lockPath, now, now).catch(()=>{});
|
|
13917
|
+
}, heartbeatMs);
|
|
13918
|
+
heartbeat.unref();
|
|
13919
|
+
return heartbeat;
|
|
13920
|
+
}
|
|
13823
13921
|
function decodeKey(b64key) {
|
|
13824
13922
|
const key = Buffer.from(b64key, "base64");
|
|
13825
13923
|
if (32 !== key.length) throw new Error("Encryption key must be a base64-encoded 32-byte key");
|
|
@@ -13864,10 +13962,19 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13864
13962
|
recursive: true
|
|
13865
13963
|
});
|
|
13866
13964
|
const authJson = JSON.stringify(auth, null, 2);
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13965
|
+
const tempFilePath = `${authFilePath}.tmp-${external_node_crypto_default().randomBytes(6).toString("hex")}`;
|
|
13966
|
+
try {
|
|
13967
|
+
await external_fs_.promises.writeFile(tempFilePath, encrypt(authJson, encryptionKey), {
|
|
13968
|
+
mode: 384
|
|
13969
|
+
});
|
|
13970
|
+
await external_fs_.promises.chmod(tempFilePath, 448);
|
|
13971
|
+
await external_fs_.promises.rename(tempFilePath, authFilePath);
|
|
13972
|
+
} catch (e) {
|
|
13973
|
+
await external_fs_.promises.rm(tempFilePath, {
|
|
13974
|
+
force: true
|
|
13975
|
+
});
|
|
13976
|
+
throw e;
|
|
13977
|
+
}
|
|
13871
13978
|
await git.raw([
|
|
13872
13979
|
"config",
|
|
13873
13980
|
"--local",
|
|
@@ -13876,8 +13983,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13876
13983
|
]);
|
|
13877
13984
|
return authFilePath;
|
|
13878
13985
|
}
|
|
13879
|
-
const
|
|
13986
|
+
const git_auth_readAuthFromRepo = async (repoPath)=>{
|
|
13880
13987
|
const git = esm_default(repoPath);
|
|
13988
|
+
if (!await git.checkIsRepo()) return;
|
|
13881
13989
|
const gitDir = await getRepositoryGitDir(repoPath, git);
|
|
13882
13990
|
const authFilePath = getAuthFilePath(gitDir);
|
|
13883
13991
|
if (!external_fs_default().existsSync(authFilePath)) return;
|
|
@@ -13889,14 +13997,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13889
13997
|
return resolveAnvilUrl();
|
|
13890
13998
|
}
|
|
13891
13999
|
const ANVIL_SYNC_CLIENT_ID = "anvil-sync";
|
|
14000
|
+
const REFRESH_REQUEST_TIMEOUT_MS = 12000;
|
|
13892
14001
|
let inMemoryAuth = null;
|
|
13893
14002
|
function authMatches(auth, anvilUrl, username) {
|
|
13894
14003
|
return auth.anvilUrl === normalizeAnvilUrl(anvilUrl) && (!username || auth.username === username);
|
|
13895
14004
|
}
|
|
13896
14005
|
let repoContext = null;
|
|
13897
|
-
function
|
|
14006
|
+
function auth_setRepoContext(repoPath) {
|
|
13898
14007
|
repoContext = repoPath;
|
|
13899
14008
|
}
|
|
14009
|
+
async function resolveAuthAnvilUrl() {
|
|
14010
|
+
if (inMemoryAuth) return inMemoryAuth.anvilUrl;
|
|
14011
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
14012
|
+
return repoAuth?.anvilUrl ?? resolveAnvilUrl();
|
|
14013
|
+
}
|
|
13900
14014
|
const inFlightRefreshes = new Map();
|
|
13901
14015
|
async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
|
|
13902
14016
|
try {
|
|
@@ -13917,7 +14031,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13917
14031
|
throw errors_createAuthError.invalid(`Network error: ${e.message}`);
|
|
13918
14032
|
}
|
|
13919
14033
|
}
|
|
13920
|
-
async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID) {
|
|
14034
|
+
async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID, options) {
|
|
13921
14035
|
try {
|
|
13922
14036
|
const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
|
|
13923
14037
|
method: "POST",
|
|
@@ -13928,11 +14042,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13928
14042
|
grant_type: "refresh_token",
|
|
13929
14043
|
refresh_token: refreshToken,
|
|
13930
14044
|
client_id: clientId
|
|
13931
|
-
})
|
|
14045
|
+
}),
|
|
14046
|
+
signal: AbortSignal.timeout(options?.timeoutMs ?? REFRESH_REQUEST_TIMEOUT_MS)
|
|
13932
14047
|
});
|
|
13933
14048
|
if (!tokenResponse.ok) {
|
|
13934
14049
|
const errorText = await tokenResponse.text();
|
|
13935
|
-
throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}
|
|
14050
|
+
throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`, tokenResponse.status);
|
|
13936
14051
|
}
|
|
13937
14052
|
const tokenData = await tokenResponse.json();
|
|
13938
14053
|
return tokenData;
|
|
@@ -13944,22 +14059,21 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13944
14059
|
async function hasTokensForUrl(anvilUrl, username) {
|
|
13945
14060
|
if (inMemoryAuth) return authMatches(inMemoryAuth, anvilUrl, username);
|
|
13946
14061
|
{
|
|
13947
|
-
const repoAuth = repoContext ? await
|
|
14062
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
13948
14063
|
if (repoAuth && authMatches(repoAuth, anvilUrl, username)) return true;
|
|
13949
14064
|
return hasTokens(anvilUrl, username);
|
|
13950
14065
|
}
|
|
13951
14066
|
}
|
|
13952
|
-
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
|
|
14067
|
+
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username, options) {
|
|
13953
14068
|
const normalized = normalizeAnvilUrl(anvilUrl);
|
|
13954
14069
|
const source = await resolveTokenSource(normalized, username);
|
|
13955
14070
|
const tokens = getSourceTokens(source);
|
|
13956
14071
|
if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
|
|
13957
|
-
|
|
13958
|
-
if (tokens.refreshToken && isExpired) {
|
|
14072
|
+
if (tokens.refreshToken && isAccessTokenExpired(tokens)) {
|
|
13959
14073
|
const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
|
|
13960
14074
|
const existingRefresh = inFlightRefreshes.get(refreshKey);
|
|
13961
14075
|
if (existingRefresh) return existingRefresh;
|
|
13962
|
-
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
|
|
14076
|
+
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken, options).finally(()=>{
|
|
13963
14077
|
inFlightRefreshes.delete(refreshKey);
|
|
13964
14078
|
});
|
|
13965
14079
|
inFlightRefreshes.set(refreshKey, refreshPromise);
|
|
@@ -13973,7 +14087,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13973
14087
|
type: "memory",
|
|
13974
14088
|
auth: inMemoryAuth
|
|
13975
14089
|
};
|
|
13976
|
-
const repoAuth = repoContext ? await
|
|
14090
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
13977
14091
|
if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
|
|
13978
14092
|
type: "repo",
|
|
13979
14093
|
auth: repoAuth,
|
|
@@ -13989,6 +14103,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13989
14103
|
function getSourceTokens(source) {
|
|
13990
14104
|
return "store" === source.type ? source.tokens : source.auth.tokens;
|
|
13991
14105
|
}
|
|
14106
|
+
function isAccessTokenExpired(tokens) {
|
|
14107
|
+
return null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
|
|
14108
|
+
}
|
|
13992
14109
|
function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
|
|
13993
14110
|
if (username) return username;
|
|
13994
14111
|
const urlTokens = getUrlTokens(normalizedUrl);
|
|
@@ -14007,11 +14124,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14007
14124
|
return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
|
|
14008
14125
|
}
|
|
14009
14126
|
}
|
|
14010
|
-
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
|
|
14127
|
+
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken, options) {
|
|
14128
|
+
if ("repo" === source.type) return refreshRepoAuthTokens(source.repoPath, {
|
|
14129
|
+
anvilUrl: normalizedUrl,
|
|
14130
|
+
username,
|
|
14131
|
+
auth: source.auth,
|
|
14132
|
+
refreshToken,
|
|
14133
|
+
lock: options?.lock,
|
|
14134
|
+
requestTimeoutMs: options?.requestTimeoutMs
|
|
14135
|
+
});
|
|
14136
|
+
return performTokenRefresh(normalizedUrl, username, source, refreshToken, options?.requestTimeoutMs);
|
|
14137
|
+
}
|
|
14138
|
+
async function refreshRepoAuthTokens(repoPath, options) {
|
|
14139
|
+
const normalizedUrl = normalizeAnvilUrl(options.anvilUrl);
|
|
14140
|
+
return withRepoAuthFileLock(repoPath, async ()=>{
|
|
14141
|
+
let auth = options.auth;
|
|
14142
|
+
let refreshToken = options.refreshToken;
|
|
14143
|
+
const current = await git_auth_readAuthFromRepo(repoPath);
|
|
14144
|
+
if (current && authMatches(current, normalizedUrl, options.username)) {
|
|
14145
|
+
if (current.tokens.authToken && !isAccessTokenExpired(current.tokens)) return current.tokens.authToken;
|
|
14146
|
+
auth = current;
|
|
14147
|
+
refreshToken = current.tokens.refreshToken ?? refreshToken;
|
|
14148
|
+
}
|
|
14149
|
+
if (!auth || !refreshToken) throw errors_createAuthError.required("No repo auth tokens available to refresh.");
|
|
14150
|
+
return performTokenRefresh(normalizedUrl, options.username, {
|
|
14151
|
+
type: "repo",
|
|
14152
|
+
auth,
|
|
14153
|
+
repoPath
|
|
14154
|
+
}, refreshToken, options.requestTimeoutMs);
|
|
14155
|
+
}, options.lock);
|
|
14156
|
+
}
|
|
14157
|
+
async function performTokenRefresh(normalizedUrl, username, source, refreshToken, requestTimeoutMs) {
|
|
14011
14158
|
try {
|
|
14012
14159
|
const tokens = getSourceTokens(source);
|
|
14013
14160
|
const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
|
|
14014
|
-
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId
|
|
14161
|
+
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId, {
|
|
14162
|
+
timeoutMs: requestTimeoutMs
|
|
14163
|
+
});
|
|
14015
14164
|
const newTokens = {
|
|
14016
14165
|
authToken: tokenData.access_token,
|
|
14017
14166
|
refreshToken: tokenData.refresh_token,
|
|
@@ -14033,12 +14182,28 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14033
14182
|
}
|
|
14034
14183
|
return tokenData.access_token;
|
|
14035
14184
|
} catch (e) {
|
|
14185
|
+
if ("repo" === source.type && isRefreshTokenRejection(e)) await clearRepoAuthTokens(source.repoPath, source.auth).catch(()=>{});
|
|
14036
14186
|
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
14037
14187
|
throw e;
|
|
14038
14188
|
}
|
|
14039
14189
|
}
|
|
14190
|
+
function isRefreshTokenRejection(e) {
|
|
14191
|
+
const error = e;
|
|
14192
|
+
return error?.type === "token_refresh_failed" && (400 === error.status || 401 === error.status);
|
|
14193
|
+
}
|
|
14194
|
+
async function clearRepoAuthTokens(repoPath, auth) {
|
|
14195
|
+
const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
|
|
14196
|
+
if (!encryptionKey) return;
|
|
14197
|
+
auth.tokens = {
|
|
14198
|
+
authToken: null,
|
|
14199
|
+
refreshToken: null,
|
|
14200
|
+
authTokenExpiresAt: null,
|
|
14201
|
+
clientId: auth.tokens.clientId
|
|
14202
|
+
};
|
|
14203
|
+
await writeAuthToFile(repoPath, auth, encryptionKey);
|
|
14204
|
+
}
|
|
14040
14205
|
async function clearTokensForAccount(url, username, refreshToken) {
|
|
14041
|
-
if (inMemoryAuth || repoContext && await
|
|
14206
|
+
if (inMemoryAuth || repoContext && await git_auth_readAuthFromRepo(repoContext)) return;
|
|
14042
14207
|
{
|
|
14043
14208
|
const normalized = normalizeAnvilUrl(url);
|
|
14044
14209
|
if (username) deleteAccountTokens(normalized, username);
|
|
@@ -14068,7 +14233,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14068
14233
|
loggedOut: false,
|
|
14069
14234
|
message: "Cannot log out when using in-memory auth tokens"
|
|
14070
14235
|
};
|
|
14071
|
-
if (repoContext && await
|
|
14236
|
+
if (repoContext && await git_auth_readAuthFromRepo(repoContext)) return {
|
|
14072
14237
|
loggedOut: false,
|
|
14073
14238
|
message: "Cannot log out when using auth tokens stored in the local git config."
|
|
14074
14239
|
};
|
|
@@ -14099,7 +14264,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14099
14264
|
inMemoryAuth.username
|
|
14100
14265
|
] : [];
|
|
14101
14266
|
{
|
|
14102
|
-
const repoAuth = repoContext ? await
|
|
14267
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
14103
14268
|
if (repoAuth && authMatches(repoAuth, url)) return [
|
|
14104
14269
|
repoAuth.username
|
|
14105
14270
|
];
|
|
@@ -14111,634 +14276,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14111
14276
|
inMemoryAuth.anvilUrl
|
|
14112
14277
|
];
|
|
14113
14278
|
{
|
|
14114
|
-
const repoAuth = repoContext ? await
|
|
14279
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
14115
14280
|
if (repoAuth) return [
|
|
14116
14281
|
repoAuth.anvilUrl
|
|
14117
14282
|
];
|
|
14118
14283
|
return getAvailableUrls();
|
|
14119
14284
|
}
|
|
14120
14285
|
}
|
|
14121
|
-
|
|
14122
|
-
|
|
14286
|
+
const promises_namespaceObject = require("timers/promises");
|
|
14287
|
+
const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
|
|
14288
|
+
const FETCH_RETRY_DELAY_MS = 200;
|
|
14289
|
+
function isWantNotValidError(error) {
|
|
14290
|
+
return WANT_NOT_VALID_RE.test(error.message || "");
|
|
14123
14291
|
}
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
const data = await resp.json();
|
|
14138
|
-
if (resp.ok) return {
|
|
14139
|
-
valid: data.valid ?? false,
|
|
14140
|
-
app_name: data.app_name
|
|
14141
|
-
};
|
|
14142
|
-
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14143
|
-
if (403 === resp.status) throw createAppError.accessDenied(appId);
|
|
14144
|
-
if (404 === resp.status) throw createAppError.notFound(appId);
|
|
14145
|
-
logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
|
|
14146
|
-
return {
|
|
14147
|
-
valid: false,
|
|
14148
|
-
error: data.error || `Server error: ${resp.status}`
|
|
14149
|
-
};
|
|
14150
|
-
} catch (error) {
|
|
14151
|
-
if (error.type) throw error;
|
|
14152
|
-
throw errors_createNetworkError.network(error.message);
|
|
14292
|
+
class GitService extends Emitter {
|
|
14293
|
+
git;
|
|
14294
|
+
repoPath;
|
|
14295
|
+
static INDEX_LOCK_RETRY_DELAYS_MS = [
|
|
14296
|
+
25,
|
|
14297
|
+
50,
|
|
14298
|
+
100,
|
|
14299
|
+
200
|
|
14300
|
+
];
|
|
14301
|
+
constructor(repoPath){
|
|
14302
|
+
super();
|
|
14303
|
+
this.repoPath = external_path_default().resolve(repoPath);
|
|
14304
|
+
this.git = esm_default(this.repoPath);
|
|
14153
14305
|
}
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
|
|
14157
|
-
const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
14158
|
-
const params = new URLSearchParams();
|
|
14159
|
-
if ("number" == typeof options.limit) params.set("limit", String(options.limit));
|
|
14160
|
-
if (options.cursor) params.set("cursor", options.cursor);
|
|
14161
|
-
if (options.q && options.q.trim()) params.set("q", options.q.trim());
|
|
14162
|
-
const query = params.toString();
|
|
14163
|
-
const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
|
|
14164
|
-
try {
|
|
14165
|
-
const resp = await fetch(url, {
|
|
14166
|
-
method: "GET",
|
|
14167
|
-
headers: {
|
|
14168
|
-
Authorization: `Bearer ${authToken}`
|
|
14169
|
-
},
|
|
14170
|
-
signal: options.signal
|
|
14171
|
-
});
|
|
14172
|
-
if (!resp.ok) {
|
|
14173
|
-
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14174
|
-
throw errors_createNetworkError.server(resp.status, resp.statusText);
|
|
14175
|
-
}
|
|
14176
|
-
const data = await resp.json();
|
|
14177
|
-
return {
|
|
14178
|
-
apps: Array.isArray(data.apps) ? data.apps : [],
|
|
14179
|
-
next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
|
|
14180
|
-
};
|
|
14181
|
-
} catch (error) {
|
|
14182
|
-
if (error.type) throw error;
|
|
14183
|
-
throw errors_createNetworkError.network(error.message);
|
|
14306
|
+
getGit() {
|
|
14307
|
+
return this.git;
|
|
14184
14308
|
}
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
const url = new URL(gitUrl);
|
|
14188
|
-
const encodedToken = encodeURIComponent(authToken);
|
|
14189
|
-
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
|
|
14190
|
-
}
|
|
14191
|
-
function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14192
|
-
const url = new URL(anvilUrl);
|
|
14193
|
-
const encodedToken = encodeURIComponent(authToken);
|
|
14194
|
-
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
|
|
14195
|
-
}
|
|
14196
|
-
function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14197
|
-
return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
|
|
14198
|
-
}
|
|
14199
|
-
async function getLatestVersion() {
|
|
14200
|
-
try {
|
|
14201
|
-
const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
|
|
14202
|
-
if (!response.ok) return null;
|
|
14203
|
-
const data = await response.json();
|
|
14204
|
-
return data.version;
|
|
14205
|
-
} catch (e) {
|
|
14206
|
-
return null;
|
|
14309
|
+
getRepoPath() {
|
|
14310
|
+
return this.repoPath;
|
|
14207
14311
|
}
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14312
|
+
async getGitDir() {
|
|
14313
|
+
try {
|
|
14314
|
+
const gitDir = (await this.git.revparse([
|
|
14315
|
+
"--git-dir"
|
|
14316
|
+
])).trim();
|
|
14317
|
+
if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
|
|
14318
|
+
return gitDir;
|
|
14319
|
+
} catch (e) {
|
|
14320
|
+
throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
|
|
14321
|
+
}
|
|
14214
14322
|
}
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
}
|
|
14227
|
-
function lookupRemoteInfoForAppId(appId, detectedRemotes) {
|
|
14228
|
-
const matches = detectedRemotes.filter((c)=>c.appId === appId);
|
|
14229
|
-
if (0 === matches.length) return {};
|
|
14230
|
-
const withUsername = matches.find((c)=>c.detectedUsername);
|
|
14231
|
-
if (withUsername) return {
|
|
14232
|
-
detectedUrl: withUsername.detectedUrl,
|
|
14233
|
-
detectedUsername: withUsername.detectedUsername
|
|
14234
|
-
};
|
|
14235
|
-
return {
|
|
14236
|
-
detectedUrl: matches[0].detectedUrl,
|
|
14237
|
-
detectedUsername: matches[0].detectedUsername
|
|
14238
|
-
};
|
|
14239
|
-
}
|
|
14240
|
-
async function detectAppIdsFromAllRemotes(repoPath) {
|
|
14241
|
-
const git = esm_default(repoPath);
|
|
14242
|
-
const out = [];
|
|
14243
|
-
try {
|
|
14244
|
-
const remotes = await git.getRemotes(true);
|
|
14245
|
-
for (const remote of remotes){
|
|
14246
|
-
const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
|
|
14247
|
-
if (httpMatch) {
|
|
14248
|
-
const [, host, detectedAppId] = httpMatch;
|
|
14249
|
-
out.push({
|
|
14250
|
-
appId: detectedAppId,
|
|
14251
|
-
source: "remote",
|
|
14252
|
-
description: `Git remote '${remote.name}'`,
|
|
14253
|
-
detectedUrl: normalizeAnvilUrl(host)
|
|
14254
|
-
});
|
|
14255
|
-
continue;
|
|
14256
|
-
}
|
|
14257
|
-
const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
|
|
14258
|
-
if (sshMatch) {
|
|
14259
|
-
const [, usernamePart, host, , detectedAppId] = sshMatch;
|
|
14260
|
-
const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
|
|
14261
|
-
const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
|
|
14262
|
-
out.push({
|
|
14263
|
-
appId: detectedAppId,
|
|
14264
|
-
source: "remote",
|
|
14265
|
-
description: `Git remote '${remote.name}' (SSH)`,
|
|
14266
|
-
detectedUrl: normalizeAnvilUrl(host),
|
|
14267
|
-
detectedUsername: finalUsername
|
|
14268
|
-
});
|
|
14269
|
-
}
|
|
14270
|
-
}
|
|
14271
|
-
} catch (_e) {}
|
|
14272
|
-
return out;
|
|
14273
|
-
}
|
|
14274
|
-
async function detectAppIdsByCommitLookup(repoPath, options) {
|
|
14275
|
-
const anvilUrl = options.anvilUrl || resolveAnvilUrl();
|
|
14276
|
-
const username = options.username;
|
|
14277
|
-
const git = esm_default(repoPath);
|
|
14278
|
-
const out = [];
|
|
14279
|
-
try {
|
|
14280
|
-
const authToken = await auth_getValidAuthToken(anvilUrl, username);
|
|
14281
|
-
const branchRef = await git.revparse([
|
|
14282
|
-
"--abbrev-ref",
|
|
14283
|
-
"HEAD"
|
|
14284
|
-
]);
|
|
14285
|
-
const currentBranch = "HEAD" === branchRef ? "master" : branchRef;
|
|
14286
|
-
const commitId = (await git.revparse([
|
|
14287
|
-
"HEAD"
|
|
14288
|
-
]))?.trim();
|
|
14289
|
-
if (!commitId) return out;
|
|
14290
|
-
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
|
|
14291
|
-
method: "POST",
|
|
14292
|
-
headers: {
|
|
14293
|
-
Authorization: `Bearer ${authToken}`,
|
|
14294
|
-
"Content-Type": "application/json"
|
|
14295
|
-
},
|
|
14296
|
-
body: JSON.stringify({
|
|
14297
|
-
commit_id: commitId,
|
|
14298
|
-
branch_name: currentBranch
|
|
14299
|
-
})
|
|
14300
|
-
});
|
|
14301
|
-
logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${currentBranch} returned ${resp.status}`);
|
|
14302
|
-
if (!resp.ok) {
|
|
14303
|
-
const errorBody = await resp.text();
|
|
14304
|
-
logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
|
|
14305
|
-
}
|
|
14306
|
-
if (resp.ok) {
|
|
14307
|
-
const data = await resp.json();
|
|
14308
|
-
logger_logger.debug(`data ${JSON.stringify(data)}`);
|
|
14309
|
-
const apps = data?.apps ?? [];
|
|
14310
|
-
for (const app of apps)if (app?.app_id) out.push({
|
|
14311
|
-
appId: app.app_id,
|
|
14312
|
-
source: "remote",
|
|
14313
|
-
description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
|
|
14314
|
-
});
|
|
14315
|
-
}
|
|
14316
|
-
} catch (_e) {}
|
|
14317
|
-
return out;
|
|
14318
|
-
}
|
|
14319
|
-
const promises_namespaceObject = require("fs/promises");
|
|
14320
|
-
var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
|
|
14321
|
-
function normalizeLineEndings(content) {
|
|
14322
|
-
return content.replace(/\r\n/g, "\n");
|
|
14323
|
-
}
|
|
14324
|
-
function pythonifyName(name) {
|
|
14325
|
-
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
14326
|
-
}
|
|
14327
|
-
function extractPythonName(relativePath, skipParts = 1) {
|
|
14328
|
-
const parts = relativePath.slice(0, -3).split("/");
|
|
14329
|
-
const isPackage = relativePath.endsWith("__init__.py");
|
|
14330
|
-
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
14331
|
-
return relevantParts.map(pythonifyName).join(".");
|
|
14332
|
-
}
|
|
14333
|
-
function deepEqual(a, b) {
|
|
14334
|
-
if (a === b) return true;
|
|
14335
|
-
if (null == a || null == b) return false;
|
|
14336
|
-
if (typeof a !== typeof b) return false;
|
|
14337
|
-
if ("object" != typeof a) return a === b;
|
|
14338
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
14339
|
-
if (Array.isArray(a)) {
|
|
14340
|
-
if (a.length !== b.length) return false;
|
|
14341
|
-
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14342
|
-
}
|
|
14343
|
-
const keysA = Object.keys(a);
|
|
14344
|
-
const keysB = Object.keys(b);
|
|
14345
|
-
if (keysA.length !== keysB.length) return false;
|
|
14346
|
-
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
14347
|
-
}
|
|
14348
|
-
function parseHtmlWithFrontmatter(htmlContent) {
|
|
14349
|
-
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14350
|
-
const match = htmlContent.match(frontmatterPattern);
|
|
14351
|
-
if (match) {
|
|
14352
|
-
const yamlStr = match[1];
|
|
14353
|
-
const html = match[2];
|
|
14354
|
-
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
14355
|
-
return {
|
|
14356
|
-
frontmatter,
|
|
14357
|
-
html
|
|
14358
|
-
};
|
|
14359
|
-
}
|
|
14360
|
-
return {
|
|
14361
|
-
frontmatter: {},
|
|
14362
|
-
html: htmlContent
|
|
14363
|
-
};
|
|
14364
|
-
}
|
|
14365
|
-
const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
|
|
14366
|
-
async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
|
|
14367
|
-
const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
|
|
14368
|
-
try {
|
|
14369
|
-
const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
|
|
14370
|
-
if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
|
|
14371
|
-
const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
|
|
14372
|
-
await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
14373
|
-
return true;
|
|
14374
|
-
} catch (error) {
|
|
14375
|
-
if ("ENOENT" !== error.code) throw error;
|
|
14376
|
-
await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
14377
|
-
return true;
|
|
14378
|
-
}
|
|
14379
|
-
}
|
|
14380
|
-
const ANVIL_DEPS_DIR = ".anvil/deps";
|
|
14381
|
-
const ANVIL_DEPS_MANIFEST = "manifest.json";
|
|
14382
|
-
const DEPS_MANIFEST_VERSION = 2;
|
|
14383
|
-
const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
|
|
14384
|
-
function formatFetchError(prefix, status, errorBody) {
|
|
14385
|
-
if (!errorBody) return `${prefix}: ${status}`;
|
|
14386
|
-
try {
|
|
14387
|
-
const parsed = JSON.parse(errorBody);
|
|
14388
|
-
if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
|
|
14389
|
-
} catch {}
|
|
14390
|
-
return `${prefix}: ${status} ${errorBody}`;
|
|
14391
|
-
}
|
|
14392
|
-
async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
|
|
14393
|
-
const { commit, includeDocs = true, knownDependencies = [] } = options;
|
|
14394
|
-
const token = await auth_getValidAuthToken(anvilUrl);
|
|
14395
|
-
const body = {
|
|
14396
|
-
include_docs: includeDocs,
|
|
14397
|
-
known_dependencies: knownDependencies
|
|
14398
|
-
};
|
|
14399
|
-
if (commit) body.commit = commit;
|
|
14400
|
-
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
|
|
14401
|
-
method: "POST",
|
|
14402
|
-
headers: {
|
|
14403
|
-
Authorization: `Bearer ${token}`,
|
|
14404
|
-
"Content-Type": "application/json"
|
|
14405
|
-
},
|
|
14406
|
-
body: JSON.stringify(body)
|
|
14407
|
-
});
|
|
14408
|
-
if (!resp.ok) {
|
|
14409
|
-
const errorBody = await resp.text();
|
|
14410
|
-
throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
|
|
14411
|
-
}
|
|
14412
|
-
return await resp.json();
|
|
14413
|
-
}
|
|
14414
|
-
async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
|
|
14415
|
-
const includeDocs = options.includeDocs ?? true;
|
|
14416
|
-
const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
|
|
14417
|
-
const response = await fetchAllDependencies(appId, anvilUrl, {
|
|
14418
|
-
commit: options.commit,
|
|
14419
|
-
includeDocs,
|
|
14420
|
-
knownDependencies
|
|
14421
|
-
});
|
|
14422
|
-
if (!response.resolved_dependencies.length) {
|
|
14423
|
-
await clearDepsCache(projectRoot);
|
|
14424
|
-
return {
|
|
14425
|
-
response,
|
|
14426
|
-
addedGitignoreEntry: false
|
|
14427
|
-
};
|
|
14428
|
-
}
|
|
14429
|
-
const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
|
|
14430
|
-
await writeAllDepsToCache(projectRoot, response, {
|
|
14431
|
-
includeDocs
|
|
14432
|
-
});
|
|
14433
|
-
return {
|
|
14434
|
-
response,
|
|
14435
|
-
addedGitignoreEntry
|
|
14436
|
-
};
|
|
14437
|
-
}
|
|
14438
|
-
function depsDir(projectRoot) {
|
|
14439
|
-
return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
|
|
14440
|
-
}
|
|
14441
|
-
function depsManifestPath(projectRoot) {
|
|
14442
|
-
return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
|
|
14443
|
-
}
|
|
14444
|
-
function isResolvedDependencyManifestEntry(value) {
|
|
14445
|
-
if (!value || "object" != typeof value) return false;
|
|
14446
|
-
const entry = value;
|
|
14447
|
-
return "string" == typeof entry.app_id && entry.app_id.length > 0 && "string" == typeof entry.commit_id && entry.commit_id.length > 0 && (void 0 === entry.branch || "string" == typeof entry.branch);
|
|
14448
|
-
}
|
|
14449
|
-
function isCachedDependencyManifestEntry(value) {
|
|
14450
|
-
if (!isResolvedDependencyManifestEntry(value)) return false;
|
|
14451
|
-
const entry = value;
|
|
14452
|
-
return "string" == typeof entry.package_name && entry.package_name.length > 0;
|
|
14453
|
-
}
|
|
14454
|
-
function isDepsCacheManifest(value) {
|
|
14455
|
-
if (!value || "object" != typeof value) return false;
|
|
14456
|
-
const manifest = value;
|
|
14457
|
-
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);
|
|
14458
|
-
}
|
|
14459
|
-
async function readDepsCacheManifest(projectRoot) {
|
|
14460
|
-
try {
|
|
14461
|
-
const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
|
|
14462
|
-
const parsed = JSON.parse(raw);
|
|
14463
|
-
return isDepsCacheManifest(parsed) ? parsed : void 0;
|
|
14464
|
-
} catch {
|
|
14465
|
-
return;
|
|
14466
|
-
}
|
|
14467
|
-
}
|
|
14468
|
-
async function getKnownDependenciesForCache(projectRoot, includeDocs) {
|
|
14469
|
-
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14470
|
-
if (!manifest || manifest.includeDocs !== includeDocs) return [];
|
|
14471
|
-
return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
|
|
14472
|
-
app_id,
|
|
14473
|
-
commit_id
|
|
14474
|
-
}));
|
|
14475
|
-
}
|
|
14476
|
-
function getDependencyWatchSubscriptionsFromManifest(manifest) {
|
|
14477
|
-
if (!manifest) return [];
|
|
14478
|
-
const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
|
|
14479
|
-
dep.app_id,
|
|
14480
|
-
dep
|
|
14481
|
-
]));
|
|
14482
|
-
return manifest.resolved_dependencies.flatMap((dep)=>{
|
|
14483
|
-
if (!dep.branch) return [];
|
|
14484
|
-
return [
|
|
14485
|
-
{
|
|
14486
|
-
appId: dep.app_id,
|
|
14487
|
-
branch: dep.branch,
|
|
14488
|
-
packageName: cachedByAppId.get(dep.app_id)?.package_name
|
|
14489
|
-
}
|
|
14490
|
-
];
|
|
14491
|
-
});
|
|
14492
|
-
}
|
|
14493
|
-
function normalizeAnvilYamlDependenciesFromContent(content) {
|
|
14494
|
-
const parsed = external_js_yaml_default().load(content);
|
|
14495
|
-
if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
|
|
14496
|
-
return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
|
|
14497
|
-
}
|
|
14498
|
-
function normalizeAnvilYamlDependenciesValue(value) {
|
|
14499
|
-
if (!Array.isArray(value)) return [];
|
|
14500
|
-
return value.map(normalizeForComparison);
|
|
14501
|
-
}
|
|
14502
|
-
async function readAnvilYamlDependencies(projectRoot) {
|
|
14503
|
-
try {
|
|
14504
|
-
const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
|
|
14505
|
-
return normalizeAnvilYamlDependenciesFromContent(content);
|
|
14506
|
-
} catch {
|
|
14507
|
-
return [];
|
|
14508
|
-
}
|
|
14509
|
-
}
|
|
14510
|
-
async function getDependencyCacheWarning(projectRoot) {
|
|
14511
|
-
const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
|
|
14512
|
-
if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
|
|
14513
|
-
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14514
|
-
if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
|
|
14515
|
-
if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
|
|
14516
|
-
const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
|
|
14517
|
-
dependency.app_id,
|
|
14518
|
-
dependency
|
|
14519
|
-
]));
|
|
14520
|
-
for (const dependency of manifest.resolved_dependencies){
|
|
14521
|
-
const cached = cachedByAppId.get(dependency.app_id);
|
|
14522
|
-
if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
|
|
14523
|
-
try {
|
|
14524
|
-
await promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
|
|
14525
|
-
} catch {
|
|
14526
|
-
return DEPENDENCY_CACHE_WARNING;
|
|
14527
|
-
}
|
|
14528
|
-
}
|
|
14529
|
-
}
|
|
14530
|
-
function anvilYamlDependenciesEqual(left, right) {
|
|
14531
|
-
return deepEqual(left, right);
|
|
14532
|
-
}
|
|
14533
|
-
function normalizeForComparison(value) {
|
|
14534
|
-
if (Array.isArray(value)) return value.map(normalizeForComparison);
|
|
14535
|
-
if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
|
|
14536
|
-
key,
|
|
14537
|
-
normalizeForComparison(entryValue)
|
|
14538
|
-
]));
|
|
14539
|
-
return value;
|
|
14540
|
-
}
|
|
14541
|
-
function cachedManifestEntry(dep, packageName) {
|
|
14542
|
-
return {
|
|
14543
|
-
app_id: dep.app_id,
|
|
14544
|
-
commit_id: dep.commit_id,
|
|
14545
|
-
...dep.branch ? {
|
|
14546
|
-
branch: dep.branch
|
|
14547
|
-
} : {},
|
|
14548
|
-
package_name: packageName
|
|
14549
|
-
};
|
|
14550
|
-
}
|
|
14551
|
-
function safeDependencyPath(filePath, kind) {
|
|
14552
|
-
const normalizedPath = external_path_default().normalize(filePath);
|
|
14553
|
-
if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
|
|
14554
|
-
return normalizedPath;
|
|
14555
|
-
}
|
|
14556
|
-
function dependencyPythonPath(name, kind) {
|
|
14557
|
-
const parts = name.split(".");
|
|
14558
|
-
if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
|
|
14559
|
-
return external_path_default().join(...parts);
|
|
14560
|
-
}
|
|
14561
|
-
async function writeDepToCache(projectRoot, dep) {
|
|
14562
|
-
const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
|
|
14563
|
-
await promises_default().rm(depDir, {
|
|
14564
|
-
recursive: true,
|
|
14565
|
-
force: true
|
|
14566
|
-
});
|
|
14567
|
-
await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
|
|
14568
|
-
recursive: true
|
|
14569
|
-
});
|
|
14570
|
-
await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
|
|
14571
|
-
recursive: true
|
|
14572
|
-
});
|
|
14573
|
-
await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
|
|
14574
|
-
package_name: dep.package_name
|
|
14575
|
-
}));
|
|
14576
|
-
for (const mod of dep.modules ?? []){
|
|
14577
|
-
const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
|
|
14578
|
-
if (mod.is_package) {
|
|
14579
|
-
await promises_default().mkdir(modulePath, {
|
|
14580
|
-
recursive: true
|
|
14581
|
-
});
|
|
14582
|
-
await promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
|
|
14583
|
-
} else {
|
|
14584
|
-
const moduleFile = `${modulePath}.py`;
|
|
14585
|
-
await promises_default().mkdir(external_path_default().dirname(moduleFile), {
|
|
14586
|
-
recursive: true
|
|
14587
|
-
});
|
|
14588
|
-
await promises_default().writeFile(moduleFile, mod.code);
|
|
14589
|
-
}
|
|
14590
|
-
}
|
|
14591
|
-
for (const form of dep.forms ?? []){
|
|
14592
|
-
const { code, class_name, is_package, ...formTemplate } = form;
|
|
14593
|
-
const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
|
|
14594
|
-
if (is_package) {
|
|
14595
|
-
await promises_default().mkdir(formPath, {
|
|
14596
|
-
recursive: true
|
|
14597
|
-
});
|
|
14598
|
-
await promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
|
|
14599
|
-
await promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
|
|
14600
|
-
} else {
|
|
14601
|
-
const formCodeFile = `${formPath}.py`;
|
|
14602
|
-
await promises_default().mkdir(external_path_default().dirname(formCodeFile), {
|
|
14603
|
-
recursive: true
|
|
14604
|
-
});
|
|
14605
|
-
await promises_default().writeFile(formCodeFile, code);
|
|
14606
|
-
await promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
|
|
14607
|
-
}
|
|
14608
|
-
}
|
|
14609
|
-
for (const mod of dep.server_modules ?? []){
|
|
14610
|
-
const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
|
|
14611
|
-
if (mod.is_package) {
|
|
14612
|
-
await promises_default().mkdir(modulePath, {
|
|
14613
|
-
recursive: true
|
|
14614
|
-
});
|
|
14615
|
-
await promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
|
|
14616
|
-
} else {
|
|
14617
|
-
const moduleFile = `${modulePath}.py`;
|
|
14618
|
-
await promises_default().mkdir(external_path_default().dirname(moduleFile), {
|
|
14619
|
-
recursive: true
|
|
14620
|
-
});
|
|
14621
|
-
await promises_default().writeFile(moduleFile, mod.code);
|
|
14622
|
-
}
|
|
14623
|
-
}
|
|
14624
|
-
for (const asset of dep.assets ?? []){
|
|
14625
|
-
const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
|
|
14626
|
-
await promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
14627
|
-
recursive: true
|
|
14628
|
-
});
|
|
14629
|
-
await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
|
|
14630
|
-
}
|
|
14631
|
-
for (const doc of dep.docs ?? []){
|
|
14632
|
-
const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
|
|
14633
|
-
await promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
14634
|
-
recursive: true
|
|
14635
|
-
});
|
|
14636
|
-
await promises_default().writeFile(targetPath, doc.content);
|
|
14637
|
-
}
|
|
14638
|
-
}
|
|
14639
|
-
async function writeAllDepsToCache(projectRoot, response, options = {}) {
|
|
14640
|
-
const includeDocs = options.includeDocs ?? true;
|
|
14641
|
-
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14642
|
-
const canReuseExistingCache = manifest?.includeDocs === includeDocs;
|
|
14643
|
-
if (!canReuseExistingCache) await clearDepsCache(projectRoot);
|
|
14644
|
-
const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
|
|
14645
|
-
const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
|
|
14646
|
-
for (const dep of previousCached)if (!resolvedAppIds.has(dep.app_id)) await promises_default().rm(external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name), {
|
|
14647
|
-
recursive: true,
|
|
14648
|
-
force: true
|
|
14649
|
-
});
|
|
14650
|
-
for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
|
|
14651
|
-
const returnedByAppId = new Map(response.dependencies.map((dep)=>[
|
|
14652
|
-
dep.app_id,
|
|
14653
|
-
dep
|
|
14654
|
-
]));
|
|
14655
|
-
const previousByAppId = new Map(previousCached.map((dep)=>[
|
|
14656
|
-
dep.app_id,
|
|
14657
|
-
dep
|
|
14658
|
-
]));
|
|
14659
|
-
const cachedDependencies = response.resolved_dependencies.map((dep)=>{
|
|
14660
|
-
const returned = returnedByAppId.get(dep.app_id);
|
|
14661
|
-
if (returned) return cachedManifestEntry(returned, returned.package_name);
|
|
14662
|
-
const previous = previousByAppId.get(dep.app_id);
|
|
14663
|
-
if (!previous) return;
|
|
14664
|
-
return cachedManifestEntry(dep, previous.package_name);
|
|
14665
|
-
}).filter((dep)=>void 0 !== dep);
|
|
14666
|
-
const newManifest = {
|
|
14667
|
-
version: DEPS_MANIFEST_VERSION,
|
|
14668
|
-
includeDocs,
|
|
14669
|
-
source_dependencies: await readAnvilYamlDependencies(projectRoot),
|
|
14670
|
-
resolved_dependencies: response.resolved_dependencies,
|
|
14671
|
-
cached_dependencies: cachedDependencies
|
|
14672
|
-
};
|
|
14673
|
-
await promises_default().mkdir(depsDir(projectRoot), {
|
|
14674
|
-
recursive: true
|
|
14675
|
-
});
|
|
14676
|
-
await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
|
|
14677
|
-
}
|
|
14678
|
-
async function clearDepsCache(projectRoot) {
|
|
14679
|
-
await promises_default().rm(depsDir(projectRoot), {
|
|
14680
|
-
recursive: true,
|
|
14681
|
-
force: true
|
|
14682
|
-
});
|
|
14683
|
-
}
|
|
14684
|
-
async function getCachedPackages(projectRoot) {
|
|
14685
|
-
try {
|
|
14686
|
-
const entries = await promises_default().readdir(depsDir(projectRoot), {
|
|
14687
|
-
withFileTypes: true
|
|
14688
|
-
});
|
|
14689
|
-
return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
|
|
14690
|
-
} catch {
|
|
14691
|
-
return [];
|
|
14692
|
-
}
|
|
14693
|
-
}
|
|
14694
|
-
const external_timers_promises_namespaceObject = require("timers/promises");
|
|
14695
|
-
const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
|
|
14696
|
-
const FETCH_RETRY_DELAY_MS = 200;
|
|
14697
|
-
function isWantNotValidError(error) {
|
|
14698
|
-
return WANT_NOT_VALID_RE.test(error.message || "");
|
|
14699
|
-
}
|
|
14700
|
-
class GitService extends Emitter {
|
|
14701
|
-
git;
|
|
14702
|
-
repoPath;
|
|
14703
|
-
static INDEX_LOCK_RETRY_DELAYS_MS = [
|
|
14704
|
-
25,
|
|
14705
|
-
50,
|
|
14706
|
-
100,
|
|
14707
|
-
200
|
|
14708
|
-
];
|
|
14709
|
-
constructor(repoPath){
|
|
14710
|
-
super();
|
|
14711
|
-
this.repoPath = external_path_default().resolve(repoPath);
|
|
14712
|
-
this.git = esm_default(this.repoPath);
|
|
14713
|
-
}
|
|
14714
|
-
getGit() {
|
|
14715
|
-
return this.git;
|
|
14716
|
-
}
|
|
14717
|
-
getRepoPath() {
|
|
14718
|
-
return this.repoPath;
|
|
14719
|
-
}
|
|
14720
|
-
async getGitDir() {
|
|
14721
|
-
try {
|
|
14722
|
-
const gitDir = (await this.git.revparse([
|
|
14723
|
-
"--git-dir"
|
|
14724
|
-
])).trim();
|
|
14725
|
-
if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
|
|
14726
|
-
return gitDir;
|
|
14727
|
-
} catch (e) {
|
|
14728
|
-
throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
|
|
14729
|
-
}
|
|
14730
|
-
}
|
|
14731
|
-
async getCurrentBranch() {
|
|
14732
|
-
try {
|
|
14733
|
-
const branchRef = await this.git.revparse([
|
|
14734
|
-
"--abbrev-ref",
|
|
14735
|
-
"HEAD"
|
|
14736
|
-
]);
|
|
14737
|
-
if ("HEAD" === branchRef) throw createSyncError.detachedHead();
|
|
14738
|
-
return branchRef;
|
|
14739
|
-
} catch (e) {
|
|
14740
|
-
if ("detached_head" === e.type) throw e;
|
|
14741
|
-
throw createSyncError.detachedHead();
|
|
14323
|
+
async getCurrentBranch() {
|
|
14324
|
+
try {
|
|
14325
|
+
const branchRef = await this.git.revparse([
|
|
14326
|
+
"--abbrev-ref",
|
|
14327
|
+
"HEAD"
|
|
14328
|
+
]);
|
|
14329
|
+
if ("HEAD" === branchRef) throw createSyncError.detachedHead();
|
|
14330
|
+
return branchRef;
|
|
14331
|
+
} catch (e) {
|
|
14332
|
+
if ("detached_head" === e.type) throw e;
|
|
14333
|
+
throw createSyncError.detachedHead();
|
|
14742
14334
|
}
|
|
14743
14335
|
}
|
|
14744
14336
|
async getCommitId() {
|
|
@@ -14818,323 +14410,982 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14818
14410
|
"--",
|
|
14819
14411
|
relativePath
|
|
14820
14412
|
]);
|
|
14821
|
-
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14822
|
-
discarded.push(relativePath);
|
|
14413
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14414
|
+
discarded.push(relativePath);
|
|
14415
|
+
} catch (e) {
|
|
14416
|
+
throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
|
|
14417
|
+
}
|
|
14418
|
+
return discarded;
|
|
14419
|
+
}
|
|
14420
|
+
async getHeadFileMode(relativePath) {
|
|
14421
|
+
const output = await this.git.raw([
|
|
14422
|
+
"ls-tree",
|
|
14423
|
+
"HEAD",
|
|
14424
|
+
"--",
|
|
14425
|
+
relativePath
|
|
14426
|
+
]);
|
|
14427
|
+
const match = output.match(/^(\d{6})\s/);
|
|
14428
|
+
return match?.[1] ?? null;
|
|
14429
|
+
}
|
|
14430
|
+
async hasSameContentAsHead(relativePath) {
|
|
14431
|
+
const headHash = (await this.git.revparse([
|
|
14432
|
+
`HEAD:${relativePath}`
|
|
14433
|
+
])).trim();
|
|
14434
|
+
const worktreeHash = (await this.git.raw([
|
|
14435
|
+
"hash-object",
|
|
14436
|
+
"--",
|
|
14437
|
+
relativePath
|
|
14438
|
+
])).trim();
|
|
14439
|
+
return headHash === worktreeHash;
|
|
14440
|
+
}
|
|
14441
|
+
async hasSameIndexContentAsHead(relativePath) {
|
|
14442
|
+
const headHash = (await this.git.revparse([
|
|
14443
|
+
`HEAD:${relativePath}`
|
|
14444
|
+
])).trim();
|
|
14445
|
+
const indexHash = (await this.git.revparse([
|
|
14446
|
+
`:${relativePath}`
|
|
14447
|
+
])).trim();
|
|
14448
|
+
return headHash === indexHash;
|
|
14449
|
+
}
|
|
14450
|
+
async restoreExecutableBit(relativePath, executable) {
|
|
14451
|
+
const filePath = external_path_default().join(this.repoPath, relativePath);
|
|
14452
|
+
const stat = await external_fs_.promises.stat(filePath);
|
|
14453
|
+
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14454
|
+
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14455
|
+
}
|
|
14456
|
+
async hasUncommittedChanges() {
|
|
14457
|
+
const status = await this.getStatus();
|
|
14458
|
+
return !status.isClean;
|
|
14459
|
+
}
|
|
14460
|
+
async fetch(url, refSpec) {
|
|
14461
|
+
let attempt = 0;
|
|
14462
|
+
while(true)try {
|
|
14463
|
+
await this.git.fetch([
|
|
14464
|
+
url,
|
|
14465
|
+
refSpec
|
|
14466
|
+
]);
|
|
14467
|
+
if (attempt > 0) this.emit("fetch-retry-resolved", {
|
|
14468
|
+
attempts: attempt,
|
|
14469
|
+
outcome: "succeeded"
|
|
14470
|
+
});
|
|
14471
|
+
return;
|
|
14472
|
+
} catch (e) {
|
|
14473
|
+
const err = e;
|
|
14474
|
+
if (!isWantNotValidError(err)) {
|
|
14475
|
+
if (attempt > 0) this.emit("fetch-retry-resolved", {
|
|
14476
|
+
attempts: attempt,
|
|
14477
|
+
outcome: "failed"
|
|
14478
|
+
});
|
|
14479
|
+
throw errors_createGitError.fetchFailed(err.message);
|
|
14480
|
+
}
|
|
14481
|
+
attempt++;
|
|
14482
|
+
this.emit("fetch-retry", {
|
|
14483
|
+
attempt,
|
|
14484
|
+
delayMs: FETCH_RETRY_DELAY_MS,
|
|
14485
|
+
error: err.message
|
|
14486
|
+
});
|
|
14487
|
+
logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
|
|
14488
|
+
await (0, promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
|
|
14489
|
+
}
|
|
14490
|
+
}
|
|
14491
|
+
async reset(ref, mode = "mixed") {
|
|
14492
|
+
await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
|
|
14493
|
+
`--${mode}`,
|
|
14494
|
+
ref
|
|
14495
|
+
]));
|
|
14496
|
+
}
|
|
14497
|
+
async checkout(paths) {
|
|
14498
|
+
await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
|
|
14499
|
+
}
|
|
14500
|
+
async runGitCommandWithIndexLockRetry(command, operation) {
|
|
14501
|
+
let lastError;
|
|
14502
|
+
for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
|
|
14503
|
+
await operation();
|
|
14504
|
+
return;
|
|
14505
|
+
} catch (e) {
|
|
14506
|
+
lastError = e;
|
|
14507
|
+
if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
|
|
14508
|
+
await (0, promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
|
|
14509
|
+
}
|
|
14510
|
+
throw errors_createGitError.commandFailed(command, lastError.message);
|
|
14511
|
+
}
|
|
14512
|
+
isIndexLockError(error) {
|
|
14513
|
+
const message = error.message ?? "";
|
|
14514
|
+
return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
|
|
14515
|
+
}
|
|
14516
|
+
async stash(message) {
|
|
14517
|
+
try {
|
|
14518
|
+
const args = [
|
|
14519
|
+
"stash",
|
|
14520
|
+
"push",
|
|
14521
|
+
"--include-untracked"
|
|
14522
|
+
];
|
|
14523
|
+
if (message) args.push("-m", message);
|
|
14524
|
+
const result = await this.git.raw(args);
|
|
14525
|
+
return !result.includes("No local changes to save");
|
|
14526
|
+
} catch (e) {
|
|
14527
|
+
throw errors_createGitError.commandFailed("stash", e.message);
|
|
14528
|
+
}
|
|
14529
|
+
}
|
|
14530
|
+
async stashPop() {
|
|
14531
|
+
try {
|
|
14532
|
+
await this.git.raw([
|
|
14533
|
+
"stash",
|
|
14534
|
+
"pop"
|
|
14535
|
+
]);
|
|
14536
|
+
} catch (e) {
|
|
14537
|
+
throw errors_createGitError.commandFailed("stash pop", e.message);
|
|
14538
|
+
}
|
|
14539
|
+
}
|
|
14540
|
+
async clean(files) {
|
|
14541
|
+
try {
|
|
14542
|
+
await this.git.clean(CleanOptions.FORCE, files);
|
|
14543
|
+
} catch (e) {
|
|
14544
|
+
throw errors_createGitError.commandFailed("clean", e.message);
|
|
14545
|
+
}
|
|
14546
|
+
}
|
|
14547
|
+
async diffNames(fromCommit, toCommit) {
|
|
14548
|
+
try {
|
|
14549
|
+
const diffResult = await this.git.diff([
|
|
14550
|
+
fromCommit,
|
|
14551
|
+
toCommit,
|
|
14552
|
+
"--name-only"
|
|
14553
|
+
]);
|
|
14554
|
+
const files = diffResult.split("\n").filter((f)=>f.trim());
|
|
14555
|
+
return files;
|
|
14556
|
+
} catch (e) {
|
|
14557
|
+
throw errors_createGitError.commandFailed("diff", e.message);
|
|
14558
|
+
}
|
|
14559
|
+
}
|
|
14560
|
+
async getAheadBehind(localRef, remoteRef) {
|
|
14561
|
+
try {
|
|
14562
|
+
const result = await this.git.raw([
|
|
14563
|
+
"rev-list",
|
|
14564
|
+
"--left-right",
|
|
14565
|
+
"--count",
|
|
14566
|
+
`${remoteRef}...${localRef}`
|
|
14567
|
+
]);
|
|
14568
|
+
const [behind, ahead] = result.trim().split("\t").map(Number);
|
|
14569
|
+
return {
|
|
14570
|
+
ahead,
|
|
14571
|
+
behind
|
|
14572
|
+
};
|
|
14573
|
+
} catch (e) {
|
|
14574
|
+
throw errors_createGitError.commandFailed("rev-list", e.message);
|
|
14575
|
+
}
|
|
14576
|
+
}
|
|
14577
|
+
async show(refPath) {
|
|
14578
|
+
try {
|
|
14579
|
+
const content = await this.git.show([
|
|
14580
|
+
refPath
|
|
14581
|
+
]);
|
|
14582
|
+
return content;
|
|
14583
|
+
} catch (e) {
|
|
14584
|
+
throw errors_createGitError.commandFailed("show", e.message);
|
|
14585
|
+
}
|
|
14586
|
+
}
|
|
14587
|
+
async push(url, refSpec, force = false) {
|
|
14588
|
+
try {
|
|
14589
|
+
const args = force ? [
|
|
14590
|
+
"push",
|
|
14591
|
+
"--force",
|
|
14592
|
+
url,
|
|
14593
|
+
refSpec
|
|
14594
|
+
] : [
|
|
14595
|
+
"push",
|
|
14596
|
+
url,
|
|
14597
|
+
refSpec
|
|
14598
|
+
];
|
|
14599
|
+
await this.git.raw(args);
|
|
14600
|
+
} catch (e) {
|
|
14601
|
+
throw errors_createGitError.commandFailed("push", e.message);
|
|
14602
|
+
}
|
|
14603
|
+
}
|
|
14604
|
+
async rebase(onto) {
|
|
14605
|
+
try {
|
|
14606
|
+
await this.git.raw([
|
|
14607
|
+
"rebase",
|
|
14608
|
+
onto
|
|
14609
|
+
]);
|
|
14610
|
+
} catch (e) {
|
|
14611
|
+
throw errors_createGitError.commandFailed("rebase", e.message);
|
|
14612
|
+
}
|
|
14613
|
+
}
|
|
14614
|
+
async rebaseAbort() {
|
|
14615
|
+
try {
|
|
14616
|
+
await this.git.raw([
|
|
14617
|
+
"rebase",
|
|
14618
|
+
"--abort"
|
|
14619
|
+
]);
|
|
14620
|
+
} catch (e) {
|
|
14621
|
+
throw errors_createGitError.commandFailed("rebase --abort", e.message);
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
async isAncestor(ancestorRef, descendantRef) {
|
|
14625
|
+
try {
|
|
14626
|
+
const mergeBase = (await this.git.raw([
|
|
14627
|
+
"merge-base",
|
|
14628
|
+
ancestorRef,
|
|
14629
|
+
descendantRef
|
|
14630
|
+
])).trim();
|
|
14631
|
+
const ancestorCommit = (await this.git.revparse([
|
|
14632
|
+
ancestorRef
|
|
14633
|
+
])).trim();
|
|
14634
|
+
return mergeBase === ancestorCommit;
|
|
14635
|
+
} catch {
|
|
14636
|
+
return false;
|
|
14637
|
+
}
|
|
14638
|
+
}
|
|
14639
|
+
async mergeFastForward(ref) {
|
|
14640
|
+
await this.runGitCommandWithIndexLockRetry("merge --ff-only", ()=>this.git.raw([
|
|
14641
|
+
"merge",
|
|
14642
|
+
"--ff-only",
|
|
14643
|
+
ref
|
|
14644
|
+
]));
|
|
14645
|
+
}
|
|
14646
|
+
async deleteRef(ref) {
|
|
14647
|
+
try {
|
|
14648
|
+
await this.git.raw([
|
|
14649
|
+
"update-ref",
|
|
14650
|
+
"-d",
|
|
14651
|
+
ref
|
|
14652
|
+
]);
|
|
14653
|
+
} catch (e) {}
|
|
14654
|
+
}
|
|
14655
|
+
async getRemotes() {
|
|
14656
|
+
try {
|
|
14657
|
+
const remotes = await this.git.getRemotes(true);
|
|
14658
|
+
return remotes.map((r)=>({
|
|
14659
|
+
name: r.name,
|
|
14660
|
+
fetchUrl: r.refs.fetch
|
|
14661
|
+
}));
|
|
14823
14662
|
} catch (e) {
|
|
14824
|
-
throw errors_createGitError.commandFailed("
|
|
14663
|
+
throw errors_createGitError.commandFailed("remote", e.message);
|
|
14825
14664
|
}
|
|
14826
|
-
return discarded;
|
|
14827
|
-
}
|
|
14828
|
-
async getHeadFileMode(relativePath) {
|
|
14829
|
-
const output = await this.git.raw([
|
|
14830
|
-
"ls-tree",
|
|
14831
|
-
"HEAD",
|
|
14832
|
-
"--",
|
|
14833
|
-
relativePath
|
|
14834
|
-
]);
|
|
14835
|
-
const match = output.match(/^(\d{6})\s/);
|
|
14836
|
-
return match?.[1] ?? null;
|
|
14837
|
-
}
|
|
14838
|
-
async hasSameContentAsHead(relativePath) {
|
|
14839
|
-
const headHash = (await this.git.revparse([
|
|
14840
|
-
`HEAD:${relativePath}`
|
|
14841
|
-
])).trim();
|
|
14842
|
-
const worktreeHash = (await this.git.raw([
|
|
14843
|
-
"hash-object",
|
|
14844
|
-
"--",
|
|
14845
|
-
relativePath
|
|
14846
|
-
])).trim();
|
|
14847
|
-
return headHash === worktreeHash;
|
|
14848
|
-
}
|
|
14849
|
-
async hasSameIndexContentAsHead(relativePath) {
|
|
14850
|
-
const headHash = (await this.git.revparse([
|
|
14851
|
-
`HEAD:${relativePath}`
|
|
14852
|
-
])).trim();
|
|
14853
|
-
const indexHash = (await this.git.revparse([
|
|
14854
|
-
`:${relativePath}`
|
|
14855
|
-
])).trim();
|
|
14856
|
-
return headHash === indexHash;
|
|
14857
14665
|
}
|
|
14858
|
-
async
|
|
14859
|
-
const
|
|
14860
|
-
|
|
14861
|
-
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14862
|
-
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14666
|
+
async isGitInitialized() {
|
|
14667
|
+
const gitPath = external_path_default().join(this.repoPath, ".git");
|
|
14668
|
+
return external_fs_.existsSync(gitPath);
|
|
14863
14669
|
}
|
|
14864
|
-
async
|
|
14865
|
-
const
|
|
14866
|
-
|
|
14670
|
+
async removeEmptyDirectories(removedFiles) {
|
|
14671
|
+
const directoriesToCheck = new Set();
|
|
14672
|
+
for (const file of removedFiles){
|
|
14673
|
+
let dir = external_path_default().dirname(file);
|
|
14674
|
+
while(dir && "." !== dir){
|
|
14675
|
+
directoriesToCheck.add(dir);
|
|
14676
|
+
const parentDir = external_path_default().dirname(dir);
|
|
14677
|
+
if (parentDir === dir) break;
|
|
14678
|
+
dir = parentDir;
|
|
14679
|
+
}
|
|
14680
|
+
}
|
|
14681
|
+
const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
|
|
14682
|
+
for (const dir of sortedDirs)try {
|
|
14683
|
+
const dirPath = external_path_default().join(this.repoPath, dir);
|
|
14684
|
+
if (!external_fs_.existsSync(dirPath)) continue;
|
|
14685
|
+
const entries = await external_fs_.promises.readdir(dirPath);
|
|
14686
|
+
if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
|
|
14687
|
+
} catch (error) {}
|
|
14867
14688
|
}
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14689
|
+
}
|
|
14690
|
+
async function getStagedFileChanges(git, gitService) {
|
|
14691
|
+
try {
|
|
14692
|
+
const status = await git.status();
|
|
14693
|
+
const renames = status.renamed || [];
|
|
14694
|
+
const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
|
|
14695
|
+
const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
|
|
14696
|
+
const stagedRenames = [];
|
|
14697
|
+
for (const f of stagedFiles)if ("R" === f.index) {
|
|
14698
|
+
const rename = renames.find((r)=>r.to === f.path);
|
|
14699
|
+
if (rename) stagedRenames.push({
|
|
14700
|
+
path: rename.to,
|
|
14701
|
+
type: "rename",
|
|
14702
|
+
from: rename.from
|
|
14878
14703
|
});
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
14704
|
+
}
|
|
14705
|
+
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
14706
|
+
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
14707
|
+
const changes = [];
|
|
14708
|
+
for (const f of stagedFiles){
|
|
14709
|
+
if ("R" !== f.index) {
|
|
14710
|
+
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
14711
|
+
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
14712
|
+
path: f.path,
|
|
14713
|
+
type: "unlink"
|
|
14714
|
+
});
|
|
14715
|
+
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
14716
|
+
path: f.path,
|
|
14717
|
+
type: "add"
|
|
14718
|
+
});
|
|
14719
|
+
else changes.push({
|
|
14720
|
+
path: f.path,
|
|
14721
|
+
type: "change"
|
|
14722
|
+
});
|
|
14723
|
+
}
|
|
14888
14724
|
}
|
|
14889
|
-
attempt++;
|
|
14890
|
-
this.emit("fetch-retry", {
|
|
14891
|
-
attempt,
|
|
14892
|
-
delayMs: FETCH_RETRY_DELAY_MS,
|
|
14893
|
-
error: err.message
|
|
14894
|
-
});
|
|
14895
|
-
logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
|
|
14896
|
-
await (0, external_timers_promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
|
|
14897
14725
|
}
|
|
14726
|
+
changes.push(...stagedRenames);
|
|
14727
|
+
return changes;
|
|
14728
|
+
} catch (e) {
|
|
14729
|
+
throw errors_createGitError.commandFailed("status", e.message);
|
|
14898
14730
|
}
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14731
|
+
}
|
|
14732
|
+
async function getCurrentOrFallbackBranchName(git) {
|
|
14733
|
+
const branchRef = (await git.revparse([
|
|
14734
|
+
"--abbrev-ref",
|
|
14735
|
+
"HEAD"
|
|
14736
|
+
])).trim();
|
|
14737
|
+
if ("HEAD" !== branchRef) return branchRef;
|
|
14738
|
+
const [localBranches, remoteBranches] = await Promise.all([
|
|
14739
|
+
git.raw([
|
|
14740
|
+
"branch",
|
|
14741
|
+
"--format=%(refname:short)",
|
|
14742
|
+
"--contains",
|
|
14743
|
+
"HEAD"
|
|
14744
|
+
]),
|
|
14745
|
+
git.raw([
|
|
14746
|
+
"branch",
|
|
14747
|
+
"-r",
|
|
14748
|
+
"--format=%(refname:short)",
|
|
14749
|
+
"--contains",
|
|
14750
|
+
"HEAD"
|
|
14751
|
+
])
|
|
14752
|
+
]);
|
|
14753
|
+
const branchNames = [
|
|
14754
|
+
...parseBranchList(localBranches),
|
|
14755
|
+
...parseBranchList(remoteBranches).filter((branchName)=>!branchName.endsWith("/HEAD")).map((branchName)=>branchName.replace(/^[^/]+\//, ""))
|
|
14756
|
+
];
|
|
14757
|
+
const branchSet = new Set(branchNames);
|
|
14758
|
+
if (branchSet.has("master")) return "master";
|
|
14759
|
+
if (branchSet.has("main")) return "main";
|
|
14760
|
+
return "master";
|
|
14761
|
+
}
|
|
14762
|
+
function parseBranchList(output) {
|
|
14763
|
+
return output.split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
|
|
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
|
+
}
|
|
14773
|
+
function anvil_api_getDefaultAnvilUrl() {
|
|
14774
|
+
return resolveAnvilUrl();
|
|
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);
|
|
14904
14792
|
}
|
|
14905
|
-
|
|
14906
|
-
|
|
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
|
+
}
|
|
14809
|
+
async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
|
|
14810
|
+
const authToken = await auth_getValidAuthToken(anvilUrl, username);
|
|
14811
|
+
try {
|
|
14812
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
|
|
14813
|
+
method: "POST",
|
|
14814
|
+
headers: {
|
|
14815
|
+
Authorization: `Bearer ${authToken}`,
|
|
14816
|
+
"Content-Type": "application/json"
|
|
14817
|
+
},
|
|
14818
|
+
body: JSON.stringify({
|
|
14819
|
+
app_id: appId
|
|
14820
|
+
})
|
|
14821
|
+
});
|
|
14822
|
+
const data = await resp.json();
|
|
14823
|
+
if (resp.ok) return {
|
|
14824
|
+
valid: data.valid ?? false,
|
|
14825
|
+
app_name: data.app_name
|
|
14826
|
+
};
|
|
14827
|
+
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14828
|
+
if (403 === resp.status) throw createAppError.accessDenied(appId);
|
|
14829
|
+
if (404 === resp.status) throw createAppError.notFound(appId);
|
|
14830
|
+
logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
|
|
14831
|
+
return {
|
|
14832
|
+
valid: false,
|
|
14833
|
+
error: data.error || `Server error: ${resp.status}`
|
|
14834
|
+
};
|
|
14835
|
+
} catch (error) {
|
|
14836
|
+
if (error.type) throw error;
|
|
14837
|
+
throw errors_createNetworkError.network(error.message);
|
|
14838
|
+
}
|
|
14839
|
+
}
|
|
14840
|
+
async function listAppsForCheckout(options = {}) {
|
|
14841
|
+
const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
|
|
14842
|
+
const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
14843
|
+
const params = new URLSearchParams();
|
|
14844
|
+
if ("number" == typeof options.limit) params.set("limit", String(options.limit));
|
|
14845
|
+
if (options.cursor) params.set("cursor", options.cursor);
|
|
14846
|
+
if (options.q && options.q.trim()) params.set("q", options.q.trim());
|
|
14847
|
+
const query = params.toString();
|
|
14848
|
+
const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
|
|
14849
|
+
try {
|
|
14850
|
+
const resp = await fetch(url, {
|
|
14851
|
+
method: "GET",
|
|
14852
|
+
headers: {
|
|
14853
|
+
Authorization: `Bearer ${authToken}`
|
|
14854
|
+
},
|
|
14855
|
+
signal: options.signal
|
|
14856
|
+
});
|
|
14857
|
+
if (!resp.ok) {
|
|
14858
|
+
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14859
|
+
throw errors_createNetworkError.server(resp.status, resp.statusText);
|
|
14860
|
+
}
|
|
14861
|
+
const data = await resp.json();
|
|
14862
|
+
return {
|
|
14863
|
+
apps: Array.isArray(data.apps) ? data.apps : [],
|
|
14864
|
+
next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
|
|
14865
|
+
};
|
|
14866
|
+
} catch (error) {
|
|
14867
|
+
if (error.type) throw error;
|
|
14868
|
+
throw errors_createNetworkError.network(error.message);
|
|
14869
|
+
}
|
|
14870
|
+
}
|
|
14871
|
+
function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14872
|
+
const url = new URL(gitUrl);
|
|
14873
|
+
const encodedToken = encodeURIComponent(authToken);
|
|
14874
|
+
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
|
|
14875
|
+
}
|
|
14876
|
+
function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14877
|
+
const url = new URL(anvilUrl);
|
|
14878
|
+
const encodedToken = encodeURIComponent(authToken);
|
|
14879
|
+
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
|
|
14880
|
+
}
|
|
14881
|
+
function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14882
|
+
return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
|
|
14883
|
+
}
|
|
14884
|
+
async function getLatestVersion() {
|
|
14885
|
+
try {
|
|
14886
|
+
const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
|
|
14887
|
+
if (!response.ok) return null;
|
|
14888
|
+
const data = await response.json();
|
|
14889
|
+
return data.version;
|
|
14890
|
+
} catch (e) {
|
|
14891
|
+
return null;
|
|
14907
14892
|
}
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
|
|
14916
|
-
await (0, external_timers_promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
|
|
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}`
|
|
14917
14900
|
}
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
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
|
+
}
|
|
14905
|
+
function filterCandidates(candidates, explicitUrl, explicitUsername) {
|
|
14906
|
+
let filtered = candidates;
|
|
14907
|
+
if (explicitUrl) {
|
|
14908
|
+
const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
|
|
14909
|
+
filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
|
|
14923
14910
|
}
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14911
|
+
if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
|
|
14912
|
+
return filtered;
|
|
14913
|
+
}
|
|
14914
|
+
function formatCandidateLabel(candidate) {
|
|
14915
|
+
const parts = [
|
|
14916
|
+
candidate.appId
|
|
14917
|
+
];
|
|
14918
|
+
if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
|
|
14919
|
+
else parts.push(`(${candidate.detectedUrl})`);
|
|
14920
|
+
parts.push(`- ${candidate.description}`);
|
|
14921
|
+
return parts.join(" ");
|
|
14922
|
+
}
|
|
14923
|
+
function lookupRemoteInfoForAppId(appId, detectedRemotes) {
|
|
14924
|
+
const matches = detectedRemotes.filter((c)=>c.appId === appId);
|
|
14925
|
+
if (0 === matches.length) return {};
|
|
14926
|
+
const withUsername = matches.find((c)=>c.detectedUsername);
|
|
14927
|
+
if (withUsername) return {
|
|
14928
|
+
detectedUrl: withUsername.detectedUrl,
|
|
14929
|
+
detectedUsername: withUsername.detectedUsername
|
|
14930
|
+
};
|
|
14931
|
+
return {
|
|
14932
|
+
detectedUrl: matches[0].detectedUrl,
|
|
14933
|
+
detectedUsername: matches[0].detectedUsername
|
|
14934
|
+
};
|
|
14935
|
+
}
|
|
14936
|
+
async function detectAppIdsFromAllRemotes(repoPath) {
|
|
14937
|
+
const git = esm_default(repoPath);
|
|
14938
|
+
const out = [];
|
|
14939
|
+
try {
|
|
14940
|
+
const remotes = await git.getRemotes(true);
|
|
14941
|
+
for (const remote of remotes){
|
|
14942
|
+
const httpMatch = remote.refs.fetch?.match(/(https?):\/\/(?:[^@\/]+@)?([^\/]+)\/git\/([A-Z0-9]+)\.git/);
|
|
14943
|
+
if (httpMatch) {
|
|
14944
|
+
const [, scheme, host, detectedAppId] = httpMatch;
|
|
14945
|
+
const origin = new URL(`${scheme}://${host}`).origin;
|
|
14946
|
+
out.push({
|
|
14947
|
+
appId: detectedAppId,
|
|
14948
|
+
source: "remote",
|
|
14949
|
+
description: `Git remote '${remote.name}'`,
|
|
14950
|
+
detectedUrl: normalizeAnvilUrl(origin)
|
|
14951
|
+
});
|
|
14952
|
+
continue;
|
|
14953
|
+
}
|
|
14954
|
+
const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
|
|
14955
|
+
if (sshMatch) {
|
|
14956
|
+
const [, usernamePart, host, , detectedAppId] = sshMatch;
|
|
14957
|
+
const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
|
|
14958
|
+
const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
|
|
14959
|
+
out.push({
|
|
14960
|
+
appId: detectedAppId,
|
|
14961
|
+
source: "remote",
|
|
14962
|
+
description: `Git remote '${remote.name}' (SSH)`,
|
|
14963
|
+
detectedUrl: normalizeAnvilUrl(host),
|
|
14964
|
+
detectedUsername: finalUsername
|
|
14965
|
+
});
|
|
14966
|
+
}
|
|
14936
14967
|
}
|
|
14937
|
-
}
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14968
|
+
} catch (_e) {}
|
|
14969
|
+
return out;
|
|
14970
|
+
}
|
|
14971
|
+
async function detectAppIdsByCommitLookup(repoPath, options) {
|
|
14972
|
+
const anvilUrl = options.anvilUrl || resolveAnvilUrl();
|
|
14973
|
+
const username = options.username;
|
|
14974
|
+
const git = esm_default(repoPath);
|
|
14975
|
+
const out = [];
|
|
14976
|
+
try {
|
|
14977
|
+
const authToken = await auth_getValidAuthToken(anvilUrl, username);
|
|
14978
|
+
const commitId = (await git.revparse([
|
|
14979
|
+
"HEAD"
|
|
14980
|
+
]))?.trim();
|
|
14981
|
+
if (!commitId) return out;
|
|
14982
|
+
const branchName = await getCurrentOrFallbackBranchName(git);
|
|
14983
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
|
|
14984
|
+
method: "POST",
|
|
14985
|
+
headers: {
|
|
14986
|
+
Authorization: `Bearer ${authToken}`,
|
|
14987
|
+
"Content-Type": "application/json"
|
|
14988
|
+
},
|
|
14989
|
+
body: JSON.stringify({
|
|
14990
|
+
commit_id: commitId,
|
|
14991
|
+
branch_name: branchName
|
|
14992
|
+
})
|
|
14993
|
+
});
|
|
14994
|
+
logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${branchName} returned ${resp.status}`);
|
|
14995
|
+
if (!resp.ok) {
|
|
14996
|
+
const errorBody = await resp.text();
|
|
14997
|
+
logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
|
|
14946
14998
|
}
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14999
|
+
if (resp.ok) {
|
|
15000
|
+
const data = await resp.json();
|
|
15001
|
+
logger_logger.debug(`data ${JSON.stringify(data)}`);
|
|
15002
|
+
const apps = data?.apps ?? [];
|
|
15003
|
+
for (const app of apps)if (app?.app_id) out.push({
|
|
15004
|
+
appId: app.app_id,
|
|
15005
|
+
source: "remote",
|
|
15006
|
+
description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
|
|
15007
|
+
});
|
|
14953
15008
|
}
|
|
15009
|
+
} catch (_e) {}
|
|
15010
|
+
return out;
|
|
15011
|
+
}
|
|
15012
|
+
const external_fs_promises_namespaceObject = require("fs/promises");
|
|
15013
|
+
var external_fs_promises_default = /*#__PURE__*/ __webpack_require__.n(external_fs_promises_namespaceObject);
|
|
15014
|
+
function normalizeLineEndings(content) {
|
|
15015
|
+
return content.replace(/\r\n/g, "\n");
|
|
15016
|
+
}
|
|
15017
|
+
function pythonifyName(name) {
|
|
15018
|
+
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
15019
|
+
}
|
|
15020
|
+
function extractPythonName(relativePath, skipParts = 1) {
|
|
15021
|
+
const parts = relativePath.slice(0, -3).split("/");
|
|
15022
|
+
const isPackage = relativePath.endsWith("__init__.py");
|
|
15023
|
+
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
15024
|
+
return relevantParts.map(pythonifyName).join(".");
|
|
15025
|
+
}
|
|
15026
|
+
function deepEqual(a, b) {
|
|
15027
|
+
if (a === b) return true;
|
|
15028
|
+
if (null == a || null == b) return false;
|
|
15029
|
+
if (typeof a !== typeof b) return false;
|
|
15030
|
+
if ("object" != typeof a) return a === b;
|
|
15031
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
15032
|
+
if (Array.isArray(a)) {
|
|
15033
|
+
if (a.length !== b.length) return false;
|
|
15034
|
+
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14954
15035
|
}
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
}
|
|
15036
|
+
const keysA = Object.keys(a);
|
|
15037
|
+
const keysB = Object.keys(b);
|
|
15038
|
+
if (keysA.length !== keysB.length) return false;
|
|
15039
|
+
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
15040
|
+
}
|
|
15041
|
+
function parseHtmlWithFrontmatter(htmlContent) {
|
|
15042
|
+
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
15043
|
+
const match = htmlContent.match(frontmatterPattern);
|
|
15044
|
+
if (match) {
|
|
15045
|
+
const yamlStr = match[1];
|
|
15046
|
+
const html = match[2];
|
|
15047
|
+
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
15048
|
+
return {
|
|
15049
|
+
frontmatter,
|
|
15050
|
+
html
|
|
15051
|
+
};
|
|
14967
15052
|
}
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
15053
|
+
return {
|
|
15054
|
+
frontmatter: {},
|
|
15055
|
+
html: htmlContent
|
|
15056
|
+
};
|
|
15057
|
+
}
|
|
15058
|
+
const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
|
|
15059
|
+
async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
|
|
15060
|
+
const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
|
|
15061
|
+
try {
|
|
15062
|
+
const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
|
|
15063
|
+
if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
|
|
15064
|
+
const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
|
|
15065
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
15066
|
+
return true;
|
|
15067
|
+
} catch (error) {
|
|
15068
|
+
if ("ENOENT" !== error.code) throw error;
|
|
15069
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
15070
|
+
return true;
|
|
14984
15071
|
}
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
15072
|
+
}
|
|
15073
|
+
const ANVIL_DEPS_DIR = ".anvil/deps";
|
|
15074
|
+
const ANVIL_DEPS_MANIFEST = "manifest.json";
|
|
15075
|
+
const DEPS_MANIFEST_VERSION = 3;
|
|
15076
|
+
const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
|
|
15077
|
+
async function fetchAllDependencies(appId, anvilUrl, options = {}) {
|
|
15078
|
+
const { commit, includeDocs = true, knownDependencies = [] } = options;
|
|
15079
|
+
anvilUrl ??= await resolveAuthAnvilUrl();
|
|
15080
|
+
const token = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
15081
|
+
const body = {
|
|
15082
|
+
include_docs: includeDocs,
|
|
15083
|
+
known_dependencies: knownDependencies
|
|
15084
|
+
};
|
|
15085
|
+
if (commit) body.commit = commit;
|
|
15086
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
|
|
15087
|
+
method: "POST",
|
|
15088
|
+
headers: {
|
|
15089
|
+
Authorization: `Bearer ${token}`,
|
|
15090
|
+
"Content-Type": "application/json"
|
|
15091
|
+
},
|
|
15092
|
+
body: JSON.stringify(body)
|
|
15093
|
+
});
|
|
15094
|
+
if (!resp.ok) {
|
|
15095
|
+
const errorBody = await resp.text();
|
|
15096
|
+
throw new Error(formatHttpError("Failed to fetch dependencies", resp.status, errorBody));
|
|
14994
15097
|
}
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15098
|
+
return await resp.json();
|
|
15099
|
+
}
|
|
15100
|
+
async function refreshDependencyCache(projectRoot, appId, anvilUrl, options = {}) {
|
|
15101
|
+
auth_setRepoContext(projectRoot);
|
|
15102
|
+
anvilUrl ??= await resolveAuthAnvilUrl();
|
|
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);
|
|
15111
|
+
const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
|
|
15112
|
+
const response = await fetchAllDependencies(appId, anvilUrl, {
|
|
15113
|
+
username: options.username,
|
|
15114
|
+
commit,
|
|
15115
|
+
includeDocs,
|
|
15116
|
+
knownDependencies
|
|
15117
|
+
});
|
|
15118
|
+
if (!response.resolved_dependencies.length) {
|
|
15119
|
+
await clearDepsCache(projectRoot);
|
|
15120
|
+
return {
|
|
15121
|
+
response,
|
|
15122
|
+
addedGitignoreEntry: false
|
|
15123
|
+
};
|
|
15011
15124
|
}
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15125
|
+
const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
|
|
15126
|
+
await writeAllDepsToCache(projectRoot, response, {
|
|
15127
|
+
includeDocs,
|
|
15128
|
+
sourceDependencies
|
|
15129
|
+
});
|
|
15130
|
+
return {
|
|
15131
|
+
response,
|
|
15132
|
+
addedGitignoreEntry
|
|
15133
|
+
};
|
|
15134
|
+
}
|
|
15135
|
+
function depsDir(projectRoot) {
|
|
15136
|
+
return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
|
|
15137
|
+
}
|
|
15138
|
+
function depsManifestPath(projectRoot) {
|
|
15139
|
+
return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
|
|
15140
|
+
}
|
|
15141
|
+
function isResolvedDependencyManifestEntry(value) {
|
|
15142
|
+
if (!value || "object" != typeof value) return false;
|
|
15143
|
+
const entry = value;
|
|
15144
|
+
return "string" == typeof entry.app_id && entry.app_id.length > 0 && "string" == typeof entry.commit_id && entry.commit_id.length > 0 && (void 0 === entry.branch || "string" == typeof entry.branch);
|
|
15145
|
+
}
|
|
15146
|
+
function isCachedDependencyManifestEntry(value) {
|
|
15147
|
+
if (!isResolvedDependencyManifestEntry(value)) return false;
|
|
15148
|
+
const entry = value;
|
|
15149
|
+
return "string" == typeof entry.package_name && entry.package_name.length > 0;
|
|
15150
|
+
}
|
|
15151
|
+
function isDepsCacheManifest(value) {
|
|
15152
|
+
if (!value || "object" != typeof value) return false;
|
|
15153
|
+
const manifest = value;
|
|
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);
|
|
15155
|
+
}
|
|
15156
|
+
async function readDepsCacheManifest(projectRoot) {
|
|
15157
|
+
try {
|
|
15158
|
+
const raw = await external_fs_promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
|
|
15159
|
+
const parsed = JSON.parse(raw);
|
|
15160
|
+
return isDepsCacheManifest(parsed) ? parsed : void 0;
|
|
15161
|
+
} catch {
|
|
15162
|
+
return;
|
|
15021
15163
|
}
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15164
|
+
}
|
|
15165
|
+
async function getKnownDependenciesForCache(projectRoot, includeDocs) {
|
|
15166
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
15167
|
+
if (!manifest || manifest.includeDocs !== includeDocs) return [];
|
|
15168
|
+
return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
|
|
15169
|
+
app_id,
|
|
15170
|
+
commit_id
|
|
15171
|
+
}));
|
|
15172
|
+
}
|
|
15173
|
+
function getDependencyWatchSubscriptionsFromManifest(manifest) {
|
|
15174
|
+
if (!manifest) return [];
|
|
15175
|
+
const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
|
|
15176
|
+
dep.app_id,
|
|
15177
|
+
dep
|
|
15178
|
+
]));
|
|
15179
|
+
return manifest.resolved_dependencies.flatMap((dep)=>{
|
|
15180
|
+
if (!dep.branch) return [];
|
|
15181
|
+
return [
|
|
15182
|
+
{
|
|
15183
|
+
appId: dep.app_id,
|
|
15184
|
+
branch: dep.branch,
|
|
15185
|
+
packageName: cachedByAppId.get(dep.app_id)?.package_name
|
|
15186
|
+
}
|
|
15187
|
+
];
|
|
15188
|
+
});
|
|
15189
|
+
}
|
|
15190
|
+
function normalizeAnvilYamlDependenciesFromContent(content) {
|
|
15191
|
+
const parsed = external_js_yaml_default().load(content);
|
|
15192
|
+
if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
|
|
15193
|
+
return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
|
|
15194
|
+
}
|
|
15195
|
+
function normalizeAnvilYamlDependenciesValue(value) {
|
|
15196
|
+
if (!Array.isArray(value)) return [];
|
|
15197
|
+
return value.map(normalizeForComparison);
|
|
15198
|
+
}
|
|
15199
|
+
async function readAnvilYamlDependencies(projectRoot) {
|
|
15200
|
+
try {
|
|
15201
|
+
const content = await external_fs_promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
|
|
15202
|
+
return normalizeAnvilYamlDependenciesFromContent(content);
|
|
15203
|
+
} catch {
|
|
15204
|
+
return [];
|
|
15031
15205
|
}
|
|
15032
|
-
|
|
15206
|
+
}
|
|
15207
|
+
async function getDependencyCacheWarning(projectRoot) {
|
|
15208
|
+
const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
|
|
15209
|
+
if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
|
|
15210
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
15211
|
+
if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
|
|
15212
|
+
if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
|
|
15213
|
+
const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
|
|
15214
|
+
dependency.app_id,
|
|
15215
|
+
dependency
|
|
15216
|
+
]));
|
|
15217
|
+
for (const dependency of manifest.resolved_dependencies){
|
|
15218
|
+
const cached = cachedByAppId.get(dependency.app_id);
|
|
15219
|
+
if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
|
|
15033
15220
|
try {
|
|
15034
|
-
|
|
15035
|
-
"merge-base",
|
|
15036
|
-
ancestorRef,
|
|
15037
|
-
descendantRef
|
|
15038
|
-
])).trim();
|
|
15039
|
-
const ancestorCommit = (await this.git.revparse([
|
|
15040
|
-
ancestorRef
|
|
15041
|
-
])).trim();
|
|
15042
|
-
return mergeBase === ancestorCommit;
|
|
15221
|
+
await external_fs_promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
|
|
15043
15222
|
} catch {
|
|
15044
|
-
return
|
|
15223
|
+
return DEPENDENCY_CACHE_WARNING;
|
|
15045
15224
|
}
|
|
15046
15225
|
}
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15226
|
+
}
|
|
15227
|
+
function anvilYamlDependenciesEqual(left, right) {
|
|
15228
|
+
return deepEqual(left, right);
|
|
15229
|
+
}
|
|
15230
|
+
function normalizeForComparison(value) {
|
|
15231
|
+
if (Array.isArray(value)) return value.map(normalizeForComparison);
|
|
15232
|
+
if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
|
|
15233
|
+
key,
|
|
15234
|
+
normalizeForComparison(entryValue)
|
|
15235
|
+
]));
|
|
15236
|
+
return value;
|
|
15237
|
+
}
|
|
15238
|
+
function cachedManifestEntry(dep, packageName) {
|
|
15239
|
+
return {
|
|
15240
|
+
app_id: dep.app_id,
|
|
15241
|
+
commit_id: dep.commit_id,
|
|
15242
|
+
...dep.branch ? {
|
|
15243
|
+
branch: dep.branch
|
|
15244
|
+
} : {},
|
|
15245
|
+
package_name: packageName
|
|
15246
|
+
};
|
|
15247
|
+
}
|
|
15248
|
+
function safeDependencyPath(filePath, kind) {
|
|
15249
|
+
const normalizedPath = external_path_default().normalize(filePath);
|
|
15250
|
+
if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
|
|
15251
|
+
return normalizedPath;
|
|
15252
|
+
}
|
|
15253
|
+
function dependencyPythonPath(name, kind) {
|
|
15254
|
+
const parts = name.split(".");
|
|
15255
|
+
if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
|
|
15256
|
+
return external_path_default().join(...parts);
|
|
15257
|
+
}
|
|
15258
|
+
async function writeDepToCache(projectRoot, dep) {
|
|
15259
|
+
const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
|
|
15260
|
+
await external_fs_promises_default().rm(depDir, {
|
|
15261
|
+
recursive: true,
|
|
15262
|
+
force: true
|
|
15263
|
+
});
|
|
15264
|
+
await external_fs_promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
|
|
15265
|
+
recursive: true
|
|
15266
|
+
});
|
|
15267
|
+
await external_fs_promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
|
|
15268
|
+
recursive: true
|
|
15269
|
+
});
|
|
15270
|
+
await external_fs_promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
|
|
15271
|
+
package_name: dep.package_name
|
|
15272
|
+
}));
|
|
15273
|
+
for (const mod of dep.modules ?? []){
|
|
15274
|
+
const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
|
|
15275
|
+
if (mod.is_package) {
|
|
15276
|
+
await external_fs_promises_default().mkdir(modulePath, {
|
|
15277
|
+
recursive: true
|
|
15278
|
+
});
|
|
15279
|
+
await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
|
|
15280
|
+
} else {
|
|
15281
|
+
const moduleFile = `${modulePath}.py`;
|
|
15282
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
|
|
15283
|
+
recursive: true
|
|
15284
|
+
});
|
|
15285
|
+
await external_fs_promises_default().writeFile(moduleFile, mod.code);
|
|
15072
15286
|
}
|
|
15073
15287
|
}
|
|
15074
|
-
|
|
15075
|
-
const
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
}
|
|
15288
|
+
for (const form of dep.forms ?? []){
|
|
15289
|
+
const { code, class_name, is_package, ...formTemplate } = form;
|
|
15290
|
+
const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
|
|
15291
|
+
if (is_package) {
|
|
15292
|
+
await external_fs_promises_default().mkdir(formPath, {
|
|
15293
|
+
recursive: true
|
|
15294
|
+
});
|
|
15295
|
+
await external_fs_promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
|
|
15296
|
+
await external_fs_promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
|
|
15297
|
+
} else {
|
|
15298
|
+
const formCodeFile = `${formPath}.py`;
|
|
15299
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(formCodeFile), {
|
|
15300
|
+
recursive: true
|
|
15301
|
+
});
|
|
15302
|
+
await external_fs_promises_default().writeFile(formCodeFile, code);
|
|
15303
|
+
await external_fs_promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
|
|
15304
|
+
}
|
|
15305
|
+
}
|
|
15306
|
+
for (const mod of dep.server_modules ?? []){
|
|
15307
|
+
const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
|
|
15308
|
+
if (mod.is_package) {
|
|
15309
|
+
await external_fs_promises_default().mkdir(modulePath, {
|
|
15310
|
+
recursive: true
|
|
15311
|
+
});
|
|
15312
|
+
await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
|
|
15313
|
+
} else {
|
|
15314
|
+
const moduleFile = `${modulePath}.py`;
|
|
15315
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
|
|
15316
|
+
recursive: true
|
|
15317
|
+
});
|
|
15318
|
+
await external_fs_promises_default().writeFile(moduleFile, mod.code);
|
|
15088
15319
|
}
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15320
|
+
}
|
|
15321
|
+
for (const asset of dep.assets ?? []){
|
|
15322
|
+
const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
|
|
15323
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
15324
|
+
recursive: true
|
|
15325
|
+
});
|
|
15326
|
+
await external_fs_promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
|
|
15327
|
+
}
|
|
15328
|
+
for (const doc of dep.docs ?? []){
|
|
15329
|
+
const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
|
|
15330
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
15331
|
+
recursive: true
|
|
15332
|
+
});
|
|
15333
|
+
await external_fs_promises_default().writeFile(targetPath, doc.content);
|
|
15096
15334
|
}
|
|
15097
15335
|
}
|
|
15098
|
-
async function
|
|
15336
|
+
async function writeAllDepsToCache(projectRoot, response, options = {}) {
|
|
15337
|
+
const includeDocs = options.includeDocs ?? true;
|
|
15338
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
15339
|
+
const canReuseExistingCache = manifest?.includeDocs === includeDocs;
|
|
15340
|
+
if (!canReuseExistingCache) await clearDepsCache(projectRoot);
|
|
15341
|
+
const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
|
|
15342
|
+
const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
|
|
15343
|
+
for (const dep of previousCached)if (!resolvedAppIds.has(dep.app_id)) await external_fs_promises_default().rm(external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name), {
|
|
15344
|
+
recursive: true,
|
|
15345
|
+
force: true
|
|
15346
|
+
});
|
|
15347
|
+
for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
|
|
15348
|
+
const returnedByAppId = new Map(response.dependencies.map((dep)=>[
|
|
15349
|
+
dep.app_id,
|
|
15350
|
+
dep
|
|
15351
|
+
]));
|
|
15352
|
+
const previousByAppId = new Map(previousCached.map((dep)=>[
|
|
15353
|
+
dep.app_id,
|
|
15354
|
+
dep
|
|
15355
|
+
]));
|
|
15356
|
+
const cachedDependencies = response.resolved_dependencies.map((dep)=>{
|
|
15357
|
+
const returned = returnedByAppId.get(dep.app_id);
|
|
15358
|
+
if (returned) return cachedManifestEntry(returned, returned.package_name);
|
|
15359
|
+
const previous = previousByAppId.get(dep.app_id);
|
|
15360
|
+
if (!previous) return;
|
|
15361
|
+
return cachedManifestEntry(dep, previous.package_name);
|
|
15362
|
+
}).filter((dep)=>void 0 !== dep);
|
|
15363
|
+
const newManifest = {
|
|
15364
|
+
version: DEPS_MANIFEST_VERSION,
|
|
15365
|
+
includeDocs,
|
|
15366
|
+
source_dependencies: void 0 === options.sourceDependencies ? await readAnvilYamlDependencies(projectRoot) : options.sourceDependencies,
|
|
15367
|
+
resolved_dependencies: response.resolved_dependencies,
|
|
15368
|
+
cached_dependencies: cachedDependencies
|
|
15369
|
+
};
|
|
15370
|
+
await external_fs_promises_default().mkdir(depsDir(projectRoot), {
|
|
15371
|
+
recursive: true
|
|
15372
|
+
});
|
|
15373
|
+
await external_fs_promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
|
|
15374
|
+
}
|
|
15375
|
+
async function clearDepsCache(projectRoot) {
|
|
15376
|
+
await external_fs_promises_default().rm(depsDir(projectRoot), {
|
|
15377
|
+
recursive: true,
|
|
15378
|
+
force: true
|
|
15379
|
+
});
|
|
15380
|
+
}
|
|
15381
|
+
async function getCachedPackages(projectRoot) {
|
|
15099
15382
|
try {
|
|
15100
|
-
const
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
const rename = renames.find((r)=>r.to === f.path);
|
|
15107
|
-
if (rename) stagedRenames.push({
|
|
15108
|
-
path: rename.to,
|
|
15109
|
-
type: "rename",
|
|
15110
|
-
from: rename.from
|
|
15111
|
-
});
|
|
15112
|
-
}
|
|
15113
|
-
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
15114
|
-
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
15115
|
-
const changes = [];
|
|
15116
|
-
for (const f of stagedFiles){
|
|
15117
|
-
if ("R" !== f.index) {
|
|
15118
|
-
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
15119
|
-
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
15120
|
-
path: f.path,
|
|
15121
|
-
type: "unlink"
|
|
15122
|
-
});
|
|
15123
|
-
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
15124
|
-
path: f.path,
|
|
15125
|
-
type: "add"
|
|
15126
|
-
});
|
|
15127
|
-
else changes.push({
|
|
15128
|
-
path: f.path,
|
|
15129
|
-
type: "change"
|
|
15130
|
-
});
|
|
15131
|
-
}
|
|
15132
|
-
}
|
|
15133
|
-
}
|
|
15134
|
-
changes.push(...stagedRenames);
|
|
15135
|
-
return changes;
|
|
15136
|
-
} catch (e) {
|
|
15137
|
-
throw errors_createGitError.commandFailed("status", e.message);
|
|
15383
|
+
const entries = await external_fs_promises_default().readdir(depsDir(projectRoot), {
|
|
15384
|
+
withFileTypes: true
|
|
15385
|
+
});
|
|
15386
|
+
return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
|
|
15387
|
+
} catch {
|
|
15388
|
+
return [];
|
|
15138
15389
|
}
|
|
15139
15390
|
}
|
|
15140
15391
|
const external_chokidar_namespaceObject = require("chokidar");
|
|
@@ -16199,8 +16450,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16199
16450
|
}
|
|
16200
16451
|
if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
|
|
16201
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");
|
|
16202
16454
|
if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
|
|
16203
|
-
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");
|
|
16204
16457
|
} else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
|
|
16205
16458
|
return issues;
|
|
16206
16459
|
}
|
|
@@ -16273,7 +16526,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16273
16526
|
"toolbox_item",
|
|
16274
16527
|
"layout_metadata",
|
|
16275
16528
|
"item_type",
|
|
16276
|
-
"slots"
|
|
16529
|
+
"slots",
|
|
16530
|
+
"help_text"
|
|
16277
16531
|
]);
|
|
16278
16532
|
const FRONTMATTER_DISALLOWED_KEYS = new Set([
|
|
16279
16533
|
"container",
|
|
@@ -16440,6 +16694,45 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16440
16694
|
};
|
|
16441
16695
|
}
|
|
16442
16696
|
const validatePython_requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
|
|
16697
|
+
const DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB = 768;
|
|
16698
|
+
const DEFAULT_PYRIGHT_TIMEOUT_MS = 300000;
|
|
16699
|
+
function parsePositiveIntegerEnv(env, name, defaultValue) {
|
|
16700
|
+
const rawValue = env[name];
|
|
16701
|
+
if (void 0 === rawValue || "" === rawValue) return {
|
|
16702
|
+
value: defaultValue
|
|
16703
|
+
};
|
|
16704
|
+
if (!/^\d+$/.test(rawValue) || Number(rawValue) <= 0 || !Number.isSafeInteger(Number(rawValue))) return {
|
|
16705
|
+
error: `${name} must be a positive integer`
|
|
16706
|
+
};
|
|
16707
|
+
return {
|
|
16708
|
+
value: Number(rawValue)
|
|
16709
|
+
};
|
|
16710
|
+
}
|
|
16711
|
+
function pyrightChildNodeOptions(nodeOptions, maxOldSpaceMb) {
|
|
16712
|
+
const maxOldSpaceFlag = "--max[-_]old[-_]space[-_]size";
|
|
16713
|
+
const withoutEqualsForm = (nodeOptions ?? "").replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}=\\S+`, "g"), "$1");
|
|
16714
|
+
const withoutSeparateForm = withoutEqualsForm.replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}\\s+\\S+`, "g"), "$1");
|
|
16715
|
+
return [
|
|
16716
|
+
withoutSeparateForm.trim(),
|
|
16717
|
+
`--max-old-space-size=${maxOldSpaceMb}`
|
|
16718
|
+
].filter(Boolean).join(" ");
|
|
16719
|
+
}
|
|
16720
|
+
function semanticProcessFailure(message) {
|
|
16721
|
+
return {
|
|
16722
|
+
ok: false,
|
|
16723
|
+
message: "Python semantic validation could not be run",
|
|
16724
|
+
issues: [
|
|
16725
|
+
{
|
|
16726
|
+
path: "root",
|
|
16727
|
+
message,
|
|
16728
|
+
severity: "error"
|
|
16729
|
+
}
|
|
16730
|
+
]
|
|
16731
|
+
};
|
|
16732
|
+
}
|
|
16733
|
+
function isV8HeapOutOfMemory(stderr) {
|
|
16734
|
+
return /(?:JavaScript heap out of memory|Reached heap limit|Ineffective mark-compacts near heap limit)/i.test(stderr);
|
|
16735
|
+
}
|
|
16443
16736
|
function resolveBundledAnvilPyright() {
|
|
16444
16737
|
try {
|
|
16445
16738
|
return {
|
|
@@ -16707,8 +17000,13 @@ print(json.dumps({"issues": issues}))
|
|
|
16707
17000
|
};
|
|
16708
17001
|
}
|
|
16709
17002
|
function validatePythonWithPyright(appRoot, options = {}) {
|
|
17003
|
+
const env = options.env ?? process.env;
|
|
17004
|
+
const maxOldSpace = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_MAX_OLD_SPACE_MB", DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB);
|
|
17005
|
+
if ("error" in maxOldSpace) return semanticProcessFailure(maxOldSpace.error);
|
|
17006
|
+
const timeout = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_TIMEOUT_MS", DEFAULT_PYRIGHT_TIMEOUT_MS);
|
|
17007
|
+
if ("error" in timeout) return semanticProcessFailure(timeout.error);
|
|
16710
17008
|
const pyright = void 0 === options.pyright ? findAnvilPyright({
|
|
16711
|
-
env
|
|
17009
|
+
env
|
|
16712
17010
|
}) : options.pyright;
|
|
16713
17011
|
if (!pyright) return {
|
|
16714
17012
|
ok: false,
|
|
@@ -16731,30 +17029,19 @@ print(json.dumps({"issues": issues}))
|
|
|
16731
17029
|
...options.files ?? []
|
|
16732
17030
|
], {
|
|
16733
17031
|
encoding: "utf8",
|
|
16734
|
-
|
|
17032
|
+
env: {
|
|
17033
|
+
...env,
|
|
17034
|
+
NODE_OPTIONS: pyrightChildNodeOptions(env.NODE_OPTIONS, maxOldSpace.value)
|
|
17035
|
+
},
|
|
17036
|
+
maxBuffer: 10485760,
|
|
17037
|
+
timeout: timeout.value
|
|
16735
17038
|
});
|
|
16736
|
-
if (result.error)
|
|
16737
|
-
|
|
16738
|
-
message
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
message: result.error.message,
|
|
16743
|
-
severity: "error"
|
|
16744
|
-
}
|
|
16745
|
-
]
|
|
16746
|
-
};
|
|
16747
|
-
if (0 !== result.status && 1 !== result.status) return {
|
|
16748
|
-
ok: false,
|
|
16749
|
-
message: "Python semantic validation could not be run",
|
|
16750
|
-
issues: [
|
|
16751
|
-
{
|
|
16752
|
-
path: "root",
|
|
16753
|
-
message: result.stderr.trim() || `anvil-pyright exited with status ${result.status}`,
|
|
16754
|
-
severity: "error"
|
|
16755
|
-
}
|
|
16756
|
-
]
|
|
16757
|
-
};
|
|
17039
|
+
if (result.error) {
|
|
17040
|
+
if ("ETIMEDOUT" === result.error.code) return semanticProcessFailure(`anvil-pyright exceeded the ${timeout.value} ms timeout. Increase ANVIL_PYRIGHT_TIMEOUT_MS or validate a smaller target.`);
|
|
17041
|
+
return semanticProcessFailure(result.error.message);
|
|
17042
|
+
}
|
|
17043
|
+
if (isV8HeapOutOfMemory(result.stderr)) return semanticProcessFailure(`anvil-pyright ran out of memory with a ${maxOldSpace.value} MB heap. Increase ANVIL_PYRIGHT_MAX_OLD_SPACE_MB or validate a smaller target.`);
|
|
17044
|
+
if (0 !== result.status && 1 !== result.status) return semanticProcessFailure(result.stderr.trim() || `anvil-pyright exited with status ${result.status}`);
|
|
16758
17045
|
let parsed;
|
|
16759
17046
|
try {
|
|
16760
17047
|
parsed = JSON.parse(result.stdout.trim());
|
|
@@ -17071,7 +17358,7 @@ print(json.dumps({"issues": issues}))
|
|
|
17071
17358
|
}
|
|
17072
17359
|
function validateScheduledTasks(value, path) {
|
|
17073
17360
|
const issues = [];
|
|
17074
|
-
if (
|
|
17361
|
+
if (null == value) return issues;
|
|
17075
17362
|
if (!Array.isArray(value)) {
|
|
17076
17363
|
validators_pushIssue(issues, path, "must be an array");
|
|
17077
17364
|
return issues;
|
|
@@ -18570,6 +18857,15 @@ print(json.dumps({"issues": issues}))
|
|
|
18570
18857
|
this.hasPendingChanges = true;
|
|
18571
18858
|
await this.processSaveBatchWithEvents();
|
|
18572
18859
|
}
|
|
18860
|
+
async flush() {
|
|
18861
|
+
const requireReady = ()=>{
|
|
18862
|
+
if (this.config.isPaused() || this.config.isCleanedUp() || !this.config.isSettlePeriodOver()) throw new Error("Cannot flush saves while watching is paused, closed, or changing branch");
|
|
18863
|
+
};
|
|
18864
|
+
requireReady();
|
|
18865
|
+
await this.forceSave();
|
|
18866
|
+
requireReady();
|
|
18867
|
+
if (this.hasPendingChanges) throw new Error("Saves remain pending");
|
|
18868
|
+
}
|
|
18573
18869
|
cleanup() {
|
|
18574
18870
|
if (this.hasPendingChanges) {
|
|
18575
18871
|
logger_logger.progressEnd("sync");
|
|
@@ -18666,10 +18962,7 @@ print(json.dumps({"issues": issues}))
|
|
|
18666
18962
|
status = await this.config.gitService.getStatus();
|
|
18667
18963
|
} catch (e) {
|
|
18668
18964
|
logger_logger.error(external_chalk_default().red(`Error reading git status: ${errors_getErrorMessage(e)}`));
|
|
18669
|
-
|
|
18670
|
-
skipped: true,
|
|
18671
|
-
reason: "Git status error"
|
|
18672
|
-
};
|
|
18965
|
+
throw e;
|
|
18673
18966
|
}
|
|
18674
18967
|
let allChanges;
|
|
18675
18968
|
if (this.config.stagedOnly) {
|
|
@@ -19515,6 +19808,7 @@ print(json.dumps({"issues": issues}))
|
|
|
19515
19808
|
try {
|
|
19516
19809
|
const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
|
|
19517
19810
|
const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
|
|
19811
|
+
username: this.username,
|
|
19518
19812
|
includeDocs: true
|
|
19519
19813
|
});
|
|
19520
19814
|
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
@@ -19826,6 +20120,12 @@ print(json.dumps({"issues": issues}))
|
|
|
19826
20120
|
if (!this.saveProcessor) return;
|
|
19827
20121
|
return this.saveProcessor.forceSave();
|
|
19828
20122
|
}
|
|
20123
|
+
async flushSaves() {
|
|
20124
|
+
if (!this.saveProcessor || this.isSwitchingForEnvironment) throw new Error("App synchronization is not ready");
|
|
20125
|
+
const branch = this.currentBranch;
|
|
20126
|
+
await this.saveProcessor.flush();
|
|
20127
|
+
if (this.isSwitchingForEnvironment || this.currentBranch !== branch || await this.gitService.getCurrentBranch() !== branch) throw new Error("App branch changed while flushing saves");
|
|
20128
|
+
}
|
|
19829
20129
|
async getSuccessMessage() {
|
|
19830
20130
|
try {
|
|
19831
20131
|
const info = await this.gitService.getCommitInfo();
|
|
@@ -20017,6 +20317,8 @@ print(json.dumps({"issues": issues}))
|
|
|
20017
20317
|
}
|
|
20018
20318
|
}
|
|
20019
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);
|
|
20020
20322
|
const external_open_namespaceObject = require("open");
|
|
20021
20323
|
var external_open_default = /*#__PURE__*/ __webpack_require__.n(external_open_namespaceObject);
|
|
20022
20324
|
const successPage = `<!DOCTYPE html>
|
|
@@ -21048,7 +21350,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21048
21350
|
value: null
|
|
21049
21351
|
});
|
|
21050
21352
|
return logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0], {
|
|
21051
|
-
action: "
|
|
21353
|
+
action: "Anvil server selection",
|
|
21052
21354
|
nonInteractiveHint: `Use --url <ANVIL_URL>. Available URLs: ${decision.urls.join(", ")}.`
|
|
21053
21355
|
});
|
|
21054
21356
|
}
|
|
@@ -21127,8 +21429,8 @@ print(json.dumps({"issues": issues}))
|
|
|
21127
21429
|
name: "Cancel",
|
|
21128
21430
|
value: null
|
|
21129
21431
|
});
|
|
21130
|
-
return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used
|
|
21131
|
-
action: "
|
|
21432
|
+
return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used?`, choices, accounts[0], {
|
|
21433
|
+
action: "Anvil account selection",
|
|
21132
21434
|
nonInteractiveHint: `Use --user <USERNAME>. Available accounts: ${accounts.join(", ")}.`
|
|
21133
21435
|
});
|
|
21134
21436
|
}
|
|
@@ -21147,7 +21449,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21147
21449
|
logger_logger.verbose(external_chalk_default().cyan("Resolved app ID: ") + external_chalk_default().bold(parsed.appId));
|
|
21148
21450
|
const anvilUrl = await resolveCheckoutUrlForApi(options.anvilUrl, parsed.detectedUrl);
|
|
21149
21451
|
if (!anvilUrl) throw new Error("Anvil URL is required. Use --url <ANVIL_URL> or pass an input URL containing the host.");
|
|
21150
|
-
const gitUrl = options.gitUrl
|
|
21452
|
+
const gitUrl = void 0 === options.gitUrl ? anvilUrl : normalizeAnvilOrigin(options.gitUrl);
|
|
21151
21453
|
const resolvedUsername = await resolveCheckoutUsernameForApi(anvilUrl, options.user);
|
|
21152
21454
|
if (resolvedUsername) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(resolvedUsername));
|
|
21153
21455
|
else logger_logger.verbose(external_chalk_default().cyan("No account preselected; resolving after auth token lookup."));
|
|
@@ -21258,7 +21560,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21258
21560
|
await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
|
|
21259
21561
|
}
|
|
21260
21562
|
function registerCheckoutCommand(program) {
|
|
21261
|
-
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)=>{
|
|
21262
21564
|
try {
|
|
21263
21565
|
const globalOptions = command?.optsWithGlobals() || options || {};
|
|
21264
21566
|
if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
|
|
@@ -21273,6 +21575,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21273
21575
|
quiet: options?.quiet,
|
|
21274
21576
|
verbose: globalOptions.verbose,
|
|
21275
21577
|
anvilUrl: options?.url,
|
|
21578
|
+
gitUrl: options?.gitUrl,
|
|
21276
21579
|
user: options?.user,
|
|
21277
21580
|
force: options?.force,
|
|
21278
21581
|
query: options?.query
|
|
@@ -21284,6 +21587,232 @@ print(json.dumps({"issues": issues}))
|
|
|
21284
21587
|
});
|
|
21285
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');
|
|
21286
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
|
+
}
|
|
21287
21816
|
const defaultGitAuthDoctorDeps = {
|
|
21288
21817
|
getAppAuthBinding: getAppAuthBinding,
|
|
21289
21818
|
getAccountsForUrl: auth_getAccountsForUrl,
|
|
@@ -21577,7 +22106,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21577
22106
|
])).trim();
|
|
21578
22107
|
const resolvedGitCommonDir = external_path_default().isAbsolute(rawGitCommonDir) ? rawGitCommonDir : external_path_default().resolve(inputPath, rawGitCommonDir);
|
|
21579
22108
|
const gitCommonDir = external_fs_default().realpathSync(resolvedGitCommonDir);
|
|
21580
|
-
|
|
22109
|
+
auth_setRepoContext(repoPath);
|
|
21581
22110
|
const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
|
|
21582
22111
|
const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
|
|
21583
22112
|
const findings = [];
|
|
@@ -21703,7 +22232,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21703
22232
|
const repoPath = options.repoPath ?? deps.cwd();
|
|
21704
22233
|
if ("get" !== operation) return null;
|
|
21705
22234
|
if (!request.protocol || !request.host) return null;
|
|
21706
|
-
|
|
22235
|
+
auth_setRepoContext(repoPath);
|
|
21707
22236
|
const appId = parseAppIdFromGitPath(request.path);
|
|
21708
22237
|
if (!appId) return null;
|
|
21709
22238
|
const requestUrl = normalizeAnvilUrl(`${request.protocol}://${request.host}`);
|
|
@@ -21754,7 +22283,6 @@ print(json.dumps({"issues": issues}))
|
|
|
21754
22283
|
}
|
|
21755
22284
|
});
|
|
21756
22285
|
}
|
|
21757
|
-
const DEFAULT_ANVIL_URL = resolveAnvilUrl();
|
|
21758
22286
|
async function syncToLatest(repoPath, appId, options) {
|
|
21759
22287
|
const { anvilUrl, gitUrl = anvilUrl, authToken, currentBranch, username } = options;
|
|
21760
22288
|
try {
|
|
@@ -21783,9 +22311,10 @@ print(json.dumps({"issues": issues}))
|
|
|
21783
22311
|
}
|
|
21784
22312
|
}
|
|
21785
22313
|
async function watch(repoPath, appId, options = {}) {
|
|
21786
|
-
const { anvilUrl = DEFAULT_ANVIL_URL, gitUrl = anvilUrl, stagedOnly = false, username } = options;
|
|
21787
22314
|
repoPath = external_path_default().resolve(repoPath);
|
|
21788
|
-
|
|
22315
|
+
auth_setRepoContext(repoPath);
|
|
22316
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
22317
|
+
const { gitUrl = anvilUrl, stagedOnly = false, username } = options;
|
|
21789
22318
|
const storedEnvPid = options.environment?.envPid ? void 0 : await readEnvironmentPid(repoPath);
|
|
21790
22319
|
const environment = options.environment ?? (storedEnvPid ? {
|
|
21791
22320
|
envPid: storedEnvPid
|
|
@@ -21973,40 +22502,10 @@ print(json.dumps({"issues": issues}))
|
|
|
21973
22502
|
}));
|
|
21974
22503
|
choices.push({
|
|
21975
22504
|
name: "Cancel",
|
|
21976
|
-
value: null
|
|
21977
|
-
});
|
|
21978
|
-
const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
|
|
21979
|
-
return selectedUrl;
|
|
21980
|
-
}
|
|
21981
|
-
const defaultConfigureWatchGitAuthDeps = {
|
|
21982
|
-
configureCredentialHelperForUrl: configureCredentialHelperForUrl,
|
|
21983
|
-
setAppAuthBinding: setAppAuthBinding
|
|
21984
|
-
};
|
|
21985
|
-
const defaultSyncStartDeps = {
|
|
21986
|
-
pushToAnvil,
|
|
21987
|
-
recreateSessionAndValidate,
|
|
21988
|
-
recheckSyncStatus,
|
|
21989
|
-
startWatchingWithEventHandlers
|
|
21990
|
-
};
|
|
21991
|
-
function resolveWatchOpenPath(repoPath) {
|
|
21992
|
-
return external_path_default().resolve(repoPath);
|
|
21993
|
-
}
|
|
21994
|
-
async function openWatchPath(targetPath, deps) {
|
|
21995
|
-
const preferredEditor = String(getConfig("preferredEditor") || "").trim();
|
|
21996
|
-
const preferredEditorCommand = preferredEditor ? getPreferredEditorCommand(preferredEditor) : "";
|
|
21997
|
-
await openPathInEditorOrDefault(targetPath, preferredEditorCommand, deps);
|
|
21998
|
-
logger_logger.info(external_chalk_default().gray(`Opened ${targetPath}`));
|
|
21999
|
-
}
|
|
22000
|
-
async function configureWatchGitAuth(options, deps = defaultConfigureWatchGitAuthDeps) {
|
|
22001
|
-
await deps.configureCredentialHelperForUrl(options.repoPath, options.anvilUrl);
|
|
22002
|
-
await deps.setAppAuthBinding(options.repoPath, options.appId, {
|
|
22003
|
-
url: options.anvilUrl,
|
|
22004
|
-
username: options.username
|
|
22505
|
+
value: null
|
|
22005
22506
|
});
|
|
22006
|
-
|
|
22007
|
-
|
|
22008
|
-
const normalized = message.toLowerCase();
|
|
22009
|
-
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]");
|
|
22507
|
+
const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
|
|
22508
|
+
return selectedUrl;
|
|
22010
22509
|
}
|
|
22011
22510
|
async function selectAppId(candidates) {
|
|
22012
22511
|
if (0 === candidates.length) {
|
|
@@ -22085,6 +22584,169 @@ print(json.dumps({"issues": issues}))
|
|
|
22085
22584
|
return null;
|
|
22086
22585
|
}
|
|
22087
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
|
+
}
|
|
22088
22750
|
async function pushToAnvil(options) {
|
|
22089
22751
|
try {
|
|
22090
22752
|
const authToken = await auth_getValidAuthToken(options.anvilUrl, options.username);
|
|
@@ -22213,7 +22875,7 @@ print(json.dumps({"issues": issues}))
|
|
|
22213
22875
|
});
|
|
22214
22876
|
logger_logger.progressEnd("verify");
|
|
22215
22877
|
const currentCategory = getSyncStateCategory(freshSession.syncStatus);
|
|
22216
|
-
const currentBranch = freshSession.getBranchName() ||
|
|
22878
|
+
const currentBranch = freshSession.getBranchName() || await getFallbackBranchName(options.repoPath);
|
|
22217
22879
|
if (currentCategory !== previousCategory || currentBranch !== previousBranch) {
|
|
22218
22880
|
logger_logger.warn("Repository status has changed. Re-evaluating...");
|
|
22219
22881
|
return freshSession;
|
|
@@ -22246,7 +22908,7 @@ print(json.dumps({"issues": issues}))
|
|
|
22246
22908
|
}
|
|
22247
22909
|
async function checkSyncStatusAndStart(session, options, deps = defaultSyncStartDeps) {
|
|
22248
22910
|
const syncStatus = session.syncStatus;
|
|
22249
|
-
const branchName = session.getBranchName() ||
|
|
22911
|
+
const branchName = session.getBranchName() || await getFallbackBranchName(options.repoPath);
|
|
22250
22912
|
const hasUncommitted = session.hasUncommittedChanges;
|
|
22251
22913
|
const stateCategory = getSyncStateCategory(syncStatus);
|
|
22252
22914
|
if (syncStatus?.branchMissing) {
|
|
@@ -22566,6 +23228,9 @@ print(json.dumps({"issues": issues}))
|
|
|
22566
23228
|
const suggestedAction = error.suggestedAction;
|
|
22567
23229
|
if ("string" == typeof suggestedAction && suggestedAction.length > 0) logger_logger.warn(suggestedAction);
|
|
22568
23230
|
}
|
|
23231
|
+
async function getFallbackBranchName(repoPath) {
|
|
23232
|
+
return getCurrentOrFallbackBranchName(esm_default(repoPath));
|
|
23233
|
+
}
|
|
22569
23234
|
async function handleWatchCommand(options) {
|
|
22570
23235
|
const invoked = process.argv[2];
|
|
22571
23236
|
if ("sync" === invoked) {
|
|
@@ -22573,127 +23238,30 @@ print(json.dumps({"issues": issues}))
|
|
|
22573
23238
|
process.exit(1);
|
|
22574
23239
|
}
|
|
22575
23240
|
const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, dependencyWatch = true, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
|
|
22576
|
-
|
|
23241
|
+
auth_setRepoContext(repoPath);
|
|
22577
23242
|
try {
|
|
22578
23243
|
const validationResult = await validateAnvilApp(repoPath);
|
|
22579
23244
|
if (validationResult.appName) logger_logger.info(external_chalk_default().green("Anvil app: ") + external_chalk_default().bold(validationResult.appName));
|
|
22580
|
-
|
|
22581
|
-
|
|
22582
|
-
|
|
22583
|
-
|
|
22584
|
-
|
|
22585
|
-
|
|
22586
|
-
|
|
22587
|
-
|
|
22588
|
-
|
|
22589
|
-
|
|
22590
|
-
finalAppId = explicitAppId;
|
|
22591
|
-
const binding = await getAppAuthBinding(repoPath, explicitAppId);
|
|
22592
|
-
if (binding.url && !explicitUrl) {
|
|
22593
|
-
anvilUrl = normalizeAnvilUrl(binding.url);
|
|
22594
|
-
logger_logger.verbose(external_chalk_default().cyan("Resolved URL from binding for app ID: ") + external_chalk_default().bold(anvilUrl));
|
|
22595
|
-
}
|
|
22596
|
-
if (binding.username && !explicitUsername) {
|
|
22597
|
-
username = binding.username;
|
|
22598
|
-
logger_logger.verbose(external_chalk_default().cyan("Resolved username from binding for app ID: ") + external_chalk_default().bold(username));
|
|
22599
|
-
}
|
|
22600
|
-
const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
|
|
22601
|
-
if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
|
|
22602
|
-
anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
|
|
22603
|
-
logger_logger.verbose(external_chalk_default().cyan("Resolved URL from remote for app ID: ") + external_chalk_default().bold(anvilUrl));
|
|
22604
|
-
}
|
|
22605
|
-
if (remoteInfo.detectedUsername && !explicitUsername && !username) {
|
|
22606
|
-
username = remoteInfo.detectedUsername;
|
|
22607
|
-
logger_logger.verbose(external_chalk_default().cyan("Resolved username from remote for app ID: ") + external_chalk_default().bold(username));
|
|
22608
|
-
}
|
|
22609
|
-
} else {
|
|
22610
|
-
logger_logger.verbose(external_chalk_default().cyan("No app ID provided, attempting auto-detection..."));
|
|
22611
|
-
if (0 === filteredCandidates.length) {
|
|
22612
|
-
logger_logger.verbose(external_chalk_default().gray("No app IDs found in git remotes."));
|
|
22613
|
-
const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
|
|
22614
|
-
if (null === resolvedFallbackUrl) {
|
|
22615
|
-
logger_logger.warn("Operation cancelled.");
|
|
22616
|
-
process.exit(0);
|
|
22617
|
-
}
|
|
22618
|
-
fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
|
|
22619
|
-
const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
|
|
22620
|
-
if (null === lookupDecision) {
|
|
22621
|
-
logger_logger.warn("Operation cancelled.");
|
|
22622
|
-
process.exit(0);
|
|
22623
|
-
}
|
|
22624
|
-
username = lookupDecision.username;
|
|
22625
|
-
if (lookupDecision.shouldContinue) {
|
|
22626
|
-
logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
|
|
22627
|
-
const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
|
|
22628
|
-
anvilUrl: fallbackUrl,
|
|
22629
|
-
username,
|
|
22630
|
-
includeRemotes: false
|
|
22631
|
-
});
|
|
22632
|
-
logger_logger.progressEnd("detect");
|
|
22633
|
-
for (const c of reverseLookupCandidates)filteredCandidates.push({
|
|
22634
|
-
...c,
|
|
22635
|
-
detectedUrl: fallbackUrl
|
|
22636
|
-
});
|
|
22637
|
-
}
|
|
22638
|
-
}
|
|
22639
|
-
if (filteredCandidates.length > 0) {
|
|
22640
|
-
for (const c of filteredCandidates)logger_logger.verbose(external_chalk_default().gray(` Found: ${formatCandidateLabel(c)}`));
|
|
22641
|
-
if (filteredCandidates.length > 1) logger_logger.verbose(external_chalk_default().yellow(`Found ${filteredCandidates.length} potential app IDs`));
|
|
22642
|
-
}
|
|
22643
|
-
if (useFirst && filteredCandidates.length > 0) {
|
|
22644
|
-
const selected = filteredCandidates[0];
|
|
22645
|
-
finalAppId = selected.appId;
|
|
22646
|
-
if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
|
|
22647
|
-
if (selected.detectedUsername && !username) username = selected.detectedUsername;
|
|
22648
|
-
logger_logger.success("Auto-selected first app ID: " + external_chalk_default().bold(finalAppId));
|
|
22649
|
-
} else {
|
|
22650
|
-
const selected = await selectAppId(filteredCandidates);
|
|
22651
|
-
if (!selected) {
|
|
22652
|
-
logger_logger.error("No app ID provided. Cannot continue without an app ID.");
|
|
22653
|
-
process.exit(1);
|
|
22654
|
-
}
|
|
22655
|
-
finalAppId = selected.appId;
|
|
22656
|
-
if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
|
|
22657
|
-
if (selected.detectedUsername && !username) username = selected.detectedUsername;
|
|
22658
|
-
}
|
|
22659
|
-
}
|
|
22660
|
-
const binding = await getAppAuthBinding(repoPath, finalAppId);
|
|
22661
|
-
if (binding.url && !explicitUrl) {
|
|
22662
|
-
anvilUrl = normalizeAnvilUrl(binding.url);
|
|
22663
|
-
logger_logger.verbose(external_chalk_default().cyan("Using app binding URL: ") + external_chalk_default().bold(anvilUrl));
|
|
22664
|
-
}
|
|
22665
|
-
if (binding.username && !explicitUsername) {
|
|
22666
|
-
username = binding.username;
|
|
22667
|
-
logger_logger.verbose(external_chalk_default().cyan("Using app binding username: ") + external_chalk_default().bold(username));
|
|
22668
|
-
}
|
|
22669
|
-
shouldPersistUsernameBinding = !binding.username && !explicitUsername;
|
|
22670
|
-
if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
|
|
22671
|
-
else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
|
|
22672
|
-
else {
|
|
22673
|
-
const resolvedFallbackUrl = await resolveUrlForFallback();
|
|
22674
|
-
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) {
|
|
22675
23255
|
logger_logger.warn("Operation cancelled.");
|
|
22676
23256
|
process.exit(0);
|
|
22677
23257
|
}
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
|
|
22684
|
-
if (null === resolvedUsername) {
|
|
22685
|
-
logger_logger.warn("Operation cancelled.");
|
|
22686
|
-
process.exit(0);
|
|
22687
|
-
}
|
|
22688
|
-
username = resolvedUsername;
|
|
22689
|
-
if (shouldPersistUsernameBinding && username && (await auth_getAccountsForUrl(anvilUrl)).length > 1) {
|
|
22690
|
-
await setAppAuthBinding(repoPath, finalAppId, {
|
|
22691
|
-
url: anvilUrl,
|
|
22692
|
-
username
|
|
22693
|
-
});
|
|
22694
|
-
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;
|
|
22695
23263
|
}
|
|
22696
|
-
|
|
23264
|
+
const { appId: finalAppId, anvilUrl, username } = account;
|
|
22697
23265
|
if (!await hasTokensForUrl(anvilUrl, username)) {
|
|
22698
23266
|
if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
|
|
22699
23267
|
else logger_logger.error(`Not logged in to ${anvilUrl}`);
|
|
@@ -22750,7 +23318,7 @@ print(json.dumps({"issues": issues}))
|
|
|
22750
23318
|
}
|
|
22751
23319
|
}
|
|
22752
23320
|
function registerWatchCommand(program) {
|
|
22753
|
-
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)=>{
|
|
22754
23322
|
const globalOptions = command.optsWithGlobals();
|
|
22755
23323
|
if (void 0 !== globalOptions.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(globalOptions.verbose);
|
|
22756
23324
|
await handleWatchCommand({
|
|
@@ -22780,6 +23348,7 @@ print(json.dumps({"issues": issues}))
|
|
|
22780
23348
|
logger_logger.success("Logged in as " + external_chalk_default().bold(result.email));
|
|
22781
23349
|
} catch (e) {
|
|
22782
23350
|
logger_logger.error("Error: " + e.message);
|
|
23351
|
+
logErrorCause(e, logger_logger);
|
|
22783
23352
|
process.exit(1);
|
|
22784
23353
|
}
|
|
22785
23354
|
});
|
|
@@ -23355,165 +23924,1141 @@ print(json.dumps({"issues": issues}))
|
|
|
23355
23924
|
function registerConfigureCommand(program, version) {
|
|
23356
23925
|
program.command("configure").description("Guided setup for configuration and login").action(async ()=>{
|
|
23357
23926
|
try {
|
|
23358
|
-
await runConfigureFlow(version, configure_defaultDeps);
|
|
23359
|
-
} catch (e) {
|
|
23360
|
-
logger_logger.error("Error: " + errors_getErrorMessage(e));
|
|
23361
|
-
|
|
23927
|
+
await runConfigureFlow(version, configure_defaultDeps);
|
|
23928
|
+
} catch (e) {
|
|
23929
|
+
logger_logger.error("Error: " + errors_getErrorMessage(e));
|
|
23930
|
+
logErrorCause(e, logger_logger);
|
|
23931
|
+
process.exit(1);
|
|
23932
|
+
}
|
|
23933
|
+
});
|
|
23934
|
+
}
|
|
23935
|
+
function registerDepsCommand(program) {
|
|
23936
|
+
const deps = program.command("deps").description("Manage app dependencies");
|
|
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)=>{
|
|
23938
|
+
try {
|
|
23939
|
+
const projectRoot = process.cwd();
|
|
23940
|
+
const { appId: mainAppId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
|
|
23941
|
+
logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
|
|
23942
|
+
const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
|
|
23943
|
+
username,
|
|
23944
|
+
commit: options.commit,
|
|
23945
|
+
includeDocs: !options.excludeDocs
|
|
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.");
|
|
23948
|
+
if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
|
|
23949
|
+
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
23950
|
+
if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
|
|
23951
|
+
logger_logger.success(`Fetched ${response.dependencies.length} dependencies:`);
|
|
23952
|
+
for (const dep of response.dependencies)logger_logger.info(` - ${dep.package_name}`);
|
|
23953
|
+
} catch (e) {
|
|
23954
|
+
logger_logger.error(e.message);
|
|
23955
|
+
process.exit(1);
|
|
23956
|
+
}
|
|
23957
|
+
});
|
|
23958
|
+
deps.command("status").description("Show cached dependencies").action(async ()=>{
|
|
23959
|
+
try {
|
|
23960
|
+
const cached = await getCachedPackages(process.cwd());
|
|
23961
|
+
if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
|
|
23962
|
+
logger_logger.info("Cached dependencies:");
|
|
23963
|
+
for (const pkg of cached)logger_logger.info(` - ${pkg}`);
|
|
23964
|
+
} catch (e) {
|
|
23965
|
+
logger_logger.error(e.message);
|
|
23966
|
+
process.exit(1);
|
|
23967
|
+
}
|
|
23968
|
+
});
|
|
23969
|
+
deps.command("clear").description("Clear cached dependencies").action(async ()=>{
|
|
23970
|
+
try {
|
|
23971
|
+
await clearDepsCache(process.cwd());
|
|
23972
|
+
logger_logger.success("Cleared .anvil/deps/");
|
|
23973
|
+
} catch (e) {
|
|
23974
|
+
logger_logger.error(e.message);
|
|
23975
|
+
process.exit(1);
|
|
23976
|
+
}
|
|
23977
|
+
});
|
|
23978
|
+
}
|
|
23979
|
+
async function applyDatabaseSchema(appId, options = {}) {
|
|
23980
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
23981
|
+
auth_setRepoContext(projectRoot);
|
|
23982
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
23983
|
+
const appYaml = external_js_yaml_default().load(await external_fs_.promises.readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf8"));
|
|
23984
|
+
const token = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
23985
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db/infer-from-env/apply-schema`, {
|
|
23986
|
+
method: "PUT",
|
|
23987
|
+
headers: {
|
|
23988
|
+
Authorization: `Bearer ${token}`,
|
|
23989
|
+
"Content-Type": "application/json"
|
|
23990
|
+
},
|
|
23991
|
+
body: JSON.stringify({
|
|
23992
|
+
schema: appYaml?.db_schema ?? {},
|
|
23993
|
+
table_id_hints: appYaml?.table_id_hints
|
|
23994
|
+
})
|
|
23995
|
+
});
|
|
23996
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to apply database schema", response.status, await response.text()));
|
|
23997
|
+
return await response.json();
|
|
23998
|
+
}
|
|
23999
|
+
async function confirmSchemaApplication(force = false) {
|
|
24000
|
+
if (force) return true;
|
|
24001
|
+
assertCanPrompt("Applying database schema", "Pass --force to apply without prompting.");
|
|
24002
|
+
return logger_logger.confirm("Apply the app environment's schema to its database? This may delete tables, columns, or data.", true);
|
|
24003
|
+
}
|
|
24004
|
+
function registerDbCommand(program) {
|
|
24005
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
24006
|
+
const db = program.command("db").description("Manage app databases");
|
|
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)=>{
|
|
24008
|
+
try {
|
|
24009
|
+
const projectRoot = process.cwd();
|
|
24010
|
+
const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
|
|
24011
|
+
if (!await confirmSchemaApplication(options.force)) return void logger_logger.info("Schema application cancelled.");
|
|
24012
|
+
const response = await applyDatabaseSchema(appId, {
|
|
24013
|
+
anvilUrl,
|
|
24014
|
+
username,
|
|
24015
|
+
projectRoot
|
|
24016
|
+
});
|
|
24017
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
24018
|
+
data: response
|
|
24019
|
+
});
|
|
24020
|
+
else logger_logger.success("Database schema applied.");
|
|
24021
|
+
} catch (error) {
|
|
24022
|
+
const message = errors_getErrorMessage(error);
|
|
24023
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24024
|
+
error: message
|
|
24025
|
+
});
|
|
24026
|
+
else logger_logger.error(message);
|
|
24027
|
+
process.exit(1);
|
|
24028
|
+
}
|
|
24029
|
+
});
|
|
24030
|
+
}
|
|
24031
|
+
async function ensureEnvironmentUrl(appId, envPid, options = {}) {
|
|
24032
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
24033
|
+
const token = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
24034
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-url`, {
|
|
24035
|
+
method: "POST",
|
|
24036
|
+
headers: {
|
|
24037
|
+
Authorization: `Bearer ${token}`
|
|
24038
|
+
}
|
|
24039
|
+
});
|
|
24040
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to get environment URL", response.status, await response.text()));
|
|
24041
|
+
const { url, expires_at } = await response.json();
|
|
24042
|
+
return {
|
|
24043
|
+
url,
|
|
24044
|
+
expires_at
|
|
24045
|
+
};
|
|
24046
|
+
}
|
|
24047
|
+
async function getTemporaryUplinkKey(appId, envPid, options = {}) {
|
|
24048
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
24049
|
+
const token = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
24050
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-uplink-key`, {
|
|
24051
|
+
method: "POST",
|
|
24052
|
+
headers: {
|
|
24053
|
+
Authorization: `Bearer ${token}`
|
|
24054
|
+
}
|
|
24055
|
+
});
|
|
24056
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to get temporary uplink key", response.status, await response.text()));
|
|
24057
|
+
const { key, url, expires_at } = await response.json();
|
|
24058
|
+
return {
|
|
24059
|
+
key,
|
|
24060
|
+
url,
|
|
24061
|
+
expires_at
|
|
24062
|
+
};
|
|
24063
|
+
}
|
|
24064
|
+
async function resolveEnvironmentContext(options = {}) {
|
|
24065
|
+
const projectRoot = process.cwd();
|
|
24066
|
+
const envPid = await readEnvironmentPid(projectRoot);
|
|
24067
|
+
if (!envPid) throw new Error("No app environment associated with the current directory.");
|
|
24068
|
+
const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, options);
|
|
24069
|
+
return {
|
|
24070
|
+
envPid,
|
|
24071
|
+
anvilUrl,
|
|
24072
|
+
appId,
|
|
24073
|
+
...username ? {
|
|
24074
|
+
username
|
|
24075
|
+
} : {}
|
|
24076
|
+
};
|
|
24077
|
+
}
|
|
24078
|
+
function registerEnvCommand(program) {
|
|
24079
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
24080
|
+
const env = program.command("env").description("Manage app environments");
|
|
24081
|
+
addAppAccountOptions(env.command("url")).description("Get a temporary private app URL for the current environment").action(async (options)=>{
|
|
24082
|
+
try {
|
|
24083
|
+
const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
|
|
24084
|
+
const { url, expires_at } = await ensureEnvironmentUrl(appId, envPid, {
|
|
24085
|
+
anvilUrl,
|
|
24086
|
+
username
|
|
24087
|
+
});
|
|
24088
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
24089
|
+
data: {
|
|
24090
|
+
url,
|
|
24091
|
+
expires_at
|
|
24092
|
+
}
|
|
24093
|
+
});
|
|
24094
|
+
else logger_logger.info(`URL: ${url}\nExpires at: ${expires_at}`);
|
|
24095
|
+
} catch (error) {
|
|
24096
|
+
const message = errors_getErrorMessage(error);
|
|
24097
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24098
|
+
error: message
|
|
24099
|
+
});
|
|
24100
|
+
else logger_logger.error(message);
|
|
24101
|
+
process.exit(1);
|
|
24102
|
+
}
|
|
24103
|
+
});
|
|
24104
|
+
addAppAccountOptions(env.command("uplink")).description("Get a temporary uplink key for the current environment").action(async (options)=>{
|
|
24105
|
+
try {
|
|
24106
|
+
const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
|
|
24107
|
+
const { key, url, expires_at } = await getTemporaryUplinkKey(appId, envPid, {
|
|
24108
|
+
anvilUrl,
|
|
24109
|
+
username
|
|
24110
|
+
});
|
|
24111
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
24112
|
+
data: {
|
|
24113
|
+
key,
|
|
24114
|
+
url,
|
|
24115
|
+
expires_at
|
|
24116
|
+
}
|
|
24117
|
+
});
|
|
24118
|
+
else logger_logger.info(`Key: ${key}\nURL: ${url}\nExpires at: ${expires_at}`);
|
|
24119
|
+
} catch (error) {
|
|
24120
|
+
const message = errors_getErrorMessage(error);
|
|
24121
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24122
|
+
error: message
|
|
24123
|
+
});
|
|
24124
|
+
else logger_logger.error(message);
|
|
24125
|
+
process.exit(1);
|
|
24126
|
+
}
|
|
24127
|
+
});
|
|
24128
|
+
}
|
|
24129
|
+
const external_events_namespaceObject = require("events");
|
|
24130
|
+
function replError(error) {
|
|
24131
|
+
if ("string" == typeof error) return {
|
|
24132
|
+
message: error
|
|
24133
|
+
};
|
|
24134
|
+
if (error && "object" == typeof error) {
|
|
24135
|
+
const value = error;
|
|
24136
|
+
return {
|
|
24137
|
+
message: String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value)),
|
|
24138
|
+
..."string" == typeof value.type ? {
|
|
24139
|
+
type: value.type
|
|
24140
|
+
} : {},
|
|
24141
|
+
...Array.isArray(value.trace) ? {
|
|
24142
|
+
trace: value.trace.filter((frame)=>Array.isArray(frame) && "string" == typeof frame[0] && "number" == typeof frame[1])
|
|
24143
|
+
} : {}
|
|
24144
|
+
};
|
|
24145
|
+
}
|
|
24146
|
+
return {
|
|
24147
|
+
message: String(error)
|
|
24148
|
+
};
|
|
24149
|
+
}
|
|
24150
|
+
class ReplServerError extends Error {
|
|
24151
|
+
detail;
|
|
24152
|
+
constructor(detail){
|
|
24153
|
+
super(detail.message), this.detail = detail;
|
|
24154
|
+
}
|
|
24155
|
+
}
|
|
24156
|
+
async function runRepl(appId, envPid, options) {
|
|
24157
|
+
const interrupted = ()=>new Error("REPL interrupted.");
|
|
24158
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
24159
|
+
const token = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
24160
|
+
const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/ws`);
|
|
24161
|
+
url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
|
|
24162
|
+
if (options.signal?.aborted) throw interrupted();
|
|
24163
|
+
const ws = new (external_ws_default())(url, {
|
|
24164
|
+
headers: {
|
|
24165
|
+
Authorization: `Bearer ${token}`
|
|
24166
|
+
},
|
|
24167
|
+
handshakeTimeout: 30000
|
|
24168
|
+
});
|
|
24169
|
+
const controller = new AbortController();
|
|
24170
|
+
let repl;
|
|
24171
|
+
let nextId = 1;
|
|
24172
|
+
let output = "";
|
|
24173
|
+
let keepalive;
|
|
24174
|
+
let pongTimeout;
|
|
24175
|
+
let closeTimeout;
|
|
24176
|
+
const startupTimeout = setTimeout(()=>{
|
|
24177
|
+
controller.abort(new Error("Timed out waiting for the REPL to start."));
|
|
24178
|
+
}, 30000);
|
|
24179
|
+
const onAbort = ()=>controller.abort(interrupted());
|
|
24180
|
+
const markResponsive = ()=>{
|
|
24181
|
+
clearTimeout(pongTimeout);
|
|
24182
|
+
pongTimeout = void 0;
|
|
24183
|
+
};
|
|
24184
|
+
options.signal?.addEventListener("abort", onAbort, {
|
|
24185
|
+
once: true
|
|
24186
|
+
});
|
|
24187
|
+
ws.on("pong", markResponsive);
|
|
24188
|
+
ws.on("unexpected-response", (request, response)=>{
|
|
24189
|
+
response.resume();
|
|
24190
|
+
controller.abort(new Error(`REPL websocket handshake failed (${response.statusCode} ${response.statusMessage}).`));
|
|
24191
|
+
request.destroy();
|
|
24192
|
+
});
|
|
24193
|
+
ws.on("error", (error)=>controller.abort(new Error(`REPL websocket connection failed: ${error.message}`)));
|
|
24194
|
+
ws.on("close", (code, reason)=>{
|
|
24195
|
+
clearTimeout(closeTimeout);
|
|
24196
|
+
controller.abort(new Error(`REPL websocket disconnected unexpectedly (code ${code}${reason.length ? `: ${reason}` : ""}).`));
|
|
24197
|
+
});
|
|
24198
|
+
const messages = (0, external_events_namespaceObject.on)(ws, "message", {
|
|
24199
|
+
signal: controller.signal
|
|
24200
|
+
});
|
|
24201
|
+
function send(message) {
|
|
24202
|
+
const id = nextId++;
|
|
24203
|
+
ws.send(JSON.stringify({
|
|
24204
|
+
...message,
|
|
24205
|
+
id
|
|
24206
|
+
}), (error)=>{
|
|
24207
|
+
if (error) controller.abort(new Error(`REPL websocket send failed: ${error.message}`));
|
|
24208
|
+
});
|
|
24209
|
+
return id;
|
|
24210
|
+
}
|
|
24211
|
+
async function readUntil(matches) {
|
|
24212
|
+
while(true){
|
|
24213
|
+
const { value, done } = await messages.next();
|
|
24214
|
+
options.signal?.throwIfAborted();
|
|
24215
|
+
if (done) throw new Error("REPL websocket message stream ended unexpectedly.");
|
|
24216
|
+
markResponsive();
|
|
24217
|
+
let message;
|
|
24218
|
+
try {
|
|
24219
|
+
message = JSON.parse(value[0].toString());
|
|
24220
|
+
if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
|
|
24221
|
+
} catch {
|
|
24222
|
+
throw new Error("Invalid REPL websocket message.");
|
|
24223
|
+
}
|
|
24224
|
+
if ("REPL_UPDATE" === message.event) {
|
|
24225
|
+
if (!repl || message.repl !== repl) continue;
|
|
24226
|
+
if ("string" == typeof message.output) {
|
|
24227
|
+
output += message.output;
|
|
24228
|
+
options.onOutput?.(message.output);
|
|
24229
|
+
options.signal?.throwIfAborted();
|
|
24230
|
+
}
|
|
24231
|
+
}
|
|
24232
|
+
if (message.error) throw new ReplServerError(replError(message.error));
|
|
24233
|
+
if ("REPL_UPDATE" === message.event && message.terminated) throw new Error("REPL terminated unexpectedly.");
|
|
24234
|
+
if (matches(message)) return message;
|
|
24235
|
+
}
|
|
24236
|
+
}
|
|
24237
|
+
try {
|
|
24238
|
+
await (0, external_events_namespaceObject.once)(ws, "open", {
|
|
24239
|
+
signal: controller.signal
|
|
24240
|
+
});
|
|
24241
|
+
const launchId = send({
|
|
24242
|
+
cmd: "LAUNCH_REPL"
|
|
24243
|
+
});
|
|
24244
|
+
const launched = await readUntil((message)=>message.id === launchId);
|
|
24245
|
+
if ("string" != typeof launched.repl || !launched.repl) throw new Error("Invalid REPL launch response: missing REPL ID.");
|
|
24246
|
+
repl = launched.repl;
|
|
24247
|
+
keepalive = setInterval(()=>{
|
|
24248
|
+
try {
|
|
24249
|
+
send({
|
|
24250
|
+
cmd: "REPL_KEEPALIVE",
|
|
24251
|
+
repl
|
|
24252
|
+
});
|
|
24253
|
+
pongTimeout = setTimeout(()=>controller.abort(new Error("REPL websocket heartbeat timed out.")), 10000);
|
|
24254
|
+
ws.ping();
|
|
24255
|
+
} catch (error) {
|
|
24256
|
+
controller.abort(error);
|
|
24257
|
+
}
|
|
24258
|
+
}, 20000);
|
|
24259
|
+
const isReady = (message)=>"REPL_UPDATE" === message.event && true === message.ready;
|
|
24260
|
+
await readUntil(isReady);
|
|
24261
|
+
clearTimeout(startupTimeout);
|
|
24262
|
+
send({
|
|
24263
|
+
cmd: "REPL_COMMAND",
|
|
24264
|
+
repl,
|
|
24265
|
+
command: options.code
|
|
24266
|
+
});
|
|
24267
|
+
await readUntil(isReady);
|
|
24268
|
+
return {
|
|
24269
|
+
output
|
|
24270
|
+
};
|
|
24271
|
+
} catch (error) {
|
|
24272
|
+
if (error instanceof ReplServerError) return {
|
|
24273
|
+
output,
|
|
24274
|
+
error: error.detail
|
|
24275
|
+
};
|
|
24276
|
+
const failure = controller.signal.aborted ? controller.signal.reason : error;
|
|
24277
|
+
throw failure instanceof Error ? failure : new Error(String(failure));
|
|
24278
|
+
} finally{
|
|
24279
|
+
clearTimeout(startupTimeout);
|
|
24280
|
+
clearInterval(keepalive);
|
|
24281
|
+
clearTimeout(pongTimeout);
|
|
24282
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
24283
|
+
controller.abort();
|
|
24284
|
+
await messages.return?.();
|
|
24285
|
+
if (ws.readyState === external_ws_default().OPEN) {
|
|
24286
|
+
if (repl) ws.send(JSON.stringify({
|
|
24287
|
+
id: nextId++,
|
|
24288
|
+
cmd: "TERMINATE_REPL",
|
|
24289
|
+
repl
|
|
24290
|
+
}), ()=>{});
|
|
24291
|
+
ws.close();
|
|
24292
|
+
} else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
|
|
24293
|
+
if (ws.readyState !== external_ws_default().CLOSED) {
|
|
24294
|
+
closeTimeout = setTimeout(()=>ws.terminate(), 1000);
|
|
24295
|
+
closeTimeout.unref();
|
|
24296
|
+
}
|
|
24297
|
+
}
|
|
24298
|
+
}
|
|
24299
|
+
function writeJsonReplOutput(output) {
|
|
24300
|
+
process.stdout.write(JSON.stringify({
|
|
24301
|
+
type: "repl_output",
|
|
24302
|
+
output,
|
|
24303
|
+
timestamp: new Date().toISOString()
|
|
24304
|
+
}) + "\n");
|
|
24305
|
+
}
|
|
24306
|
+
function registerReplCommand(program) {
|
|
24307
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
24308
|
+
addAppAccountOptions(program.command("repl <code>")).description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
|
|
24309
|
+
Examples:
|
|
24310
|
+
anvil repl '1 + 2'
|
|
24311
|
+
anvil --json repl '1 + 2'
|
|
24312
|
+
anvil repl - < script.py
|
|
24313
|
+
`).action(async (code, options)=>{
|
|
24314
|
+
const controller = new AbortController();
|
|
24315
|
+
const onInterrupt = ()=>controller.abort();
|
|
24316
|
+
try {
|
|
24317
|
+
const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
|
|
24318
|
+
if ("-" === code) {
|
|
24319
|
+
const chunks = [];
|
|
24320
|
+
for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
24321
|
+
code = Buffer.concat(chunks).toString("utf8");
|
|
24322
|
+
}
|
|
24323
|
+
process.on("SIGINT", onInterrupt);
|
|
24324
|
+
const response = await runRepl(appId, envPid, {
|
|
24325
|
+
code,
|
|
24326
|
+
anvilUrl,
|
|
24327
|
+
username,
|
|
24328
|
+
signal: controller.signal,
|
|
24329
|
+
onOutput: getGlobalOutputConfig().jsonMode ? writeJsonReplOutput : (output)=>{
|
|
24330
|
+
process.stdout.write(output);
|
|
24331
|
+
}
|
|
24332
|
+
});
|
|
24333
|
+
const error = response.error;
|
|
24334
|
+
const message = error ? `${error.type ? `${error.type}: ` : ""}${error.message}` : void 0;
|
|
24335
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(!error, {
|
|
24336
|
+
data: response,
|
|
24337
|
+
error: message
|
|
24338
|
+
});
|
|
24339
|
+
else if (error) {
|
|
24340
|
+
process.stderr.write(`${message}\n`);
|
|
24341
|
+
for (const [file, line] of error.trace ?? [])process.stderr.write(` at ${file}:${line}\n`);
|
|
24342
|
+
}
|
|
24343
|
+
if (error) process.exitCode = 1;
|
|
24344
|
+
} catch (error) {
|
|
24345
|
+
const message = errors_getErrorMessage(error);
|
|
24346
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24347
|
+
error: message
|
|
24348
|
+
});
|
|
24349
|
+
else logger_logger.error(message);
|
|
24350
|
+
process.exitCode = controller.signal.aborted ? 130 : 1;
|
|
24351
|
+
} finally{
|
|
24352
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
23362
24353
|
}
|
|
23363
24354
|
});
|
|
23364
24355
|
}
|
|
23365
|
-
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
const
|
|
23369
|
-
|
|
23370
|
-
|
|
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
|
+
})
|
|
23371
24372
|
});
|
|
23372
|
-
|
|
24373
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to set secret", response.status, await response.text()));
|
|
24374
|
+
return await response.json();
|
|
23373
24375
|
}
|
|
23374
|
-
function
|
|
23375
|
-
const
|
|
23376
|
-
|
|
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)=>{
|
|
23377
24491
|
try {
|
|
23378
24492
|
const projectRoot = process.cwd();
|
|
23379
|
-
const anvilUrl =
|
|
23380
|
-
const
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
process.exit(1);
|
|
23385
|
-
}
|
|
23386
|
-
logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
|
|
23387
|
-
const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
|
|
23388
|
-
commit: options.commit,
|
|
23389
|
-
includeDocs: !options.excludeDocs
|
|
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
|
|
23390
24498
|
});
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23408
|
-
logger_logger.
|
|
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);
|
|
23409
24523
|
process.exit(1);
|
|
23410
24524
|
}
|
|
23411
24525
|
});
|
|
23412
|
-
|
|
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)=>{
|
|
23413
24527
|
try {
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23417
|
-
|
|
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);
|
|
23418
24557
|
process.exit(1);
|
|
23419
24558
|
}
|
|
23420
24559
|
});
|
|
23421
24560
|
}
|
|
23422
|
-
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
|
|
23427
|
-
|
|
23428
|
-
}
|
|
23429
|
-
|
|
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;
|
|
23430
24571
|
}
|
|
23431
|
-
async function
|
|
23432
|
-
|
|
23433
|
-
|
|
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, {
|
|
23434
24651
|
headers: {
|
|
23435
24652
|
Authorization: `Bearer ${token}`
|
|
23436
|
-
}
|
|
24653
|
+
},
|
|
24654
|
+
handshakeTimeout: 30000
|
|
23437
24655
|
});
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
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
|
+
});
|
|
23441
24757
|
}
|
|
23442
|
-
const
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
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();
|
|
23448
24814
|
});
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
async function writeTableMappings(projectRoot, mappings) {
|
|
23454
|
-
const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
|
|
23455
|
-
await promises_default().mkdir(external_path_default().dirname(filePath), {
|
|
23456
|
-
recursive: true
|
|
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}` : ""}`));
|
|
23457
24819
|
});
|
|
23458
|
-
|
|
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();
|
|
23459
24936
|
}
|
|
23460
|
-
async function
|
|
23461
|
-
const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
|
|
24937
|
+
async function getBranch(git) {
|
|
23462
24938
|
try {
|
|
23463
|
-
|
|
23464
|
-
return JSON.parse(content);
|
|
24939
|
+
return await git.getCurrentBranch();
|
|
23465
24940
|
} catch {
|
|
23466
|
-
|
|
24941
|
+
throw new Error("HEAD isn't on a branch. Check out the branch you've pushed to Anvil.");
|
|
23467
24942
|
}
|
|
23468
24943
|
}
|
|
23469
|
-
async function
|
|
23470
|
-
const
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
|
|
23474
|
-
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
|
|
23481
|
-
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
} catch (e) {
|
|
23494
|
-
logger_logger.error(e.message);
|
|
23495
|
-
process.exit(1);
|
|
23496
|
-
}
|
|
23497
|
-
});
|
|
23498
|
-
tables.command("status").description("Show cached table mappings").action(async ()=>{
|
|
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;
|
|
23499
24968
|
try {
|
|
23500
|
-
|
|
23501
|
-
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
logger_logger.error(e.message);
|
|
23506
|
-
process.exit(1);
|
|
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.");
|
|
23507
24974
|
}
|
|
23508
|
-
|
|
23509
|
-
|
|
23510
|
-
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
|
|
23514
|
-
|
|
23515
|
-
|
|
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
|
+
});
|
|
23516
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);
|
|
23517
25062
|
});
|
|
23518
25063
|
}
|
|
23519
25064
|
const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
|
|
@@ -23927,6 +25472,7 @@ print(json.dumps({"issues": issues}))
|
|
|
23927
25472
|
});
|
|
23928
25473
|
const watchCommand = registerWatchCommand(program);
|
|
23929
25474
|
registerCheckoutCommand(program);
|
|
25475
|
+
registerCreateCommand(program);
|
|
23930
25476
|
registerGitAuthCommand(program);
|
|
23931
25477
|
registerGitCredentialCommand(program);
|
|
23932
25478
|
registerLoginCommand(program);
|
|
@@ -23936,7 +25482,11 @@ print(json.dumps({"issues": issues}))
|
|
|
23936
25482
|
registerVersionCommand(program, VERSION);
|
|
23937
25483
|
registerConfigureCommand(program, VERSION);
|
|
23938
25484
|
registerDepsCommand(program);
|
|
23939
|
-
|
|
25485
|
+
registerDbCommand(program);
|
|
25486
|
+
registerEnvCommand(program);
|
|
25487
|
+
registerReplCommand(program);
|
|
25488
|
+
registerSecretCommand(program);
|
|
25489
|
+
registerTestCommand(program);
|
|
23940
25490
|
program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
|
|
23941
25491
|
await handleUpdateCommand();
|
|
23942
25492
|
});
|