@anvil-works/anvil-cli 0.7.0-canary.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/dist/SavePathRouter.d.ts +2 -5
- package/dist/SavePathRouter.d.ts.map +1 -1
- package/dist/WatchSession.d.ts +21 -0
- package/dist/WatchSession.d.ts.map +1 -1
- package/dist/api.d.ts +4 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/cli.js +2569 -497
- package/dist/commands/appDetection.d.ts +2 -0
- package/dist/commands/appDetection.d.ts.map +1 -0
- package/dist/commands/convertTemplate.d.ts +25 -0
- package/dist/commands/convertTemplate.d.ts.map +1 -0
- package/dist/commands/deps.d.ts +7 -0
- package/dist/commands/deps.d.ts.map +1 -0
- package/dist/commands/gitAuth.d.ts +89 -0
- package/dist/commands/gitAuth.d.ts.map +1 -0
- package/dist/commands/gitCredential.d.ts.map +1 -1
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/tables.d.ts +7 -0
- package/dist/commands/tables.d.ts.map +1 -0
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/componentBuiltinTypes.d.ts.map +1 -1
- package/dist/componentName.d.ts +5 -0
- package/dist/componentName.d.ts.map +1 -0
- package/dist/componentTypeSpec.d.ts +8 -0
- package/dist/componentTypeSpec.d.ts.map +1 -0
- package/dist/formSpecContext.d.ts +4 -0
- package/dist/formSpecContext.d.ts.map +1 -0
- package/dist/formTemplateValidation.d.ts +13 -0
- package/dist/formTemplateValidation.d.ts.map +1 -0
- package/dist/index.js +2050 -427
- package/dist/program.d.ts.map +1 -1
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/deps.d.ts +106 -0
- package/dist/services/deps.d.ts.map +1 -0
- package/dist/services/git-auth.d.ts.map +1 -1
- package/dist/services/git.d.ts +25 -1
- package/dist/services/git.d.ts.map +1 -1
- package/dist/services/gitignore.d.ts +2 -0
- package/dist/services/gitignore.d.ts.map +1 -0
- package/dist/services/tables.d.ts +27 -0
- package/dist/services/tables.d.ts.map +1 -0
- package/dist/validateFormTemplateHtml.d.ts +10 -0
- package/dist/validateFormTemplateHtml.d.ts.map +1 -0
- package/dist/validatePython.d.ts +22 -0
- package/dist/validatePython.d.ts.map +1 -0
- package/dist/validators.d.ts +8 -8
- package/dist/validators.d.ts.map +1 -1
- package/dist/watch/ConflictResolver.d.ts +8 -1
- package/dist/watch/ConflictResolver.d.ts.map +1 -1
- package/dist/watch/SaveProcessor.d.ts +10 -1
- package/dist/watch/SaveProcessor.d.ts.map +1 -1
- package/dist/watch/SyncManager.d.ts +7 -2
- package/dist/watch/SyncManager.d.ts.map +1 -1
- package/dist/watch/WebSocketClient.d.ts +21 -0
- package/dist/watch/WebSocketClient.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -7371,6 +7371,7 @@ var __webpack_exports__ = {};
|
|
|
7371
7371
|
setConfig: ()=>setConfig,
|
|
7372
7372
|
getAccountsForUrl: ()=>auth_getAccountsForUrl,
|
|
7373
7373
|
AppIdWithContext: ()=>anvil_api_namespaceObject.AppIdWithContext,
|
|
7374
|
+
inspectCredentialHelperCommand: ()=>inspectCredentialHelperCommand,
|
|
7374
7375
|
executeCheckout: ()=>executeCheckout,
|
|
7375
7376
|
validateCheckoutDestination: ()=>validateCheckoutDestination,
|
|
7376
7377
|
DeviceAuthorizationResponse: ()=>oauth_login_namespaceObject.DeviceAuthorizationResponse,
|
|
@@ -7386,7 +7387,9 @@ var __webpack_exports__ = {};
|
|
|
7386
7387
|
verifyAndStoreTokens: ()=>auth_verifyAndStoreTokens,
|
|
7387
7388
|
formatValidationPath: ()=>formatValidationPath,
|
|
7388
7389
|
resolveAnvilUrl: ()=>resolveAnvilUrl,
|
|
7390
|
+
buildGitAuthDoctorReport: ()=>buildGitAuthDoctorReport,
|
|
7389
7391
|
parseCheckoutInput: ()=>parseCheckoutInput,
|
|
7392
|
+
formatGitAuthDoctorReport: ()=>formatGitAuthDoctorReport,
|
|
7390
7393
|
resetConfig: ()=>resetConfig,
|
|
7391
7394
|
FormTemplateValidationResult: ()=>validators_namespaceObject.FormTemplateValidationResult,
|
|
7392
7395
|
createWatchDiagnostic: ()=>createWatchDiagnostic,
|
|
@@ -7400,6 +7403,7 @@ var __webpack_exports__ = {};
|
|
|
7400
7403
|
CheckoutAppListItem: ()=>anvil_api_namespaceObject.CheckoutAppListItem,
|
|
7401
7404
|
getConfig: ()=>config_getConfig,
|
|
7402
7405
|
CheckoutExecutionDeps: ()=>checkout_namespaceObject.CheckoutExecutionDeps,
|
|
7406
|
+
validateFormTemplateHtml: ()=>validateFormTemplateHtml,
|
|
7403
7407
|
ValidateBranchSyncStatusOptions: ()=>validation_namespaceObject.ValidateBranchSyncStatusOptions,
|
|
7404
7408
|
BranchSyncStatus: ()=>validation_namespaceObject.BranchSyncStatus,
|
|
7405
7409
|
getLatestVersion: ()=>getLatestVersion,
|
|
@@ -7449,6 +7453,7 @@ var __webpack_exports__ = {};
|
|
|
7449
7453
|
executeGitCredentialOperation: ()=>executeGitCredentialOperation,
|
|
7450
7454
|
CheckoutResult: ()=>checkout_namespaceObject.CheckoutResult,
|
|
7451
7455
|
ParsedCheckoutInput: ()=>checkout_namespaceObject.ParsedCheckoutInput,
|
|
7456
|
+
parseCredentialHelperCommand: ()=>parseCredentialHelperCommand,
|
|
7452
7457
|
clearInMemoryTokens: ()=>clearInMemoryTokens,
|
|
7453
7458
|
getDefaultDestinationDirectory: ()=>getDefaultDestinationDirectory,
|
|
7454
7459
|
lookupByCommit: ()=>lookupByCommit,
|
|
@@ -13693,7 +13698,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13693
13698
|
}
|
|
13694
13699
|
function getCredentialHelperModulePath(options) {
|
|
13695
13700
|
const currentModulePath = options?.currentModulePath ?? __filename;
|
|
13696
|
-
if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
|
|
13701
|
+
if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
|
|
13697
13702
|
return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
|
|
13698
13703
|
}
|
|
13699
13704
|
async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
|
|
@@ -13880,6 +13885,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13880
13885
|
function auth_setRepoContext(repoPath) {
|
|
13881
13886
|
repoContext = repoPath;
|
|
13882
13887
|
}
|
|
13888
|
+
const inFlightRefreshes = new Map();
|
|
13883
13889
|
async function auth_verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
|
|
13884
13890
|
try {
|
|
13885
13891
|
const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
|
|
@@ -13933,53 +13939,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13933
13939
|
}
|
|
13934
13940
|
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
|
|
13935
13941
|
const normalized = config_normalizeAnvilUrl(anvilUrl);
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
|
|
13939
|
-
else {
|
|
13940
|
-
repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13941
|
-
tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
|
|
13942
|
-
}
|
|
13942
|
+
const source = await resolveTokenSource(normalized, username);
|
|
13943
|
+
const tokens = getSourceTokens(source);
|
|
13943
13944
|
if (!tokens.authToken && !tokens.refreshToken) throw createAuthError.required("Not logged in. Please log in first.");
|
|
13944
13945
|
const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
|
|
13945
|
-
if (tokens.refreshToken && isExpired)
|
|
13946
|
+
if (tokens.refreshToken && isExpired) {
|
|
13947
|
+
const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
|
|
13948
|
+
const existingRefresh = inFlightRefreshes.get(refreshKey);
|
|
13949
|
+
if (existingRefresh) return existingRefresh;
|
|
13950
|
+
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
|
|
13951
|
+
inFlightRefreshes.delete(refreshKey);
|
|
13952
|
+
});
|
|
13953
|
+
inFlightRefreshes.set(refreshKey, refreshPromise);
|
|
13954
|
+
return refreshPromise;
|
|
13955
|
+
}
|
|
13956
|
+
if (tokens.authToken) return tokens.authToken;
|
|
13957
|
+
throw createAuthError.required("No valid token available");
|
|
13958
|
+
}
|
|
13959
|
+
async function resolveTokenSource(normalizedUrl, username) {
|
|
13960
|
+
if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
|
|
13961
|
+
type: "memory",
|
|
13962
|
+
auth: inMemoryAuth
|
|
13963
|
+
};
|
|
13964
|
+
const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13965
|
+
if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
|
|
13966
|
+
type: "repo",
|
|
13967
|
+
auth: repoAuth,
|
|
13968
|
+
repoPath: repoContext
|
|
13969
|
+
};
|
|
13970
|
+
const tokens = getTokensAutoSelect(normalizedUrl, username);
|
|
13971
|
+
return {
|
|
13972
|
+
type: "store",
|
|
13973
|
+
tokens,
|
|
13974
|
+
accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
|
|
13975
|
+
};
|
|
13976
|
+
}
|
|
13977
|
+
function getSourceTokens(source) {
|
|
13978
|
+
return "store" === source.type ? source.tokens : source.auth.tokens;
|
|
13979
|
+
}
|
|
13980
|
+
function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
|
|
13981
|
+
if (username) return username;
|
|
13982
|
+
const urlTokens = getUrlTokens(normalizedUrl);
|
|
13983
|
+
if (!urlTokens) return;
|
|
13984
|
+
const accounts = Object.keys(urlTokens);
|
|
13985
|
+
if (1 === accounts.length) return accounts[0];
|
|
13986
|
+
return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
|
|
13987
|
+
}
|
|
13988
|
+
function getRefreshKey(normalizedUrl, username, source, refreshToken) {
|
|
13989
|
+
switch(source.type){
|
|
13990
|
+
case "memory":
|
|
13991
|
+
return `memory:${normalizedUrl}:${source.auth.username}`;
|
|
13992
|
+
case "repo":
|
|
13993
|
+
return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
|
|
13994
|
+
case "store":
|
|
13995
|
+
return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
|
|
13996
|
+
}
|
|
13997
|
+
}
|
|
13998
|
+
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
|
|
13999
|
+
try {
|
|
14000
|
+
const tokens = getSourceTokens(source);
|
|
13946
14001
|
const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
|
|
13947
|
-
const tokenData = await refreshAccessToken(
|
|
13948
|
-
const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
|
|
14002
|
+
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
|
|
13949
14003
|
const newTokens = {
|
|
13950
14004
|
authToken: tokenData.access_token,
|
|
13951
14005
|
refreshToken: tokenData.refresh_token,
|
|
13952
|
-
authTokenExpiresAt:
|
|
14006
|
+
authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
|
|
13953
14007
|
clientId
|
|
13954
14008
|
};
|
|
13955
|
-
if (
|
|
13956
|
-
else if (
|
|
14009
|
+
if ("memory" === source.type) source.auth.tokens = newTokens;
|
|
14010
|
+
else if ("repo" === source.type) {
|
|
13957
14011
|
const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
|
|
13958
14012
|
if (!encryptionKey) throw createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
|
|
13959
|
-
|
|
13960
|
-
await writeAuthToFile(
|
|
14013
|
+
source.auth.tokens = newTokens;
|
|
14014
|
+
await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
|
|
13961
14015
|
} else {
|
|
13962
|
-
|
|
13963
|
-
|
|
13964
|
-
const urlTokens = getUrlTokens(normalized);
|
|
13965
|
-
if (urlTokens) {
|
|
13966
|
-
const accounts = Object.keys(urlTokens);
|
|
13967
|
-
accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
|
|
13968
|
-
}
|
|
13969
|
-
}
|
|
13970
|
-
if (!accountUsername) {
|
|
13971
|
-
await clearTokensForAccount(normalized, username, tokens.refreshToken);
|
|
14016
|
+
if (!source.accountUsername) {
|
|
14017
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13972
14018
|
throw createAuthError.required("Cannot determine which account to refresh. Please log in again.");
|
|
13973
14019
|
}
|
|
13974
|
-
setAccountTokens(
|
|
14020
|
+
setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
|
|
13975
14021
|
}
|
|
13976
14022
|
return tokenData.access_token;
|
|
13977
14023
|
} catch (e) {
|
|
13978
|
-
await clearTokensForAccount(
|
|
14024
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13979
14025
|
throw e;
|
|
13980
14026
|
}
|
|
13981
|
-
if (tokens.authToken) return tokens.authToken;
|
|
13982
|
-
throw createAuthError.required("No valid token available");
|
|
13983
14027
|
}
|
|
13984
14028
|
async function clearTokensForAccount(url, username, refreshToken) {
|
|
13985
14029
|
if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
|
|
@@ -14309,6 +14353,324 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14309
14353
|
} catch (_e) {}
|
|
14310
14354
|
return out;
|
|
14311
14355
|
}
|
|
14356
|
+
const promises_namespaceObject = require("fs/promises");
|
|
14357
|
+
var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
|
|
14358
|
+
function normalizeLineEndings(content) {
|
|
14359
|
+
return content.replace(/\r\n/g, "\n");
|
|
14360
|
+
}
|
|
14361
|
+
function pythonifyName(name) {
|
|
14362
|
+
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
14363
|
+
}
|
|
14364
|
+
function extractPythonName(relativePath, skipParts = 1) {
|
|
14365
|
+
const parts = relativePath.slice(0, -3).split("/");
|
|
14366
|
+
const isPackage = relativePath.endsWith("__init__.py");
|
|
14367
|
+
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
14368
|
+
return relevantParts.map(pythonifyName).join(".");
|
|
14369
|
+
}
|
|
14370
|
+
function deepEqual(a, b) {
|
|
14371
|
+
if (a === b) return true;
|
|
14372
|
+
if (null == a || null == b) return false;
|
|
14373
|
+
if (typeof a !== typeof b) return false;
|
|
14374
|
+
if ("object" != typeof a) return a === b;
|
|
14375
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
14376
|
+
if (Array.isArray(a)) {
|
|
14377
|
+
if (a.length !== b.length) return false;
|
|
14378
|
+
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14379
|
+
}
|
|
14380
|
+
const keysA = Object.keys(a);
|
|
14381
|
+
const keysB = Object.keys(b);
|
|
14382
|
+
if (keysA.length !== keysB.length) return false;
|
|
14383
|
+
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
14384
|
+
}
|
|
14385
|
+
function parseHtmlWithFrontmatter(htmlContent) {
|
|
14386
|
+
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14387
|
+
const match = htmlContent.match(frontmatterPattern);
|
|
14388
|
+
if (match) {
|
|
14389
|
+
const yamlStr = match[1];
|
|
14390
|
+
const html = match[2];
|
|
14391
|
+
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
14392
|
+
return {
|
|
14393
|
+
frontmatter,
|
|
14394
|
+
html
|
|
14395
|
+
};
|
|
14396
|
+
}
|
|
14397
|
+
return {
|
|
14398
|
+
frontmatter: {},
|
|
14399
|
+
html: htmlContent
|
|
14400
|
+
};
|
|
14401
|
+
}
|
|
14402
|
+
const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
|
|
14403
|
+
async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
|
|
14404
|
+
const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
|
|
14405
|
+
try {
|
|
14406
|
+
const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
|
|
14407
|
+
if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
|
|
14408
|
+
const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
|
|
14409
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
14410
|
+
return true;
|
|
14411
|
+
} catch (error) {
|
|
14412
|
+
if ("ENOENT" !== error.code) throw error;
|
|
14413
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
14414
|
+
return true;
|
|
14415
|
+
}
|
|
14416
|
+
}
|
|
14417
|
+
const ANVIL_DEPS_DIR = ".anvil/deps";
|
|
14418
|
+
const ANVIL_DEPS_MANIFEST = "manifest.json";
|
|
14419
|
+
const DEPS_MANIFEST_VERSION = 1;
|
|
14420
|
+
function formatFetchError(prefix, status, errorBody) {
|
|
14421
|
+
if (!errorBody) return `${prefix}: ${status}`;
|
|
14422
|
+
try {
|
|
14423
|
+
const parsed = JSON.parse(errorBody);
|
|
14424
|
+
if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
|
|
14425
|
+
} catch {}
|
|
14426
|
+
return `${prefix}: ${status} ${errorBody}`;
|
|
14427
|
+
}
|
|
14428
|
+
async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
|
|
14429
|
+
const { commit, includeDocs = true, knownDependencies = [] } = options;
|
|
14430
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
14431
|
+
const body = {
|
|
14432
|
+
include_docs: includeDocs,
|
|
14433
|
+
known_dependencies: knownDependencies
|
|
14434
|
+
};
|
|
14435
|
+
if (commit) body.commit = commit;
|
|
14436
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
|
|
14437
|
+
method: "POST",
|
|
14438
|
+
headers: {
|
|
14439
|
+
Authorization: `Bearer ${token}`,
|
|
14440
|
+
"Content-Type": "application/json"
|
|
14441
|
+
},
|
|
14442
|
+
body: JSON.stringify(body)
|
|
14443
|
+
});
|
|
14444
|
+
if (!resp.ok) {
|
|
14445
|
+
const errorBody = await resp.text();
|
|
14446
|
+
throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
|
|
14447
|
+
}
|
|
14448
|
+
return await resp.json();
|
|
14449
|
+
}
|
|
14450
|
+
async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
|
|
14451
|
+
const includeDocs = options.includeDocs ?? true;
|
|
14452
|
+
const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
|
|
14453
|
+
const response = await fetchAllDependencies(appId, anvilUrl, {
|
|
14454
|
+
commit: options.commit,
|
|
14455
|
+
includeDocs,
|
|
14456
|
+
knownDependencies
|
|
14457
|
+
});
|
|
14458
|
+
if (!response.resolved_dependencies.length) {
|
|
14459
|
+
await clearDepsCache(projectRoot);
|
|
14460
|
+
return {
|
|
14461
|
+
response,
|
|
14462
|
+
addedGitignoreEntry: false
|
|
14463
|
+
};
|
|
14464
|
+
}
|
|
14465
|
+
const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
|
|
14466
|
+
await writeAllDepsToCache(projectRoot, response, {
|
|
14467
|
+
includeDocs
|
|
14468
|
+
});
|
|
14469
|
+
return {
|
|
14470
|
+
response,
|
|
14471
|
+
addedGitignoreEntry
|
|
14472
|
+
};
|
|
14473
|
+
}
|
|
14474
|
+
function depsDir(projectRoot) {
|
|
14475
|
+
return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
|
|
14476
|
+
}
|
|
14477
|
+
function depsManifestPath(projectRoot) {
|
|
14478
|
+
return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
|
|
14479
|
+
}
|
|
14480
|
+
function isResolvedDependencyManifestEntry(value) {
|
|
14481
|
+
if (!value || "object" != typeof value) return false;
|
|
14482
|
+
const entry = value;
|
|
14483
|
+
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);
|
|
14484
|
+
}
|
|
14485
|
+
function isCachedDependencyManifestEntry(value) {
|
|
14486
|
+
if (!isResolvedDependencyManifestEntry(value)) return false;
|
|
14487
|
+
const entry = value;
|
|
14488
|
+
return "string" == typeof entry.package_name && entry.package_name.length > 0;
|
|
14489
|
+
}
|
|
14490
|
+
function isDepsCacheManifest(value) {
|
|
14491
|
+
if (!value || "object" != typeof value) return false;
|
|
14492
|
+
const manifest = value;
|
|
14493
|
+
return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
|
|
14494
|
+
}
|
|
14495
|
+
async function readDepsCacheManifest(projectRoot) {
|
|
14496
|
+
try {
|
|
14497
|
+
const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
|
|
14498
|
+
const parsed = JSON.parse(raw);
|
|
14499
|
+
return isDepsCacheManifest(parsed) ? parsed : void 0;
|
|
14500
|
+
} catch {
|
|
14501
|
+
return;
|
|
14502
|
+
}
|
|
14503
|
+
}
|
|
14504
|
+
async function getKnownDependenciesForCache(projectRoot, includeDocs) {
|
|
14505
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14506
|
+
if (!manifest || manifest.includeDocs !== includeDocs) return [];
|
|
14507
|
+
return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
|
|
14508
|
+
app_id,
|
|
14509
|
+
commit_id
|
|
14510
|
+
}));
|
|
14511
|
+
}
|
|
14512
|
+
function getDependencyWatchSubscriptionsFromManifest(manifest) {
|
|
14513
|
+
if (!manifest) return [];
|
|
14514
|
+
const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
|
|
14515
|
+
dep.app_id,
|
|
14516
|
+
dep
|
|
14517
|
+
]));
|
|
14518
|
+
return manifest.resolved_dependencies.flatMap((dep)=>{
|
|
14519
|
+
if (!dep.branch) return [];
|
|
14520
|
+
return [
|
|
14521
|
+
{
|
|
14522
|
+
appId: dep.app_id,
|
|
14523
|
+
branch: dep.branch,
|
|
14524
|
+
packageName: cachedByAppId.get(dep.app_id)?.package_name
|
|
14525
|
+
}
|
|
14526
|
+
];
|
|
14527
|
+
});
|
|
14528
|
+
}
|
|
14529
|
+
function normalizeAnvilYamlDependenciesFromContent(content) {
|
|
14530
|
+
const parsed = external_js_yaml_default().load(content);
|
|
14531
|
+
if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
|
|
14532
|
+
return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
|
|
14533
|
+
}
|
|
14534
|
+
function normalizeAnvilYamlDependenciesValue(value) {
|
|
14535
|
+
if (!Array.isArray(value)) return [];
|
|
14536
|
+
return normalizeForComparison(value);
|
|
14537
|
+
}
|
|
14538
|
+
async function readAnvilYamlDependencies(projectRoot) {
|
|
14539
|
+
try {
|
|
14540
|
+
const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
|
|
14541
|
+
return normalizeAnvilYamlDependenciesFromContent(content);
|
|
14542
|
+
} catch {
|
|
14543
|
+
return [];
|
|
14544
|
+
}
|
|
14545
|
+
}
|
|
14546
|
+
function anvilYamlDependenciesEqual(left, right) {
|
|
14547
|
+
return deepEqual(left, right);
|
|
14548
|
+
}
|
|
14549
|
+
function normalizeForComparison(value) {
|
|
14550
|
+
if (Array.isArray(value)) return value.map(normalizeForComparison);
|
|
14551
|
+
if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
|
|
14552
|
+
key,
|
|
14553
|
+
normalizeForComparison(entryValue)
|
|
14554
|
+
]));
|
|
14555
|
+
return value;
|
|
14556
|
+
}
|
|
14557
|
+
function cachedManifestEntry(dep, packageName) {
|
|
14558
|
+
return {
|
|
14559
|
+
app_id: dep.app_id,
|
|
14560
|
+
commit_id: dep.commit_id,
|
|
14561
|
+
...dep.branch ? {
|
|
14562
|
+
branch: dep.branch
|
|
14563
|
+
} : {},
|
|
14564
|
+
package_name: packageName
|
|
14565
|
+
};
|
|
14566
|
+
}
|
|
14567
|
+
function safeDependencyPath(filePath, kind) {
|
|
14568
|
+
const normalizedPath = external_path_default().normalize(filePath);
|
|
14569
|
+
if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
|
|
14570
|
+
return normalizedPath;
|
|
14571
|
+
}
|
|
14572
|
+
async function writeDepToCache(projectRoot, dep) {
|
|
14573
|
+
const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
|
|
14574
|
+
await promises_default().rm(depDir, {
|
|
14575
|
+
recursive: true,
|
|
14576
|
+
force: true
|
|
14577
|
+
});
|
|
14578
|
+
await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
|
|
14579
|
+
recursive: true
|
|
14580
|
+
});
|
|
14581
|
+
await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
|
|
14582
|
+
recursive: true
|
|
14583
|
+
});
|
|
14584
|
+
await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
|
|
14585
|
+
package_name: dep.package_name
|
|
14586
|
+
}));
|
|
14587
|
+
for (const mod of dep.modules ?? [])if (mod.is_package) {
|
|
14588
|
+
const modDir = external_path_default().join(depDir, "client_code", mod.name);
|
|
14589
|
+
await promises_default().mkdir(modDir, {
|
|
14590
|
+
recursive: true
|
|
14591
|
+
});
|
|
14592
|
+
await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
|
|
14593
|
+
} else await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${mod.name}.py`), mod.code);
|
|
14594
|
+
for (const form of dep.forms ?? []){
|
|
14595
|
+
const { code, class_name, is_package, ...formTemplate } = form;
|
|
14596
|
+
if (is_package) {
|
|
14597
|
+
const formDir = external_path_default().join(depDir, "client_code", class_name);
|
|
14598
|
+
await promises_default().mkdir(formDir, {
|
|
14599
|
+
recursive: true
|
|
14600
|
+
});
|
|
14601
|
+
await promises_default().writeFile(external_path_default().join(formDir, "__init__.py"), code);
|
|
14602
|
+
await promises_default().writeFile(external_path_default().join(formDir, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
|
|
14603
|
+
} else {
|
|
14604
|
+
await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.py`), code);
|
|
14605
|
+
await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.yaml`), external_js_yaml_default().dump(formTemplate));
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
for (const mod of dep.server_modules ?? [])if (mod.is_package) {
|
|
14609
|
+
const modDir = external_path_default().join(depDir, "server_code", mod.name);
|
|
14610
|
+
await promises_default().mkdir(modDir, {
|
|
14611
|
+
recursive: true
|
|
14612
|
+
});
|
|
14613
|
+
await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
|
|
14614
|
+
} else await promises_default().writeFile(external_path_default().join(depDir, "server_code", `${mod.name}.py`), mod.code);
|
|
14615
|
+
for (const asset of dep.assets ?? []){
|
|
14616
|
+
const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
|
|
14617
|
+
await promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
14618
|
+
recursive: true
|
|
14619
|
+
});
|
|
14620
|
+
await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
|
|
14621
|
+
}
|
|
14622
|
+
for (const doc of dep.docs ?? []){
|
|
14623
|
+
const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
|
|
14624
|
+
await promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
14625
|
+
recursive: true
|
|
14626
|
+
});
|
|
14627
|
+
await promises_default().writeFile(targetPath, doc.content);
|
|
14628
|
+
}
|
|
14629
|
+
}
|
|
14630
|
+
async function writeAllDepsToCache(projectRoot, response, options = {}) {
|
|
14631
|
+
const includeDocs = options.includeDocs ?? true;
|
|
14632
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14633
|
+
const canReuseExistingCache = manifest?.includeDocs === includeDocs;
|
|
14634
|
+
if (!canReuseExistingCache) await clearDepsCache(projectRoot);
|
|
14635
|
+
const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
|
|
14636
|
+
const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
|
|
14637
|
+
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), {
|
|
14638
|
+
recursive: true,
|
|
14639
|
+
force: true
|
|
14640
|
+
});
|
|
14641
|
+
for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
|
|
14642
|
+
const returnedByAppId = new Map(response.dependencies.map((dep)=>[
|
|
14643
|
+
dep.app_id,
|
|
14644
|
+
dep
|
|
14645
|
+
]));
|
|
14646
|
+
const previousByAppId = new Map(previousCached.map((dep)=>[
|
|
14647
|
+
dep.app_id,
|
|
14648
|
+
dep
|
|
14649
|
+
]));
|
|
14650
|
+
const cachedDependencies = response.resolved_dependencies.map((dep)=>{
|
|
14651
|
+
const returned = returnedByAppId.get(dep.app_id);
|
|
14652
|
+
if (returned) return cachedManifestEntry(returned, returned.package_name);
|
|
14653
|
+
const previous = previousByAppId.get(dep.app_id);
|
|
14654
|
+
if (!previous) return;
|
|
14655
|
+
return cachedManifestEntry(dep, previous.package_name);
|
|
14656
|
+
}).filter((dep)=>void 0 !== dep);
|
|
14657
|
+
const newManifest = {
|
|
14658
|
+
version: DEPS_MANIFEST_VERSION,
|
|
14659
|
+
includeDocs,
|
|
14660
|
+
resolved_dependencies: response.resolved_dependencies,
|
|
14661
|
+
cached_dependencies: cachedDependencies
|
|
14662
|
+
};
|
|
14663
|
+
await promises_default().mkdir(depsDir(projectRoot), {
|
|
14664
|
+
recursive: true
|
|
14665
|
+
});
|
|
14666
|
+
await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
|
|
14667
|
+
}
|
|
14668
|
+
async function clearDepsCache(projectRoot) {
|
|
14669
|
+
await promises_default().rm(depsDir(projectRoot), {
|
|
14670
|
+
recursive: true,
|
|
14671
|
+
force: true
|
|
14672
|
+
});
|
|
14673
|
+
}
|
|
14312
14674
|
class git_GitService {
|
|
14313
14675
|
git;
|
|
14314
14676
|
repoPath;
|
|
@@ -14395,6 +14757,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14395
14757
|
throw errors_createGitError.commandFailed("status", e.message);
|
|
14396
14758
|
}
|
|
14397
14759
|
}
|
|
14760
|
+
async discardModeOnlyChanges(paths) {
|
|
14761
|
+
const discarded = [];
|
|
14762
|
+
for (const relativePath of paths)try {
|
|
14763
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14764
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14765
|
+
const contentUnchanged = await this.hasSameContentAsHead(relativePath);
|
|
14766
|
+
if (!contentUnchanged) continue;
|
|
14767
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14768
|
+
discarded.push(relativePath);
|
|
14769
|
+
} catch (e) {
|
|
14770
|
+
throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
|
|
14771
|
+
}
|
|
14772
|
+
return discarded;
|
|
14773
|
+
}
|
|
14774
|
+
async discardStagedModeOnlyChanges(paths) {
|
|
14775
|
+
const discarded = [];
|
|
14776
|
+
for (const relativePath of paths)try {
|
|
14777
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14778
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14779
|
+
const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
|
|
14780
|
+
if (!indexContentUnchanged) continue;
|
|
14781
|
+
await this.git.raw([
|
|
14782
|
+
"reset",
|
|
14783
|
+
"-q",
|
|
14784
|
+
"HEAD",
|
|
14785
|
+
"--",
|
|
14786
|
+
relativePath
|
|
14787
|
+
]);
|
|
14788
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14789
|
+
discarded.push(relativePath);
|
|
14790
|
+
} catch (e) {
|
|
14791
|
+
throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
|
|
14792
|
+
}
|
|
14793
|
+
return discarded;
|
|
14794
|
+
}
|
|
14795
|
+
async getHeadFileMode(relativePath) {
|
|
14796
|
+
const output = await this.git.raw([
|
|
14797
|
+
"ls-tree",
|
|
14798
|
+
"HEAD",
|
|
14799
|
+
"--",
|
|
14800
|
+
relativePath
|
|
14801
|
+
]);
|
|
14802
|
+
const match = output.match(/^(\d{6})\s/);
|
|
14803
|
+
return match?.[1] ?? null;
|
|
14804
|
+
}
|
|
14805
|
+
async hasSameContentAsHead(relativePath) {
|
|
14806
|
+
const headHash = (await this.git.revparse([
|
|
14807
|
+
`HEAD:${relativePath}`
|
|
14808
|
+
])).trim();
|
|
14809
|
+
const worktreeHash = (await this.git.raw([
|
|
14810
|
+
"hash-object",
|
|
14811
|
+
"--",
|
|
14812
|
+
relativePath
|
|
14813
|
+
])).trim();
|
|
14814
|
+
return headHash === worktreeHash;
|
|
14815
|
+
}
|
|
14816
|
+
async hasSameIndexContentAsHead(relativePath) {
|
|
14817
|
+
const headHash = (await this.git.revparse([
|
|
14818
|
+
`HEAD:${relativePath}`
|
|
14819
|
+
])).trim();
|
|
14820
|
+
const indexHash = (await this.git.revparse([
|
|
14821
|
+
`:${relativePath}`
|
|
14822
|
+
])).trim();
|
|
14823
|
+
return headHash === indexHash;
|
|
14824
|
+
}
|
|
14825
|
+
async restoreExecutableBit(relativePath, executable) {
|
|
14826
|
+
const filePath = external_path_default().join(this.repoPath, relativePath);
|
|
14827
|
+
const stat = await external_fs_.promises.stat(filePath);
|
|
14828
|
+
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14829
|
+
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14830
|
+
}
|
|
14398
14831
|
async hasUncommittedChanges() {
|
|
14399
14832
|
const status = await this.getStatus();
|
|
14400
14833
|
return !status.isClean;
|
|
@@ -14534,6 +14967,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14534
14967
|
throw errors_createGitError.commandFailed("rebase --abort", e.message);
|
|
14535
14968
|
}
|
|
14536
14969
|
}
|
|
14970
|
+
async isAncestor(ancestorRef, descendantRef) {
|
|
14971
|
+
try {
|
|
14972
|
+
const mergeBase = (await this.git.raw([
|
|
14973
|
+
"merge-base",
|
|
14974
|
+
ancestorRef,
|
|
14975
|
+
descendantRef
|
|
14976
|
+
])).trim();
|
|
14977
|
+
const ancestorCommit = (await this.git.revparse([
|
|
14978
|
+
ancestorRef
|
|
14979
|
+
])).trim();
|
|
14980
|
+
return mergeBase === ancestorCommit;
|
|
14981
|
+
} catch {
|
|
14982
|
+
return false;
|
|
14983
|
+
}
|
|
14984
|
+
}
|
|
14985
|
+
async mergeFastForward(ref) {
|
|
14986
|
+
try {
|
|
14987
|
+
await this.git.raw([
|
|
14988
|
+
"merge",
|
|
14989
|
+
"--ff-only",
|
|
14990
|
+
ref
|
|
14991
|
+
]);
|
|
14992
|
+
} catch (e) {
|
|
14993
|
+
throw errors_createGitError.commandFailed("merge --ff-only", e.message);
|
|
14994
|
+
}
|
|
14995
|
+
}
|
|
14537
14996
|
async deleteRef(ref) {
|
|
14538
14997
|
try {
|
|
14539
14998
|
await this.git.raw([
|
|
@@ -14578,11 +15037,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14578
15037
|
} catch (error) {}
|
|
14579
15038
|
}
|
|
14580
15039
|
}
|
|
14581
|
-
async function getStagedFileChanges(git) {
|
|
15040
|
+
async function getStagedFileChanges(git, gitService) {
|
|
14582
15041
|
try {
|
|
14583
15042
|
const status = await git.status();
|
|
14584
15043
|
const renames = status.renamed || [];
|
|
14585
15044
|
const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
|
|
15045
|
+
const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
|
|
14586
15046
|
const stagedRenames = [];
|
|
14587
15047
|
for (const f of stagedFiles)if ("R" === f.index) {
|
|
14588
15048
|
const rename = renames.find((r)=>r.to === f.path);
|
|
@@ -14595,19 +15055,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14595
15055
|
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
14596
15056
|
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
14597
15057
|
const changes = [];
|
|
14598
|
-
for (const f of stagedFiles)
|
|
14599
|
-
if (
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
15058
|
+
for (const f of stagedFiles){
|
|
15059
|
+
if ("R" !== f.index) {
|
|
15060
|
+
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
15061
|
+
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
15062
|
+
path: f.path,
|
|
15063
|
+
type: "unlink"
|
|
15064
|
+
});
|
|
15065
|
+
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
15066
|
+
path: f.path,
|
|
15067
|
+
type: "add"
|
|
15068
|
+
});
|
|
15069
|
+
else changes.push({
|
|
15070
|
+
path: f.path,
|
|
15071
|
+
type: "change"
|
|
15072
|
+
});
|
|
15073
|
+
}
|
|
15074
|
+
}
|
|
14611
15075
|
}
|
|
14612
15076
|
changes.push(...stagedRenames);
|
|
14613
15077
|
return changes;
|
|
@@ -14615,50 +15079,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14615
15079
|
throw errors_createGitError.commandFailed("status", e.message);
|
|
14616
15080
|
}
|
|
14617
15081
|
}
|
|
14618
|
-
function normalizeLineEndings(content) {
|
|
14619
|
-
return content.replace(/\r\n/g, "\n");
|
|
14620
|
-
}
|
|
14621
|
-
function pythonifyName(name) {
|
|
14622
|
-
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
14623
|
-
}
|
|
14624
|
-
function extractPythonName(relativePath, skipParts = 1) {
|
|
14625
|
-
const parts = relativePath.slice(0, -3).split("/");
|
|
14626
|
-
const isPackage = relativePath.endsWith("__init__.py");
|
|
14627
|
-
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
14628
|
-
return relevantParts.map(pythonifyName).join(".");
|
|
14629
|
-
}
|
|
14630
|
-
function deepEqual(a, b) {
|
|
14631
|
-
if (a === b) return true;
|
|
14632
|
-
if (null == a || null == b) return false;
|
|
14633
|
-
if (typeof a !== typeof b) return false;
|
|
14634
|
-
if ("object" != typeof a) return a === b;
|
|
14635
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
14636
|
-
if (Array.isArray(a)) {
|
|
14637
|
-
if (a.length !== b.length) return false;
|
|
14638
|
-
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14639
|
-
}
|
|
14640
|
-
const keysA = Object.keys(a);
|
|
14641
|
-
const keysB = Object.keys(b);
|
|
14642
|
-
if (keysA.length !== keysB.length) return false;
|
|
14643
|
-
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
14644
|
-
}
|
|
14645
|
-
function parseHtmlWithFrontmatter(htmlContent) {
|
|
14646
|
-
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14647
|
-
const match = htmlContent.match(frontmatterPattern);
|
|
14648
|
-
if (match) {
|
|
14649
|
-
const yamlStr = match[1];
|
|
14650
|
-
const html = match[2];
|
|
14651
|
-
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
14652
|
-
return {
|
|
14653
|
-
frontmatter,
|
|
14654
|
-
html
|
|
14655
|
-
};
|
|
14656
|
-
}
|
|
14657
|
-
return {
|
|
14658
|
-
frontmatter: {},
|
|
14659
|
-
html: htmlContent
|
|
14660
|
-
};
|
|
14661
|
-
}
|
|
14662
15082
|
const external_chokidar_namespaceObject = require("chokidar");
|
|
14663
15083
|
var external_chokidar_default = /*#__PURE__*/ __webpack_require__.n(external_chokidar_namespaceObject);
|
|
14664
15084
|
class FileWatcher extends Emitter {
|
|
@@ -14808,6 +15228,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14808
15228
|
editSession;
|
|
14809
15229
|
username;
|
|
14810
15230
|
environment;
|
|
15231
|
+
dependencyWatchEnabled;
|
|
15232
|
+
dependencySubscriptions;
|
|
15233
|
+
activeDependencySubscriptions = new Map();
|
|
15234
|
+
watchedPrimaryBranch = null;
|
|
15235
|
+
nextSubscriptionId = 1;
|
|
15236
|
+
pendingDependencySubscriptionById = new Map();
|
|
14811
15237
|
reconnectAttempts = 0;
|
|
14812
15238
|
connectAttempts = 0;
|
|
14813
15239
|
reconnectTimer = null;
|
|
@@ -14830,6 +15256,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14830
15256
|
this.editSession = options.editSession;
|
|
14831
15257
|
this.username = options.username;
|
|
14832
15258
|
this.environment = options.environment;
|
|
15259
|
+
this.dependencyWatchEnabled = options.dependencyWatch ?? true;
|
|
15260
|
+
this.dependencySubscriptions = dedupeDependencySubscriptions(options.dependencySubscriptions ?? []);
|
|
14833
15261
|
this.sessionId = Math.random().toString(36).substring(2, 10);
|
|
14834
15262
|
this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
|
|
14835
15263
|
}
|
|
@@ -14855,6 +15283,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14855
15283
|
const subscribeMsg = this.buildSubscribeMessage(this.currentBranch);
|
|
14856
15284
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing: ${JSON.stringify(subscribeMsg)}`);
|
|
14857
15285
|
this.ws?.send(JSON.stringify(subscribeMsg));
|
|
15286
|
+
this.watchedPrimaryBranch = this.currentBranch;
|
|
15287
|
+
this.activeDependencySubscriptions.clear();
|
|
15288
|
+
this.subscribeToDependencies();
|
|
14858
15289
|
this.emit("connected", void 0);
|
|
14859
15290
|
});
|
|
14860
15291
|
this.ws.on("unexpected-response", (request, response)=>{
|
|
@@ -14877,6 +15308,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14877
15308
|
try {
|
|
14878
15309
|
const msg = JSON.parse(data.toString());
|
|
14879
15310
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
|
|
15311
|
+
this.handleDependencySubscriptionReply(msg);
|
|
14880
15312
|
this.handleMessage(msg);
|
|
14881
15313
|
} catch (error) {
|
|
14882
15314
|
logger_logger.error(external_chalk_default().red(`Failed to parse WebSocket message: ${error.message}`));
|
|
@@ -14927,7 +15359,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14927
15359
|
if (!ref_update) return void logger_logger.debug("Ignoring REPO_UPDATE with no ref_update");
|
|
14928
15360
|
const { ref } = ref_update;
|
|
14929
15361
|
const commitHash = "string" == typeof commit ? commit : commit?.v;
|
|
15362
|
+
const appId = this.getMessageAppId(msg);
|
|
14930
15363
|
this.emit("repo-update", {
|
|
15364
|
+
appId,
|
|
14931
15365
|
ref,
|
|
14932
15366
|
commitHash: commitHash || "unknown",
|
|
14933
15367
|
saveSeq: ref_update?.["save-seq"]
|
|
@@ -14942,11 +15376,26 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14942
15376
|
}
|
|
14943
15377
|
updateBranch(branch) {
|
|
14944
15378
|
this.currentBranch = branch;
|
|
14945
|
-
if (this.ws?.readyState === external_ws_default().OPEN)
|
|
15379
|
+
if (this.ws?.readyState === external_ws_default().OPEN) {
|
|
15380
|
+
this.ws.send(JSON.stringify(this.buildSubscribeMessage(branch)));
|
|
15381
|
+
this.watchedPrimaryBranch = branch;
|
|
15382
|
+
this.subscribeToDependencies();
|
|
15383
|
+
}
|
|
14946
15384
|
}
|
|
14947
15385
|
updateEnvironment(environment) {
|
|
14948
15386
|
this.environment = environment;
|
|
14949
15387
|
}
|
|
15388
|
+
updateDependencySubscriptions(subscriptions) {
|
|
15389
|
+
const previousSubscriptions = this.dependencySubscriptions;
|
|
15390
|
+
this.dependencySubscriptions = dedupeDependencySubscriptions(subscriptions);
|
|
15391
|
+
if (this.ws?.readyState === external_ws_default().OPEN) {
|
|
15392
|
+
this.unsubscribeStaleDependencies(previousSubscriptions, this.dependencySubscriptions);
|
|
15393
|
+
this.subscribeToDependencies();
|
|
15394
|
+
}
|
|
15395
|
+
}
|
|
15396
|
+
getDependencySubscriptions() {
|
|
15397
|
+
return this.dependencySubscriptions;
|
|
15398
|
+
}
|
|
14950
15399
|
getEditSession() {
|
|
14951
15400
|
return this.editSession;
|
|
14952
15401
|
}
|
|
@@ -14957,7 +15406,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14957
15406
|
clearTimeout(this.reconnectTimer);
|
|
14958
15407
|
this.reconnectTimer = null;
|
|
14959
15408
|
}
|
|
15409
|
+
if (this.ws?.readyState === external_ws_default().OPEN) {
|
|
15410
|
+
this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(this.appId)));
|
|
15411
|
+
this.unwatchDependencySubscriptions([
|
|
15412
|
+
...this.activeDependencySubscriptions.values()
|
|
15413
|
+
]);
|
|
15414
|
+
}
|
|
15415
|
+
this.teardownSocket();
|
|
15416
|
+
}
|
|
15417
|
+
reconnect() {
|
|
15418
|
+
if (this.isClosing) return;
|
|
14960
15419
|
this.teardownSocket();
|
|
15420
|
+
this.connect().catch((error)=>{
|
|
15421
|
+
this.emit("error", {
|
|
15422
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
15423
|
+
});
|
|
15424
|
+
});
|
|
14961
15425
|
}
|
|
14962
15426
|
isConnected() {
|
|
14963
15427
|
return this.ws?.readyState === external_ws_default().OPEN;
|
|
@@ -14990,6 +15454,72 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14990
15454
|
} : {}
|
|
14991
15455
|
};
|
|
14992
15456
|
}
|
|
15457
|
+
buildUnsubscribeMessage(appId) {
|
|
15458
|
+
return {
|
|
15459
|
+
cmd: "UNWATCH_APP",
|
|
15460
|
+
app: appId
|
|
15461
|
+
};
|
|
15462
|
+
}
|
|
15463
|
+
subscribeToDependencies() {
|
|
15464
|
+
if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
|
|
15465
|
+
for (const subscription of this.dependencySubscriptions){
|
|
15466
|
+
const key = this.dependencySubscriptionKey(subscription);
|
|
15467
|
+
if (this.activeDependencySubscriptions.has(key)) continue;
|
|
15468
|
+
const id = this.nextSubscriptionId++;
|
|
15469
|
+
this.pendingDependencySubscriptionById.set(id, subscription);
|
|
15470
|
+
this.activeDependencySubscriptions.set(key, subscription);
|
|
15471
|
+
const subscribeMsg = {
|
|
15472
|
+
id,
|
|
15473
|
+
cmd: "WATCH_APP",
|
|
15474
|
+
app: subscription.appId,
|
|
15475
|
+
only_branch: subscription.branch
|
|
15476
|
+
};
|
|
15477
|
+
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing dependency: ${JSON.stringify(subscribeMsg)}`);
|
|
15478
|
+
logger_logger.verbose(external_chalk_default().gray(`Watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch})`));
|
|
15479
|
+
this.ws.send(JSON.stringify(subscribeMsg));
|
|
15480
|
+
}
|
|
15481
|
+
}
|
|
15482
|
+
handleDependencySubscriptionReply(msg) {
|
|
15483
|
+
if ("number" != typeof msg.id) return;
|
|
15484
|
+
const subscription = this.pendingDependencySubscriptionById.get(msg.id);
|
|
15485
|
+
if (!subscription) return;
|
|
15486
|
+
this.pendingDependencySubscriptionById.delete(msg.id);
|
|
15487
|
+
if (!msg.error) return;
|
|
15488
|
+
this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
|
|
15489
|
+
logger_logger.warn(external_chalk_default().yellow(`Could not watch dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch}): ${this.formatDependencySubscriptionError(msg.error)}`));
|
|
15490
|
+
}
|
|
15491
|
+
formatDependencySubscriptionError(error) {
|
|
15492
|
+
if ("string" == typeof error) return error;
|
|
15493
|
+
if (error && "object" == typeof error && "string" == typeof error.message) return error.message;
|
|
15494
|
+
return JSON.stringify(error);
|
|
15495
|
+
}
|
|
15496
|
+
getMessageAppId(msg) {
|
|
15497
|
+
if ("string" == typeof msg.app) return msg.app;
|
|
15498
|
+
if ("string" == typeof msg.app_id) return msg.app_id;
|
|
15499
|
+
if ("string" == typeof msg.ref_update?.app) return msg.ref_update.app;
|
|
15500
|
+
if ("string" == typeof msg.ref_update?.app_id) return msg.ref_update.app_id;
|
|
15501
|
+
return this.appId;
|
|
15502
|
+
}
|
|
15503
|
+
unsubscribeStaleDependencies(previousSubscriptions, nextSubscriptions) {
|
|
15504
|
+
const nextAppIds = new Set(nextSubscriptions.map((subscription)=>subscription.appId));
|
|
15505
|
+
const staleSubscriptions = previousSubscriptions.filter((subscription)=>!nextAppIds.has(subscription.appId));
|
|
15506
|
+
this.unwatchDependencySubscriptions(staleSubscriptions);
|
|
15507
|
+
const nextKeys = new Set(nextSubscriptions.map((subscription)=>this.dependencySubscriptionKey(subscription)));
|
|
15508
|
+
for (const subscription of previousSubscriptions){
|
|
15509
|
+
const key = this.dependencySubscriptionKey(subscription);
|
|
15510
|
+
if (!nextKeys.has(key)) this.activeDependencySubscriptions.delete(key);
|
|
15511
|
+
}
|
|
15512
|
+
}
|
|
15513
|
+
unwatchDependencySubscriptions(subscriptions) {
|
|
15514
|
+
if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
|
|
15515
|
+
const unwatchedAppIds = new Set();
|
|
15516
|
+
for (const subscription of subscriptions)if (!unwatchedAppIds.has(subscription.appId)) {
|
|
15517
|
+
unwatchedAppIds.add(subscription.appId);
|
|
15518
|
+
logger_logger.verbose(external_chalk_default().gray(`Stopped watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId})`));
|
|
15519
|
+
this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(subscription.appId)));
|
|
15520
|
+
}
|
|
15521
|
+
for (const subscription of subscriptions)this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
|
|
15522
|
+
}
|
|
14993
15523
|
getConnectionContext() {
|
|
14994
15524
|
return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
|
|
14995
15525
|
}
|
|
@@ -15061,6 +15591,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15061
15591
|
}
|
|
15062
15592
|
teardownSocket() {
|
|
15063
15593
|
this.stopHeartbeat();
|
|
15594
|
+
this.pendingDependencySubscriptionById.clear();
|
|
15595
|
+
this.activeDependencySubscriptions.clear();
|
|
15596
|
+
this.watchedPrimaryBranch = null;
|
|
15064
15597
|
const socket = this.ws;
|
|
15065
15598
|
this.ws = null;
|
|
15066
15599
|
if (!socket) return;
|
|
@@ -15076,6 +15609,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15076
15609
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
|
|
15077
15610
|
}
|
|
15078
15611
|
}
|
|
15612
|
+
dependencySubscriptionKey(subscription) {
|
|
15613
|
+
return `${subscription.appId}\0${subscription.branch}`;
|
|
15614
|
+
}
|
|
15079
15615
|
}
|
|
15080
15616
|
function normalizeEnvironmentBinding(msg) {
|
|
15081
15617
|
return {
|
|
@@ -15084,6 +15620,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15084
15620
|
tableMappingId: msg.tableMappingId ?? msg.table_mapping_id ?? null
|
|
15085
15621
|
};
|
|
15086
15622
|
}
|
|
15623
|
+
function dedupeDependencySubscriptions(subscriptions) {
|
|
15624
|
+
const seen = new Set();
|
|
15625
|
+
const deduped = [];
|
|
15626
|
+
for (const subscription of subscriptions){
|
|
15627
|
+
const key = `${subscription.appId}\0${subscription.branch}`;
|
|
15628
|
+
if (!seen.has(key)) {
|
|
15629
|
+
seen.add(key);
|
|
15630
|
+
deduped.push(subscription);
|
|
15631
|
+
}
|
|
15632
|
+
}
|
|
15633
|
+
return deduped;
|
|
15634
|
+
}
|
|
15087
15635
|
async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
|
|
15088
15636
|
try {
|
|
15089
15637
|
const files = await gitService.diffNames(oldCommitId, newCommitId);
|
|
@@ -15134,7 +15682,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15134
15682
|
...status.modified,
|
|
15135
15683
|
...status.notAdded,
|
|
15136
15684
|
...status.created,
|
|
15137
|
-
...status.deleted
|
|
15685
|
+
...status.deleted,
|
|
15686
|
+
...(status.renamed ?? []).flatMap((r)=>[
|
|
15687
|
+
r.from,
|
|
15688
|
+
r.to
|
|
15689
|
+
])
|
|
15138
15690
|
]);
|
|
15139
15691
|
}
|
|
15140
15692
|
function extractChangesFromStatus(status) {
|
|
@@ -15176,12 +15728,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15176
15728
|
result.localOnlyChanges.forEach((c)=>logger_logger.verbose(external_chalk_default().gray(` ${c.path}`)));
|
|
15177
15729
|
}
|
|
15178
15730
|
}
|
|
15179
|
-
async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
|
|
15731
|
+
async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles, options = {}) {
|
|
15180
15732
|
const filesToRemove = [];
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
15733
|
+
const localChangesToPreserve = options.localChangesToPreserve ?? new Set();
|
|
15734
|
+
for (const file of unstagedFiles)if (!localChangesToPreserve.has(file)) {
|
|
15735
|
+
try {
|
|
15736
|
+
await gitService.show(`HEAD:${file}`);
|
|
15737
|
+
continue;
|
|
15738
|
+
} catch (e) {}
|
|
15739
|
+
if (options.oldCommitId) {
|
|
15740
|
+
try {
|
|
15741
|
+
await gitService.show(`${options.oldCommitId}:${file}`);
|
|
15742
|
+
filesToRemove.push(file);
|
|
15743
|
+
} catch (e) {}
|
|
15744
|
+
continue;
|
|
15745
|
+
}
|
|
15746
|
+
if (remotelyChangedFiles.has(file)) filesToRemove.push(file);
|
|
15185
15747
|
}
|
|
15186
15748
|
if (filesToRemove.length > 0) try {
|
|
15187
15749
|
await gitService.clean(filesToRemove);
|
|
@@ -15193,6 +15755,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15193
15755
|
return filesToRemove;
|
|
15194
15756
|
}
|
|
15195
15757
|
const external_crypto_namespaceObject = require("crypto");
|
|
15758
|
+
const form_template_parser_namespaceObject = require("@anvil-works/form-template-parser");
|
|
15196
15759
|
const BUILTIN_COMPONENT_TYPES = new Set([
|
|
15197
15760
|
"Label",
|
|
15198
15761
|
"RichText",
|
|
@@ -15223,59 +15786,156 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15223
15786
|
"CustomComponent",
|
|
15224
15787
|
"LayoutSlot",
|
|
15225
15788
|
"FlowPanel",
|
|
15226
|
-
"HtmlTemplate"
|
|
15227
|
-
|
|
15228
|
-
const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
|
|
15229
|
-
"/runtime/services/tables.yml",
|
|
15230
|
-
"/runtime/services/google.yml",
|
|
15231
|
-
"/runtime/services/uplink.yml",
|
|
15232
|
-
"/runtime/services/stripe.yml",
|
|
15233
|
-
"/runtime/services/segment.yml",
|
|
15234
|
-
"/runtime/services/facebook.yml",
|
|
15235
|
-
"/runtime/services/anvil/users.yml",
|
|
15236
|
-
"/runtime/services/anvil/secrets.yml",
|
|
15237
|
-
"/runtime/services/anvil/saml.yml",
|
|
15238
|
-
"/runtime/services/anvil/microsoft.yml",
|
|
15239
|
-
"/runtime/services/anvil/email.yml",
|
|
15240
|
-
"/runtime/services/anvil/files.yml"
|
|
15789
|
+
"HtmlTemplate",
|
|
15790
|
+
"HtmlComponent"
|
|
15241
15791
|
]);
|
|
15242
15792
|
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15243
|
-
const
|
|
15244
|
-
|
|
15245
|
-
"
|
|
15246
|
-
"
|
|
15247
|
-
"
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
"
|
|
15251
|
-
"
|
|
15252
|
-
"
|
|
15253
|
-
"
|
|
15254
|
-
"
|
|
15255
|
-
"
|
|
15256
|
-
"
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
"
|
|
15260
|
-
"
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
"
|
|
15264
|
-
"
|
|
15265
|
-
"
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
"
|
|
15269
|
-
"
|
|
15270
|
-
"
|
|
15271
|
-
"
|
|
15272
|
-
"
|
|
15793
|
+
const PYTHON_KEYWORDS = new Set([
|
|
15794
|
+
"False",
|
|
15795
|
+
"None",
|
|
15796
|
+
"True",
|
|
15797
|
+
"and",
|
|
15798
|
+
"as",
|
|
15799
|
+
"assert",
|
|
15800
|
+
"async",
|
|
15801
|
+
"await",
|
|
15802
|
+
"break",
|
|
15803
|
+
"class",
|
|
15804
|
+
"continue",
|
|
15805
|
+
"def",
|
|
15806
|
+
"del",
|
|
15807
|
+
"elif",
|
|
15808
|
+
"else",
|
|
15809
|
+
"except",
|
|
15810
|
+
"finally",
|
|
15811
|
+
"for",
|
|
15812
|
+
"from",
|
|
15813
|
+
"global",
|
|
15814
|
+
"if",
|
|
15815
|
+
"import",
|
|
15816
|
+
"in",
|
|
15817
|
+
"is",
|
|
15818
|
+
"lambda",
|
|
15819
|
+
"nonlocal",
|
|
15820
|
+
"not",
|
|
15821
|
+
"or",
|
|
15822
|
+
"pass",
|
|
15823
|
+
"raise",
|
|
15824
|
+
"return",
|
|
15825
|
+
"try",
|
|
15826
|
+
"while",
|
|
15827
|
+
"with",
|
|
15828
|
+
"yield",
|
|
15829
|
+
"match",
|
|
15830
|
+
"case"
|
|
15273
15831
|
]);
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15832
|
+
function isValidPythonIdentifier(value) {
|
|
15833
|
+
return IDENTIFIER_RE.test(value) && !PYTHON_KEYWORDS.has(value);
|
|
15834
|
+
}
|
|
15835
|
+
function isParserGeneratedComponentName(name) {
|
|
15836
|
+
if (!name.startsWith("$")) return false;
|
|
15837
|
+
const suffix = name.slice(1);
|
|
15838
|
+
return suffix.length > 0 && isValidPythonIdentifier(suffix);
|
|
15839
|
+
}
|
|
15840
|
+
function pushIssue(issues, path, message) {
|
|
15841
|
+
issues.push({
|
|
15842
|
+
path,
|
|
15843
|
+
message
|
|
15844
|
+
});
|
|
15845
|
+
}
|
|
15846
|
+
function validateComponentName(value, path, issues) {
|
|
15847
|
+
if (void 0 === value || "string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
|
|
15848
|
+
if (isParserGeneratedComponentName(value)) return;
|
|
15849
|
+
if (!IDENTIFIER_RE.test(value)) return void pushIssue(issues, path, "must be a valid identifier");
|
|
15850
|
+
if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15851
|
+
}
|
|
15852
|
+
const componentTypeSpec_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15853
|
+
const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
|
|
15854
|
+
const componentTypeSpec_PYTHON_KEYWORDS = new Set([
|
|
15855
|
+
"False",
|
|
15856
|
+
"None",
|
|
15857
|
+
"True",
|
|
15858
|
+
"and",
|
|
15859
|
+
"as",
|
|
15860
|
+
"assert",
|
|
15861
|
+
"async",
|
|
15862
|
+
"await",
|
|
15863
|
+
"break",
|
|
15864
|
+
"class",
|
|
15865
|
+
"continue",
|
|
15866
|
+
"def",
|
|
15867
|
+
"del",
|
|
15868
|
+
"elif",
|
|
15869
|
+
"else",
|
|
15870
|
+
"except",
|
|
15871
|
+
"finally",
|
|
15872
|
+
"for",
|
|
15873
|
+
"from",
|
|
15874
|
+
"global",
|
|
15875
|
+
"if",
|
|
15876
|
+
"import",
|
|
15877
|
+
"in",
|
|
15878
|
+
"is",
|
|
15879
|
+
"lambda",
|
|
15880
|
+
"nonlocal",
|
|
15881
|
+
"not",
|
|
15882
|
+
"or",
|
|
15883
|
+
"pass",
|
|
15884
|
+
"raise",
|
|
15885
|
+
"return",
|
|
15886
|
+
"try",
|
|
15887
|
+
"while",
|
|
15888
|
+
"with",
|
|
15889
|
+
"yield",
|
|
15890
|
+
"match",
|
|
15891
|
+
"case"
|
|
15277
15892
|
]);
|
|
15278
|
-
const
|
|
15893
|
+
const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)";
|
|
15894
|
+
const COMPONENT_TYPE_REFERENCE_MESSAGE = "must be a known Anvil component type (see componentIcons.ts), a form: reference, or a package-qualified form reference (Package.Form)";
|
|
15895
|
+
function isValidIdentifierSegment(value) {
|
|
15896
|
+
return value.length > 0 && componentTypeSpec_IDENTIFIER_RE.test(value) && !componentTypeSpec_PYTHON_KEYWORDS.has(value);
|
|
15897
|
+
}
|
|
15898
|
+
function isValidLegacyFormTypeReference(typeName) {
|
|
15899
|
+
if (!typeName.startsWith("form:")) return false;
|
|
15900
|
+
const rest = typeName.slice(5);
|
|
15901
|
+
if (0 === rest.length) return false;
|
|
15902
|
+
let classPath = rest;
|
|
15903
|
+
if (rest.startsWith("dep_")) {
|
|
15904
|
+
const splitIndex = rest.indexOf(":");
|
|
15905
|
+
if (splitIndex <= 0) return false;
|
|
15906
|
+
const depId = rest.slice(0, splitIndex);
|
|
15907
|
+
if (!DEP_ID_RE.test(depId)) return false;
|
|
15908
|
+
classPath = rest.slice(splitIndex + 1);
|
|
15909
|
+
}
|
|
15910
|
+
if (0 === classPath.length) return false;
|
|
15911
|
+
return classPath.split(".").every((part)=>isValidIdentifierSegment(part));
|
|
15912
|
+
}
|
|
15913
|
+
function isValidPackageQualifiedComponentType(typeName) {
|
|
15914
|
+
if (typeName.startsWith("anvil.")) return false;
|
|
15915
|
+
if (!typeName.includes(".")) return false;
|
|
15916
|
+
const parts = typeName.split(".");
|
|
15917
|
+
if (parts.length < 2) return false;
|
|
15918
|
+
return parts.every((part)=>isValidIdentifierSegment(part));
|
|
15919
|
+
}
|
|
15920
|
+
function componentTypeSpec_pushIssue(issues, path, message) {
|
|
15921
|
+
issues.push({
|
|
15922
|
+
path,
|
|
15923
|
+
message
|
|
15924
|
+
});
|
|
15925
|
+
}
|
|
15926
|
+
function validateComponentTypeSpec(value, path, issues, allowedSingleSegmentTypes) {
|
|
15927
|
+
if ("string" != typeof value || 0 === value.length) return void componentTypeSpec_pushIssue(issues, path, "must be a non-empty string");
|
|
15928
|
+
const typeName = value;
|
|
15929
|
+
if (typeName.startsWith("form:")) {
|
|
15930
|
+
if (!isValidLegacyFormTypeReference(typeName)) componentTypeSpec_pushIssue(issues, path, LEGACY_FORM_TYPE_REFERENCE_MESSAGE);
|
|
15931
|
+
return;
|
|
15932
|
+
}
|
|
15933
|
+
if (isValidPackageQualifiedComponentType(typeName)) return;
|
|
15934
|
+
if (!isValidIdentifierSegment(typeName)) return void componentTypeSpec_pushIssue(issues, path, "must be a valid identifier");
|
|
15935
|
+
if (!allowedSingleSegmentTypes.has(typeName)) componentTypeSpec_pushIssue(issues, path, COMPONENT_TYPE_REFERENCE_MESSAGE);
|
|
15936
|
+
}
|
|
15937
|
+
const formTemplateValidation_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15938
|
+
const formTemplateValidation_PYTHON_KEYWORDS = new Set([
|
|
15279
15939
|
"False",
|
|
15280
15940
|
"None",
|
|
15281
15941
|
"True",
|
|
@@ -15329,58 +15989,29 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15329
15989
|
"padding",
|
|
15330
15990
|
"spacing"
|
|
15331
15991
|
]);
|
|
15332
|
-
function isValidFormReference(typeName) {
|
|
15333
|
-
if (!typeName.startsWith("form:")) return false;
|
|
15334
|
-
const rest = typeName.slice(5);
|
|
15335
|
-
if (0 === rest.length) return false;
|
|
15336
|
-
let classPath = rest;
|
|
15337
|
-
if (rest.startsWith("dep_")) {
|
|
15338
|
-
const splitIndex = rest.indexOf(":");
|
|
15339
|
-
if (splitIndex <= 0) return false;
|
|
15340
|
-
const depId = rest.slice(0, splitIndex);
|
|
15341
|
-
if (!DEP_ID_RE.test(depId)) return false;
|
|
15342
|
-
classPath = rest.slice(splitIndex + 1);
|
|
15343
|
-
}
|
|
15344
|
-
if (0 === classPath.length) return false;
|
|
15345
|
-
return classPath.split(".").every((part)=>IDENTIFIER_RE.test(part));
|
|
15346
|
-
}
|
|
15347
|
-
function isBuiltinComponentType(typeName) {
|
|
15348
|
-
return BUILTIN_COMPONENT_TYPES.has(typeName);
|
|
15349
|
-
}
|
|
15350
15992
|
function isPlainObject(value) {
|
|
15351
15993
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
15352
15994
|
}
|
|
15353
|
-
function
|
|
15995
|
+
function formTemplateValidation_pushIssue(issues, path, message) {
|
|
15354
15996
|
issues.push({
|
|
15355
15997
|
path,
|
|
15356
15998
|
message
|
|
15357
15999
|
});
|
|
15358
16000
|
}
|
|
15359
|
-
function formatValidationPath(path) {
|
|
15360
|
-
return path || "root";
|
|
15361
|
-
}
|
|
15362
16001
|
function validateIdentifier(value, path, issues) {
|
|
15363
|
-
if ("string" != typeof value || 0 === value.length) return void
|
|
15364
|
-
if (!
|
|
15365
|
-
if (
|
|
16002
|
+
if ("string" != typeof value || 0 === value.length) return void formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
|
|
16003
|
+
if (!formTemplateValidation_IDENTIFIER_RE.test(value)) return void formTemplateValidation_pushIssue(issues, path, "must be a valid identifier");
|
|
16004
|
+
if (formTemplateValidation_PYTHON_KEYWORDS.has(value)) formTemplateValidation_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15366
16005
|
}
|
|
15367
16006
|
function validateComponentType(value, path, issues) {
|
|
15368
|
-
|
|
15369
|
-
const typeName = value;
|
|
15370
|
-
if (typeName.startsWith("form:")) {
|
|
15371
|
-
if (!isValidFormReference(typeName)) pushIssue(issues, path, "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)");
|
|
15372
|
-
return;
|
|
15373
|
-
}
|
|
15374
|
-
if (!IDENTIFIER_RE.test(typeName)) return void pushIssue(issues, path, "must be a valid identifier");
|
|
15375
|
-
if (PYTHON_KEYWORDS.has(typeName)) return void pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15376
|
-
if (!isBuiltinComponentType(typeName)) pushIssue(issues, path, "must be a known Anvil component type (see componentIcons.ts) or a form: reference");
|
|
16007
|
+
validateComponentTypeSpec(value, path, issues, BUILTIN_COMPONENT_TYPES);
|
|
15377
16008
|
}
|
|
15378
16009
|
function validatePropertiesObject(value, path, issues) {
|
|
15379
|
-
if (void 0 !== value && !isPlainObject(value))
|
|
16010
|
+
if (void 0 !== value && !isPlainObject(value)) formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15380
16011
|
}
|
|
15381
16012
|
function validateEventBindingsObject(value, path, issues) {
|
|
15382
16013
|
if (void 0 === value) return;
|
|
15383
|
-
if (!isPlainObject(value)) return void
|
|
16014
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15384
16015
|
for (const [eventName, handlerName] of Object.entries(value)){
|
|
15385
16016
|
validateIdentifier(eventName, `${path}.${eventName}.name`, issues);
|
|
15386
16017
|
validateIdentifier(handlerName, `${path}.${eventName}`, issues);
|
|
@@ -15388,13 +16019,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15388
16019
|
}
|
|
15389
16020
|
function validateDataBindings(value, path, issues) {
|
|
15390
16021
|
if (void 0 === value) return;
|
|
15391
|
-
if (!Array.isArray(value)) return void
|
|
16022
|
+
if (!Array.isArray(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an array");
|
|
15392
16023
|
value.forEach((binding, index)=>{
|
|
15393
16024
|
const bindingPath = `${path}[${index}]`;
|
|
15394
|
-
if (!isPlainObject(binding)) return void
|
|
15395
|
-
if ("string" != typeof binding.property || 0 === binding.property.length)
|
|
15396
|
-
if ("string" != typeof binding.code || 0 === binding.code.length)
|
|
15397
|
-
if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback)
|
|
16025
|
+
if (!isPlainObject(binding)) return void formTemplateValidation_pushIssue(issues, bindingPath, "must be an object");
|
|
16026
|
+
if ("string" != typeof binding.property || 0 === binding.property.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
|
|
16027
|
+
if ("string" != typeof binding.code || 0 === binding.code.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
|
|
16028
|
+
if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) formTemplateValidation_pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
|
|
15398
16029
|
});
|
|
15399
16030
|
}
|
|
15400
16031
|
function validateComponentArray(components, pathPrefix, issues, seenNames) {
|
|
@@ -15402,146 +16033,563 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15402
16033
|
const childPath = `${pathPrefix}[${index}]`;
|
|
15403
16034
|
validateComponentNode(component, childPath, issues, seenNames);
|
|
15404
16035
|
if (isPlainObject(component) && "string" == typeof component.name && component.name.length > 0) {
|
|
15405
|
-
if (seenNames.has(component.name))
|
|
16036
|
+
if (seenNames.has(component.name)) formTemplateValidation_pushIssue(issues, `${childPath}.name`, "must be unique across form components");
|
|
15406
16037
|
seenNames.add(component.name);
|
|
15407
16038
|
}
|
|
15408
16039
|
});
|
|
15409
16040
|
}
|
|
15410
16041
|
function validateComponentNode(value, path, issues, seenNames) {
|
|
15411
|
-
if (!isPlainObject(value)) return void
|
|
16042
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15412
16043
|
validateComponentType(value.type, `${path}.type`, issues);
|
|
15413
|
-
|
|
15414
|
-
else validateIdentifier(value.name, `${path}.name`, issues);
|
|
16044
|
+
validateComponentName(value.name, `${path}.name`, issues);
|
|
15415
16045
|
validatePropertiesObject(value.properties, `${path}.properties`, issues);
|
|
15416
16046
|
validatePropertiesObject(value.layout_properties, `${path}.layout_properties`, issues);
|
|
15417
16047
|
validateEventBindingsObject(value.event_bindings, `${path}.event_bindings`, issues);
|
|
15418
16048
|
validateDataBindings(value.data_bindings, `${path}.data_bindings`, issues);
|
|
15419
16049
|
if (void 0 !== value.components) if (Array.isArray(value.components)) validateComponentArray(value.components, `${path}.components`, issues, seenNames);
|
|
15420
|
-
else
|
|
16050
|
+
else formTemplateValidation_pushIssue(issues, `${path}.components`, "must be an array");
|
|
15421
16051
|
}
|
|
15422
16052
|
function validatePropertyDefinition(value, path, issues) {
|
|
15423
|
-
if (!isPlainObject(value)) return void
|
|
16053
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15424
16054
|
validateIdentifier(value.name, `${path}.name`, issues);
|
|
15425
|
-
if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type))
|
|
15426
|
-
if (void 0 !== value.description && "string" != typeof value.description)
|
|
15427
|
-
if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop)
|
|
15428
|
-
if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback)
|
|
16055
|
+
if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type)) formTemplateValidation_pushIssue(issues, `${path}.type`, "must be a supported property type");
|
|
16056
|
+
if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
|
|
16057
|
+
if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop) formTemplateValidation_pushIssue(issues, `${path}.default_binding_prop`, "must be a boolean");
|
|
16058
|
+
if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback) formTemplateValidation_pushIssue(issues, `${path}.allow_binding_writeback`, "must be a boolean");
|
|
15429
16059
|
if (void 0 !== value.binding_writeback_events) if (Array.isArray(value.binding_writeback_events)) value.binding_writeback_events.forEach((eventName, index)=>{
|
|
15430
|
-
if ("string" != typeof eventName || 0 === eventName.length)
|
|
16060
|
+
if ("string" != typeof eventName || 0 === eventName.length) formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events[${index}]`, "must be a non-empty string");
|
|
15431
16061
|
});
|
|
15432
|
-
else
|
|
16062
|
+
else formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events`, "must be an array");
|
|
15433
16063
|
if ("enum" === value.type) {
|
|
15434
|
-
if (!Array.isArray(value.options) || value.options.some((option)=>"string" != typeof option))
|
|
16064
|
+
if (!Array.isArray(value.options) || value.options.some((option)=>"string" != typeof option)) formTemplateValidation_pushIssue(issues, `${path}.options`, "must be an array of strings for enum properties");
|
|
15435
16065
|
}
|
|
15436
16066
|
}
|
|
15437
16067
|
function validateEventDefinition(value, path, issues) {
|
|
15438
|
-
if (!isPlainObject(value)) return void
|
|
16068
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15439
16069
|
validateIdentifier(value.name, `${path}.name`, issues);
|
|
15440
|
-
if (void 0 !== value.description && "string" != typeof value.description)
|
|
15441
|
-
if (void 0 !== value.default_event && "boolean" != typeof value.default_event)
|
|
16070
|
+
if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
|
|
16071
|
+
if (void 0 !== value.default_event && "boolean" != typeof value.default_event) formTemplateValidation_pushIssue(issues, `${path}.default_event`, "must be a boolean");
|
|
15442
16072
|
if (void 0 !== value.parameters) if (Array.isArray(value.parameters)) value.parameters.forEach((parameter, index)=>{
|
|
15443
|
-
if (!isPlainObject(parameter)) return void
|
|
16073
|
+
if (!isPlainObject(parameter)) return void formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}]`, "must be an object");
|
|
15444
16074
|
validateIdentifier(parameter.name, `${path}.parameters[${index}].name`, issues);
|
|
15445
|
-
if (void 0 !== parameter.description && "string" != typeof parameter.description)
|
|
16075
|
+
if (void 0 !== parameter.description && "string" != typeof parameter.description) formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}].description`, "must be a string");
|
|
15446
16076
|
});
|
|
15447
|
-
else
|
|
16077
|
+
else formTemplateValidation_pushIssue(issues, `${path}.parameters`, "must be an array");
|
|
15448
16078
|
}
|
|
15449
16079
|
function validateSlotDefinition(value, path, issues, seenNames) {
|
|
15450
|
-
if (!isPlainObject(value)) return void
|
|
15451
|
-
if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0)
|
|
16080
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
16081
|
+
if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0) formTemplateValidation_pushIssue(issues, `${path}.index`, "must be a non-negative integer");
|
|
15452
16082
|
validatePropertiesObject(value.set_layout_properties, `${path}.set_layout_properties`, issues);
|
|
15453
16083
|
if (isPlainObject(value.target)) {
|
|
15454
|
-
if ("container" !== value.target.type && "slot" !== value.target.type)
|
|
15455
|
-
validateIdentifier(value.target.name, `${path}.target.name`, issues);
|
|
15456
|
-
} else
|
|
16084
|
+
if ("container" !== value.target.type && "slot" !== value.target.type) formTemplateValidation_pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
|
|
16085
|
+
if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
|
|
16086
|
+
} else formTemplateValidation_pushIssue(issues, `${path}.target`, "must be an object");
|
|
15457
16087
|
if (void 0 !== value.template) validateComponentNode(value.template, `${path}.template`, issues, seenNames);
|
|
15458
16088
|
}
|
|
16089
|
+
function prefixedPath(pathPrefix, path) {
|
|
16090
|
+
return pathPrefix.length > 0 ? `${pathPrefix}.${path}` : path;
|
|
16091
|
+
}
|
|
16092
|
+
function validateFormTemplateMetadata(value, options = {}) {
|
|
16093
|
+
const issues = [];
|
|
16094
|
+
if (!isPlainObject(value)) {
|
|
16095
|
+
formTemplateValidation_pushIssue(issues, options.pathPrefix ?? "", "must be a YAML object");
|
|
16096
|
+
return issues;
|
|
16097
|
+
}
|
|
16098
|
+
const pathPrefix = options.pathPrefix ?? "";
|
|
16099
|
+
const path = (suffix)=>prefixedPath(pathPrefix, suffix);
|
|
16100
|
+
if (void 0 !== value.custom_component && "boolean" != typeof value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component"), "must be a boolean");
|
|
16101
|
+
if (void 0 !== value.custom_component_container && "boolean" != typeof value.custom_component_container) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "must be a boolean");
|
|
16102
|
+
if (true === value.custom_component_container && true !== value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "requires custom_component: true");
|
|
16103
|
+
const properties = value.properties;
|
|
16104
|
+
if (void 0 !== properties) if (Array.isArray(properties)) {
|
|
16105
|
+
const seenNames = new Set();
|
|
16106
|
+
let defaultBindingPropCount = 0;
|
|
16107
|
+
properties.forEach((property, index)=>{
|
|
16108
|
+
validatePropertyDefinition(property, path(`properties[${index}]`), issues);
|
|
16109
|
+
if (!isPlainObject(property) || "string" != typeof property.name) return;
|
|
16110
|
+
if (seenNames.has(property.name)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].name`), "must be unique");
|
|
16111
|
+
seenNames.add(property.name);
|
|
16112
|
+
if (property.default_binding_prop) defaultBindingPropCount++;
|
|
16113
|
+
});
|
|
16114
|
+
if (defaultBindingPropCount > 1) formTemplateValidation_pushIssue(issues, path("properties"), "can only define one default_binding_prop");
|
|
16115
|
+
} else formTemplateValidation_pushIssue(issues, path("properties"), "must be an array");
|
|
16116
|
+
const events = value.events;
|
|
16117
|
+
const eventNames = new Set();
|
|
16118
|
+
if (void 0 !== events) if (Array.isArray(events)) {
|
|
16119
|
+
let defaultEventCount = 0;
|
|
16120
|
+
events.forEach((event, index)=>{
|
|
16121
|
+
validateEventDefinition(event, path(`events[${index}]`), issues);
|
|
16122
|
+
if (!isPlainObject(event) || "string" != typeof event.name) return;
|
|
16123
|
+
if (eventNames.has(event.name)) formTemplateValidation_pushIssue(issues, path(`events[${index}].name`), "must be unique");
|
|
16124
|
+
eventNames.add(event.name);
|
|
16125
|
+
if (event.default_event) defaultEventCount++;
|
|
16126
|
+
});
|
|
16127
|
+
if (defaultEventCount > 1) formTemplateValidation_pushIssue(issues, path("events"), "can only define one default_event");
|
|
16128
|
+
} else formTemplateValidation_pushIssue(issues, path("events"), "must be an array");
|
|
16129
|
+
if (Array.isArray(properties)) properties.forEach((property, index)=>{
|
|
16130
|
+
if (!isPlainObject(property) || !Array.isArray(property.binding_writeback_events)) return;
|
|
16131
|
+
property.binding_writeback_events.forEach((eventName, eventIndex)=>{
|
|
16132
|
+
if ("string" == typeof eventName && !eventNames.has(eventName)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].binding_writeback_events[${eventIndex}]`), "must reference a defined event");
|
|
16133
|
+
});
|
|
16134
|
+
});
|
|
16135
|
+
if (void 0 !== value.slots) {
|
|
16136
|
+
const slotsPath = path("slots");
|
|
16137
|
+
if (isPlainObject(value.slots)) if ("empty-marker" === options.slotsPolicy) {
|
|
16138
|
+
if (Object.keys(value.slots).length > 0) formTemplateValidation_pushIssue(issues, slotsPath, "must be empty when defined in frontmatter");
|
|
16139
|
+
} else {
|
|
16140
|
+
const componentNames = options.componentNames ?? new Set();
|
|
16141
|
+
for (const [slotName, slotDef] of Object.entries(value.slots)){
|
|
16142
|
+
validateIdentifier(slotName, path(`slots.${slotName}.name`), issues);
|
|
16143
|
+
validateSlotDefinition(slotDef, path(`slots.${slotName}`), issues, componentNames);
|
|
16144
|
+
}
|
|
16145
|
+
}
|
|
16146
|
+
else formTemplateValidation_pushIssue(issues, slotsPath, "must be an object");
|
|
16147
|
+
}
|
|
16148
|
+
if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
|
|
16149
|
+
if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
|
|
16150
|
+
if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
|
|
16151
|
+
if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
|
|
16152
|
+
} else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
|
|
16153
|
+
return issues;
|
|
16154
|
+
}
|
|
15459
16155
|
function validateFormTemplateData(value) {
|
|
15460
16156
|
const issues = [];
|
|
15461
16157
|
const componentNames = new Set();
|
|
15462
16158
|
if (!isPlainObject(value)) {
|
|
15463
|
-
|
|
16159
|
+
formTemplateValidation_pushIssue(issues, "", "must be a YAML object");
|
|
15464
16160
|
return issues;
|
|
15465
16161
|
}
|
|
15466
16162
|
const hasContainer = void 0 !== value.container;
|
|
15467
16163
|
const hasLayout = void 0 !== value.layout;
|
|
15468
16164
|
const hasComponents = void 0 !== value.components;
|
|
15469
16165
|
const hasComponentsBySlot = void 0 !== value.components_by_slot;
|
|
15470
|
-
if (hasContainer && hasLayout)
|
|
15471
|
-
if (!hasContainer && !hasLayout)
|
|
15472
|
-
if (void 0 !== value.is_package && "boolean" != typeof value.is_package)
|
|
16166
|
+
if (hasContainer && hasLayout) formTemplateValidation_pushIssue(issues, "", "cannot define both container and layout");
|
|
16167
|
+
if (!hasContainer && !hasLayout) formTemplateValidation_pushIssue(issues, "", "must define either container or layout");
|
|
16168
|
+
if (void 0 !== value.is_package && "boolean" != typeof value.is_package) formTemplateValidation_pushIssue(issues, "is_package", "must be a boolean");
|
|
15473
16169
|
if (hasContainer) if (isPlainObject(value.container)) {
|
|
15474
16170
|
validateComponentType(value.container.type, "container.type", issues);
|
|
15475
16171
|
validatePropertiesObject(value.container.properties, "container.properties", issues);
|
|
15476
16172
|
validatePropertiesObject(value.container.layout_properties, "container.layout_properties", issues);
|
|
15477
16173
|
validateEventBindingsObject(value.container.event_bindings, "container.event_bindings", issues);
|
|
15478
16174
|
validateDataBindings(value.container.data_bindings, "container.data_bindings", issues);
|
|
15479
|
-
} else
|
|
16175
|
+
} else formTemplateValidation_pushIssue(issues, "container", "must be an object");
|
|
15480
16176
|
if (hasLayout) if (isPlainObject(value.layout)) {
|
|
15481
16177
|
validateComponentType(value.layout.type, "layout.type", issues);
|
|
15482
16178
|
validatePropertiesObject(value.layout.properties, "layout.properties", issues);
|
|
15483
16179
|
validateEventBindingsObject(value.layout.event_bindings, "layout.event_bindings", issues);
|
|
15484
16180
|
validateEventBindingsObject(value.layout.form_event_bindings, "layout.form_event_bindings", issues);
|
|
15485
16181
|
validateDataBindings(value.layout.data_bindings, "layout.data_bindings", issues);
|
|
15486
|
-
} else
|
|
15487
|
-
if (hasComponents && hasLayout)
|
|
15488
|
-
else if (hasComponents && !hasContainer)
|
|
16182
|
+
} else formTemplateValidation_pushIssue(issues, "layout", "must be an object");
|
|
16183
|
+
if (hasComponents && hasLayout) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
|
|
16184
|
+
else if (hasComponents && !hasContainer) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
|
|
15489
16185
|
else if (hasComponents) if (Array.isArray(value.components)) validateComponentArray(value.components, "components", issues, componentNames);
|
|
15490
|
-
else
|
|
15491
|
-
if (hasComponentsBySlot && hasContainer)
|
|
15492
|
-
else if (hasComponentsBySlot && !hasLayout)
|
|
16186
|
+
else formTemplateValidation_pushIssue(issues, "components", "must be an array");
|
|
16187
|
+
if (hasComponentsBySlot && hasContainer) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
|
|
16188
|
+
else if (hasComponentsBySlot && !hasLayout) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
|
|
15493
16189
|
else if (hasComponentsBySlot) if (isPlainObject(value.components_by_slot)) for (const [slotName, slotComponents] of Object.entries(value.components_by_slot)){
|
|
15494
16190
|
if (!Array.isArray(slotComponents)) {
|
|
15495
|
-
|
|
16191
|
+
formTemplateValidation_pushIssue(issues, `components_by_slot.${slotName}`, "must be an array");
|
|
15496
16192
|
continue;
|
|
15497
16193
|
}
|
|
15498
16194
|
validateComponentArray(slotComponents, `components_by_slot.${slotName}`, issues, componentNames);
|
|
15499
16195
|
}
|
|
15500
|
-
else
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
|
|
15509
|
-
|
|
15510
|
-
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
16196
|
+
else formTemplateValidation_pushIssue(issues, "components_by_slot", "must be an object");
|
|
16197
|
+
issues.push(...validateFormTemplateMetadata(value, {
|
|
16198
|
+
componentNames
|
|
16199
|
+
}));
|
|
16200
|
+
return issues;
|
|
16201
|
+
}
|
|
16202
|
+
const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
|
|
16203
|
+
"/runtime/services/tables.yml",
|
|
16204
|
+
"/runtime/services/google.yml",
|
|
16205
|
+
"/runtime/services/uplink.yml",
|
|
16206
|
+
"/runtime/services/stripe.yml",
|
|
16207
|
+
"/runtime/services/segment.yml",
|
|
16208
|
+
"/runtime/services/facebook.yml",
|
|
16209
|
+
"/runtime/services/anvil/users.yml",
|
|
16210
|
+
"/runtime/services/anvil/secrets.yml",
|
|
16211
|
+
"/runtime/services/anvil/saml.yml",
|
|
16212
|
+
"/runtime/services/anvil/microsoft.yml",
|
|
16213
|
+
"/runtime/services/anvil/email.yml",
|
|
16214
|
+
"/runtime/services/anvil/files.yml"
|
|
16215
|
+
]);
|
|
16216
|
+
const FRONTMATTER_ALLOWED_KEYS = new Set([
|
|
16217
|
+
"custom_component",
|
|
16218
|
+
"custom_component_container",
|
|
16219
|
+
"properties",
|
|
16220
|
+
"events",
|
|
16221
|
+
"toolbox_item",
|
|
16222
|
+
"layout_metadata",
|
|
16223
|
+
"item_type",
|
|
16224
|
+
"slots"
|
|
16225
|
+
]);
|
|
16226
|
+
const FRONTMATTER_DISALLOWED_KEYS = new Set([
|
|
16227
|
+
"container",
|
|
16228
|
+
"components",
|
|
16229
|
+
"layout",
|
|
16230
|
+
"components_by_slot",
|
|
16231
|
+
"code",
|
|
16232
|
+
"class_name",
|
|
16233
|
+
"is_package",
|
|
16234
|
+
"save_as_html",
|
|
16235
|
+
"serialized_html"
|
|
16236
|
+
]);
|
|
16237
|
+
function validateFormTemplateHtml_isPlainObject(value) {
|
|
16238
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
16239
|
+
}
|
|
16240
|
+
function validateFormTemplateHtml_pushIssue(issues, issuePath, message) {
|
|
16241
|
+
issues.push({
|
|
16242
|
+
path: issuePath,
|
|
16243
|
+
message
|
|
16244
|
+
});
|
|
16245
|
+
}
|
|
16246
|
+
function splitHtmlFormTemplateFrontmatter(htmlContent) {
|
|
16247
|
+
const match = htmlContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
16248
|
+
if (!match) return {
|
|
16249
|
+
frontmatter: null,
|
|
16250
|
+
body: htmlContent
|
|
16251
|
+
};
|
|
16252
|
+
return {
|
|
16253
|
+
frontmatter: match[1],
|
|
16254
|
+
body: htmlContent.slice(match[0].length)
|
|
16255
|
+
};
|
|
16256
|
+
}
|
|
16257
|
+
function validateHtmlFrontmatter(frontmatterYaml) {
|
|
16258
|
+
const issues = [];
|
|
16259
|
+
if (0 === frontmatterYaml.trim().length) {
|
|
16260
|
+
validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must not be empty; omit frontmatter entirely when unused");
|
|
16261
|
+
return issues;
|
|
16262
|
+
}
|
|
16263
|
+
let data;
|
|
16264
|
+
try {
|
|
16265
|
+
data = external_js_yaml_namespaceObject.load(frontmatterYaml);
|
|
16266
|
+
} catch (error) {
|
|
16267
|
+
validateFormTemplateHtml_pushIssue(issues, "frontmatter", error instanceof Error ? error.message : String(error));
|
|
16268
|
+
return issues;
|
|
16269
|
+
}
|
|
16270
|
+
if (!validateFormTemplateHtml_isPlainObject(data)) {
|
|
16271
|
+
validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must be a YAML object");
|
|
16272
|
+
return issues;
|
|
16273
|
+
}
|
|
16274
|
+
for (const key of Object.keys(data))if (FRONTMATTER_DISALLOWED_KEYS.has(key)) validateFormTemplateHtml_pushIssue(issues, `frontmatter.${key}`, "belongs in the HTML body, not frontmatter");
|
|
16275
|
+
else if (!FRONTMATTER_ALLOWED_KEYS.has(key)) validateFormTemplateHtml_pushIssue(issues, `frontmatter.${key}`, "is not an allowed frontmatter field");
|
|
16276
|
+
issues.push(...validateFormTemplateMetadata(data, {
|
|
16277
|
+
pathPrefix: "frontmatter",
|
|
16278
|
+
slotsPolicy: "empty-marker"
|
|
16279
|
+
}));
|
|
16280
|
+
return issues;
|
|
16281
|
+
}
|
|
16282
|
+
function validateParsedHtmlTemplate(parsed) {
|
|
16283
|
+
return validateFormTemplateData(parsed);
|
|
16284
|
+
}
|
|
16285
|
+
function validateFormTemplateHtml(htmlContent) {
|
|
16286
|
+
const { frontmatter, body } = splitHtmlFormTemplateFrontmatter(htmlContent);
|
|
16287
|
+
if (null !== frontmatter) {
|
|
16288
|
+
const frontmatterIssues = validateHtmlFrontmatter(frontmatter);
|
|
16289
|
+
if (frontmatterIssues.length > 0) return {
|
|
16290
|
+
ok: false,
|
|
16291
|
+
message: "form_template.html is invalid for Anvil",
|
|
16292
|
+
issues: frontmatterIssues
|
|
16293
|
+
};
|
|
16294
|
+
}
|
|
16295
|
+
let parsed;
|
|
16296
|
+
try {
|
|
16297
|
+
parsed = (0, form_template_parser_namespaceObject.parseSerializedHtml)(body);
|
|
16298
|
+
} catch (error) {
|
|
16299
|
+
return {
|
|
16300
|
+
ok: false,
|
|
16301
|
+
message: "form_template.html could not be parsed",
|
|
16302
|
+
issues: [
|
|
16303
|
+
{
|
|
16304
|
+
path: "",
|
|
16305
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16306
|
+
}
|
|
16307
|
+
]
|
|
16308
|
+
};
|
|
16309
|
+
}
|
|
16310
|
+
const issues = validateParsedHtmlTemplate(parsed);
|
|
16311
|
+
if (issues.length > 0) return {
|
|
16312
|
+
ok: false,
|
|
16313
|
+
message: "form_template.html is invalid for Anvil",
|
|
16314
|
+
issues
|
|
16315
|
+
};
|
|
16316
|
+
return {
|
|
16317
|
+
ok: true
|
|
16318
|
+
};
|
|
16319
|
+
}
|
|
16320
|
+
const PYTHON_VALIDATOR_SCRIPT = String.raw`
|
|
16321
|
+
import ast
|
|
16322
|
+
import json
|
|
16323
|
+
import sys
|
|
16324
|
+
|
|
16325
|
+
|
|
16326
|
+
def issue_path(error):
|
|
16327
|
+
if getattr(error, "lineno", None) is None:
|
|
16328
|
+
return "root"
|
|
16329
|
+
if getattr(error, "offset", None) is None:
|
|
16330
|
+
return f"line {error.lineno}"
|
|
16331
|
+
return f"line {error.lineno}, column {error.offset}"
|
|
16332
|
+
|
|
16333
|
+
|
|
16334
|
+
def base_name(node):
|
|
16335
|
+
if isinstance(node, ast.Name):
|
|
16336
|
+
return node.id
|
|
16337
|
+
if isinstance(node, ast.Attribute):
|
|
16338
|
+
return node.attr
|
|
16339
|
+
return None
|
|
16340
|
+
|
|
16341
|
+
|
|
16342
|
+
def imports_template_from_anvil_designer(node, template_name):
|
|
16343
|
+
if node.level < 1:
|
|
16344
|
+
return False
|
|
16345
|
+
if node.module not in ("_anvil_designer", None):
|
|
16346
|
+
return False
|
|
16347
|
+
return any(alias.name == template_name for alias in node.names)
|
|
16348
|
+
|
|
16349
|
+
|
|
16350
|
+
file_path = sys.argv[1]
|
|
16351
|
+
class_name = sys.argv[2]
|
|
16352
|
+
template_name = f"{class_name}Template" if class_name else ""
|
|
16353
|
+
source = sys.stdin.read()
|
|
16354
|
+
issues = []
|
|
16355
|
+
|
|
16356
|
+
try:
|
|
16357
|
+
compile(source, file_path, "exec")
|
|
16358
|
+
except SyntaxError as error:
|
|
16359
|
+
issues.append({"path": issue_path(error), "message": error.msg})
|
|
16360
|
+
print(json.dumps({"issues": issues}))
|
|
16361
|
+
sys.exit(0)
|
|
16362
|
+
|
|
16363
|
+
if class_name:
|
|
16364
|
+
tree = ast.parse(source, filename=file_path)
|
|
16365
|
+
has_template_import = False
|
|
16366
|
+
has_form_class = False
|
|
16367
|
+
|
|
16368
|
+
for node in ast.walk(tree):
|
|
16369
|
+
if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
|
|
16370
|
+
has_template_import = True
|
|
16371
|
+
elif isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
16372
|
+
if any(base_name(base) == template_name for base in node.bases):
|
|
16373
|
+
has_form_class = True
|
|
16374
|
+
|
|
16375
|
+
if not has_template_import:
|
|
16376
|
+
issues.append({
|
|
16377
|
+
"path": "root",
|
|
16378
|
+
"message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
|
|
16379
|
+
})
|
|
16380
|
+
if not has_form_class:
|
|
16381
|
+
issues.append({
|
|
16382
|
+
"path": "root",
|
|
16383
|
+
"message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
|
|
16384
|
+
})
|
|
16385
|
+
|
|
16386
|
+
print(json.dumps({"issues": issues}))
|
|
16387
|
+
`;
|
|
16388
|
+
function findPythonInterpreter(env = process.env, probe = external_child_process_namespaceObject.spawnSync) {
|
|
16389
|
+
const candidates = [
|
|
16390
|
+
..."string" == typeof env.ANVIL_PYTHON && env.ANVIL_PYTHON.length > 0 ? [
|
|
16391
|
+
{
|
|
16392
|
+
command: env.ANVIL_PYTHON,
|
|
16393
|
+
args: []
|
|
16394
|
+
}
|
|
16395
|
+
] : [],
|
|
16396
|
+
{
|
|
16397
|
+
command: "uv",
|
|
16398
|
+
args: [
|
|
16399
|
+
"run",
|
|
16400
|
+
"--isolated",
|
|
16401
|
+
"--no-python-downloads",
|
|
16402
|
+
"python"
|
|
16403
|
+
]
|
|
16404
|
+
},
|
|
16405
|
+
{
|
|
16406
|
+
command: "python3",
|
|
16407
|
+
args: []
|
|
16408
|
+
},
|
|
16409
|
+
{
|
|
16410
|
+
command: "python",
|
|
16411
|
+
args: []
|
|
16412
|
+
}
|
|
16413
|
+
];
|
|
16414
|
+
for (const candidate of candidates){
|
|
16415
|
+
const result = probe(candidate.command, [
|
|
16416
|
+
...candidate.args,
|
|
16417
|
+
"--version"
|
|
16418
|
+
], {
|
|
16419
|
+
encoding: "utf8"
|
|
15534
16420
|
});
|
|
16421
|
+
if (!result.error && 0 === result.status) return candidate;
|
|
16422
|
+
}
|
|
16423
|
+
return null;
|
|
16424
|
+
}
|
|
16425
|
+
function isClientFormPython(filePath) {
|
|
16426
|
+
if ("__init__.py" === external_path_default().basename(filePath)) {
|
|
16427
|
+
const formDir = external_path_default().dirname(filePath);
|
|
16428
|
+
return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
|
|
16429
|
+
}
|
|
16430
|
+
const parsed = external_path_default().parse(filePath);
|
|
16431
|
+
return external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.html`)) || external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.yaml`));
|
|
16432
|
+
}
|
|
16433
|
+
function expectedFormClassName(filePath) {
|
|
16434
|
+
if (!isClientFormPython(filePath)) return null;
|
|
16435
|
+
if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
|
|
16436
|
+
return external_path_default().basename(filePath, ".py");
|
|
16437
|
+
}
|
|
16438
|
+
function isUnderDirectory(filePath, directoryPath) {
|
|
16439
|
+
const relativePath = external_path_default().relative(directoryPath, filePath);
|
|
16440
|
+
return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
|
|
16441
|
+
}
|
|
16442
|
+
function validatePython(filePath, fileContent, appRoot) {
|
|
16443
|
+
const interpreter = findPythonInterpreter();
|
|
16444
|
+
if (!interpreter) return {
|
|
16445
|
+
ok: false,
|
|
16446
|
+
message: "Python file could not be validated",
|
|
16447
|
+
issues: [
|
|
16448
|
+
{
|
|
16449
|
+
path: "root",
|
|
16450
|
+
message: "Python validation requires uv, python3, or python on PATH, or ANVIL_PYTHON to be set"
|
|
16451
|
+
}
|
|
16452
|
+
]
|
|
16453
|
+
};
|
|
16454
|
+
const absolutePath = external_path_default().resolve(filePath);
|
|
16455
|
+
const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
|
|
16456
|
+
const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
|
|
16457
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter.command, [
|
|
16458
|
+
...interpreter.args,
|
|
16459
|
+
"-c",
|
|
16460
|
+
PYTHON_VALIDATOR_SCRIPT,
|
|
16461
|
+
filePath,
|
|
16462
|
+
formClassName ?? ""
|
|
16463
|
+
], {
|
|
16464
|
+
encoding: "utf8",
|
|
16465
|
+
input: fileContent,
|
|
16466
|
+
maxBuffer: 1048576
|
|
16467
|
+
});
|
|
16468
|
+
if (result.error || 0 !== result.status) return {
|
|
16469
|
+
ok: false,
|
|
16470
|
+
message: "Python file could not be validated",
|
|
16471
|
+
issues: [
|
|
16472
|
+
{
|
|
16473
|
+
path: "root",
|
|
16474
|
+
message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
|
|
16475
|
+
}
|
|
16476
|
+
]
|
|
16477
|
+
};
|
|
16478
|
+
let parsed;
|
|
16479
|
+
try {
|
|
16480
|
+
parsed = JSON.parse(result.stdout);
|
|
16481
|
+
} catch (error) {
|
|
16482
|
+
return {
|
|
16483
|
+
ok: false,
|
|
16484
|
+
message: "Python file could not be validated",
|
|
16485
|
+
issues: [
|
|
16486
|
+
{
|
|
16487
|
+
path: "root",
|
|
16488
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16489
|
+
}
|
|
16490
|
+
]
|
|
16491
|
+
};
|
|
16492
|
+
}
|
|
16493
|
+
const issues = parsed.issues ?? [];
|
|
16494
|
+
if (issues.length > 0) return {
|
|
16495
|
+
ok: false,
|
|
16496
|
+
message: "Python file is invalid for Anvil",
|
|
16497
|
+
issues
|
|
16498
|
+
};
|
|
16499
|
+
return {
|
|
16500
|
+
ok: true
|
|
16501
|
+
};
|
|
16502
|
+
}
|
|
16503
|
+
const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16504
|
+
const DB_ACCESS_LEVELS = new Set([
|
|
16505
|
+
"none",
|
|
16506
|
+
"search",
|
|
16507
|
+
"full"
|
|
16508
|
+
]);
|
|
16509
|
+
const DB_BASIC_COLUMN_TYPES = new Set([
|
|
16510
|
+
"string",
|
|
16511
|
+
"number",
|
|
16512
|
+
"bool",
|
|
16513
|
+
"date",
|
|
16514
|
+
"datetime",
|
|
16515
|
+
"media",
|
|
16516
|
+
"simpleObject"
|
|
16517
|
+
]);
|
|
16518
|
+
const DB_LINK_COLUMN_TYPES = new Set([
|
|
16519
|
+
"link_single",
|
|
16520
|
+
"link_multiple"
|
|
16521
|
+
]);
|
|
16522
|
+
const DB_TABLE_INDEX_TYPES = new Set([
|
|
16523
|
+
"b_tree",
|
|
16524
|
+
"trigram",
|
|
16525
|
+
"full_text"
|
|
16526
|
+
]);
|
|
16527
|
+
const SCHEDULE_EVERY_VALUES = new Set([
|
|
16528
|
+
"minute",
|
|
16529
|
+
"hour",
|
|
16530
|
+
"day",
|
|
16531
|
+
"week",
|
|
16532
|
+
"month"
|
|
16533
|
+
]);
|
|
16534
|
+
const CLIENT_VERSIONS = new Set([
|
|
16535
|
+
"2",
|
|
16536
|
+
"3"
|
|
16537
|
+
]);
|
|
16538
|
+
const validators_PYTHON_KEYWORDS = new Set([
|
|
16539
|
+
"False",
|
|
16540
|
+
"None",
|
|
16541
|
+
"True",
|
|
16542
|
+
"and",
|
|
16543
|
+
"as",
|
|
16544
|
+
"assert",
|
|
16545
|
+
"async",
|
|
16546
|
+
"await",
|
|
16547
|
+
"break",
|
|
16548
|
+
"class",
|
|
16549
|
+
"continue",
|
|
16550
|
+
"def",
|
|
16551
|
+
"del",
|
|
16552
|
+
"elif",
|
|
16553
|
+
"else",
|
|
16554
|
+
"except",
|
|
16555
|
+
"finally",
|
|
16556
|
+
"for",
|
|
16557
|
+
"from",
|
|
16558
|
+
"global",
|
|
16559
|
+
"if",
|
|
16560
|
+
"import",
|
|
16561
|
+
"in",
|
|
16562
|
+
"is",
|
|
16563
|
+
"lambda",
|
|
16564
|
+
"nonlocal",
|
|
16565
|
+
"not",
|
|
16566
|
+
"or",
|
|
16567
|
+
"pass",
|
|
16568
|
+
"raise",
|
|
16569
|
+
"return",
|
|
16570
|
+
"try",
|
|
16571
|
+
"while",
|
|
16572
|
+
"with",
|
|
16573
|
+
"yield",
|
|
16574
|
+
"match",
|
|
16575
|
+
"case"
|
|
16576
|
+
]);
|
|
16577
|
+
function validators_isPlainObject(value) {
|
|
16578
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
16579
|
+
}
|
|
16580
|
+
function validators_pushIssue(issues, path, message) {
|
|
16581
|
+
issues.push({
|
|
16582
|
+
path,
|
|
16583
|
+
message
|
|
15535
16584
|
});
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
if (
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
return issues;
|
|
16585
|
+
}
|
|
16586
|
+
function formatValidationPath(path) {
|
|
16587
|
+
return path || "root";
|
|
16588
|
+
}
|
|
16589
|
+
function validators_validateIdentifier(value, path, issues) {
|
|
16590
|
+
if ("string" != typeof value || 0 === value.length) return void validators_pushIssue(issues, path, "must be a non-empty string");
|
|
16591
|
+
if (!validators_IDENTIFIER_RE.test(value)) return void validators_pushIssue(issues, path, "must be a valid identifier");
|
|
16592
|
+
if (validators_PYTHON_KEYWORDS.has(value)) validators_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15545
16593
|
}
|
|
15546
16594
|
function validateFormTemplate(yamlContent) {
|
|
15547
16595
|
try {
|
|
@@ -15568,7 +16616,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15568
16616
|
};
|
|
15569
16617
|
}
|
|
15570
16618
|
}
|
|
15571
|
-
const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>
|
|
16619
|
+
const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>validators_isPlainObject(input), "must be an object");
|
|
15572
16620
|
const AnvilServiceEntrySchema = external_valibot_namespaceObject.looseObject({
|
|
15573
16621
|
source: external_valibot_namespaceObject.pipe(external_valibot_namespaceObject.string(), external_valibot_namespaceObject.nonEmpty()),
|
|
15574
16622
|
client_config: external_valibot_namespaceObject.optional(serviceConfigObject),
|
|
@@ -15596,14 +16644,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15596
16644
|
function validateRuntimeOptions(value, path) {
|
|
15597
16645
|
const issues = [];
|
|
15598
16646
|
if (void 0 === value) return issues;
|
|
15599
|
-
if (!
|
|
16647
|
+
if (!validators_isPlainObject(value)) return issues;
|
|
15600
16648
|
if (void 0 !== value.client_version) {
|
|
15601
|
-
if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version))
|
|
16649
|
+
if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version)) validators_pushIssue(issues, `${path}.client_version`, 'must be "2" or "3"');
|
|
15602
16650
|
}
|
|
15603
16651
|
if (void 0 !== value.server_spec && null !== value.server_spec) {
|
|
15604
|
-
if (!
|
|
15605
|
-
if ("requirements" in value.server_spec)
|
|
15606
|
-
if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base)
|
|
16652
|
+
if (!validators_isPlainObject(value.server_spec)) return issues;
|
|
16653
|
+
if ("requirements" in value.server_spec) validators_pushIssue(issues, `${path}.server_spec.requirements`, "is not a valid anvil.yaml field; requirements come from server_code/requirements.txt");
|
|
16654
|
+
if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base) validators_pushIssue(issues, `${path}.server_spec.base`, "must be a string");
|
|
15607
16655
|
}
|
|
15608
16656
|
return issues;
|
|
15609
16657
|
}
|
|
@@ -15611,27 +16659,27 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15611
16659
|
const issues = [];
|
|
15612
16660
|
if (void 0 === value) return issues;
|
|
15613
16661
|
if (null === value) {
|
|
15614
|
-
|
|
16662
|
+
validators_pushIssue(issues, path, "must be an object, an empty array, or omitted");
|
|
15615
16663
|
return issues;
|
|
15616
16664
|
}
|
|
15617
16665
|
if (Array.isArray(value)) {
|
|
15618
16666
|
if (0 === value.length) return issues;
|
|
15619
|
-
|
|
16667
|
+
validators_pushIssue(issues, path, "must be an object mapping table names to schemas, or an empty array");
|
|
15620
16668
|
return issues;
|
|
15621
16669
|
}
|
|
15622
|
-
if (!
|
|
15623
|
-
|
|
16670
|
+
if (!validators_isPlainObject(value)) {
|
|
16671
|
+
validators_pushIssue(issues, path, "must be an object mapping table names to schemas");
|
|
15624
16672
|
return issues;
|
|
15625
16673
|
}
|
|
15626
16674
|
for (const tableName of Object.keys(value)){
|
|
15627
16675
|
const tablePath = `${path}.${tableName}`;
|
|
15628
|
-
if (!
|
|
15629
|
-
|
|
16676
|
+
if (!validators_IDENTIFIER_RE.test(tableName)) {
|
|
16677
|
+
validators_pushIssue(issues, tablePath, "must be a valid Python identifier");
|
|
15630
16678
|
continue;
|
|
15631
16679
|
}
|
|
15632
16680
|
const table = value[tableName];
|
|
15633
|
-
if (!
|
|
15634
|
-
|
|
16681
|
+
if (!validators_isPlainObject(table)) {
|
|
16682
|
+
validators_pushIssue(issues, tablePath, "must be an object");
|
|
15635
16683
|
continue;
|
|
15636
16684
|
}
|
|
15637
16685
|
for (const req of [
|
|
@@ -15639,61 +16687,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15639
16687
|
"server",
|
|
15640
16688
|
"title",
|
|
15641
16689
|
"columns"
|
|
15642
|
-
])if (void 0 === table[req])
|
|
16690
|
+
])if (void 0 === table[req]) validators_pushIssue(issues, `${tablePath}.${req}`, "is required");
|
|
15643
16691
|
if (void 0 !== table.client) {
|
|
15644
|
-
if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client))
|
|
16692
|
+
if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client)) validators_pushIssue(issues, `${tablePath}.client`, 'must be "none", "search", or "full"');
|
|
15645
16693
|
}
|
|
15646
16694
|
if (void 0 !== table.server) {
|
|
15647
|
-
if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server))
|
|
16695
|
+
if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server)) validators_pushIssue(issues, `${tablePath}.server`, 'must be "none", "search", or "full"');
|
|
15648
16696
|
}
|
|
15649
16697
|
if (void 0 !== table.title) {
|
|
15650
|
-
if ("string" != typeof table.title || 0 === table.title.length)
|
|
16698
|
+
if ("string" != typeof table.title || 0 === table.title.length) validators_pushIssue(issues, `${tablePath}.title`, "must be a non-empty string");
|
|
15651
16699
|
}
|
|
15652
16700
|
if (void 0 !== table.columns) if (Array.isArray(table.columns)) table.columns.forEach((column, index)=>{
|
|
15653
16701
|
validateDbSchemaColumn(column, `${tablePath}.columns[${index}]`, issues);
|
|
15654
16702
|
});
|
|
15655
|
-
else
|
|
16703
|
+
else validators_pushIssue(issues, `${tablePath}.columns`, "must be an array");
|
|
15656
16704
|
if (void 0 !== table.indexes) if (Array.isArray(table.indexes)) table.indexes.forEach((indexEntry, index)=>{
|
|
15657
16705
|
validateDbTableIndex(indexEntry, `${tablePath}.indexes[${index}]`, issues);
|
|
15658
16706
|
});
|
|
15659
|
-
else
|
|
16707
|
+
else validators_pushIssue(issues, `${tablePath}.indexes`, "must be an array");
|
|
15660
16708
|
}
|
|
15661
16709
|
return issues;
|
|
15662
16710
|
}
|
|
15663
16711
|
function validateDbTableIndex(value, path, issues) {
|
|
15664
|
-
if (!
|
|
15665
|
-
if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c))
|
|
15666
|
-
if (void 0 === value.type)
|
|
15667
|
-
else if ("string" != typeof value.type || !DB_TABLE_INDEX_TYPES.has(value.type))
|
|
16712
|
+
if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
|
|
16713
|
+
if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c)) validators_pushIssue(issues, `${path}.columns`, "must be an array of strings");
|
|
16714
|
+
if (void 0 === value.type) validators_pushIssue(issues, `${path}.type`, "is required");
|
|
16715
|
+
else if ("string" != typeof value.type || !DB_TABLE_INDEX_TYPES.has(value.type)) validators_pushIssue(issues, `${path}.type`, 'must be "b_tree", "trigram", or "full_text"');
|
|
15668
16716
|
}
|
|
15669
16717
|
function validateDbSchemaColumn(value, path, issues) {
|
|
15670
|
-
if (!
|
|
15671
|
-
if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length)
|
|
15672
|
-
else if (!
|
|
15673
|
-
if (void 0 === value.type) return void
|
|
15674
|
-
if ("string" != typeof value.type) return void
|
|
16718
|
+
if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
|
|
16719
|
+
if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) validators_pushIssue(issues, `${path}.name`, "must be a non-empty string");
|
|
16720
|
+
else if (!validators_IDENTIFIER_RE.test(value.name)) validators_pushIssue(issues, `${path}.name`, "must be a valid Python identifier");
|
|
16721
|
+
if (void 0 === value.type) return void validators_pushIssue(issues, `${path}.type`, "is required");
|
|
16722
|
+
if ("string" != typeof value.type) return void validators_pushIssue(issues, `${path}.type`, "must be a string");
|
|
15675
16723
|
const colType = value.type;
|
|
15676
16724
|
if (DB_LINK_COLUMN_TYPES.has(colType)) {
|
|
15677
|
-
if (void 0 === value.target || "string" != typeof value.target || 0 === value.target.length)
|
|
15678
|
-
else if (!
|
|
15679
|
-
} else if (!DB_BASIC_COLUMN_TYPES.has(colType))
|
|
16725
|
+
if (void 0 === value.target || "string" != typeof value.target || 0 === value.target.length) validators_pushIssue(issues, `${path}.target`, "must be a non-empty string (target table python name)");
|
|
16726
|
+
else if (!validators_IDENTIFIER_RE.test(value.target)) validators_pushIssue(issues, `${path}.target`, "must be a valid Python identifier");
|
|
16727
|
+
} else if (!DB_BASIC_COLUMN_TYPES.has(colType)) validators_pushIssue(issues, `${path}.type`, `must be a basic column type (${[
|
|
15680
16728
|
...DB_BASIC_COLUMN_TYPES
|
|
15681
16729
|
].join(", ")}) or ${[
|
|
15682
16730
|
...DB_LINK_COLUMN_TYPES
|
|
15683
16731
|
].join(", ")}`);
|
|
15684
|
-
if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden)
|
|
15685
|
-
if (void 0 !== value.admin_ui && null !== value.admin_ui) if (
|
|
16732
|
+
if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden) validators_pushIssue(issues, `${path}.client_hidden`, "must be a boolean");
|
|
16733
|
+
if (void 0 !== value.admin_ui && null !== value.admin_ui) if (validators_isPlainObject(value.admin_ui)) {
|
|
15686
16734
|
const ui = value.admin_ui;
|
|
15687
|
-
if (void 0 !== ui.width && "number" != typeof ui.width)
|
|
15688
|
-
if (void 0 !== ui.order && "number" != typeof ui.order)
|
|
15689
|
-
} else
|
|
16735
|
+
if (void 0 !== ui.width && "number" != typeof ui.width) validators_pushIssue(issues, `${path}.admin_ui.width`, "must be a number");
|
|
16736
|
+
if (void 0 !== ui.order && "number" != typeof ui.order) validators_pushIssue(issues, `${path}.admin_ui.order`, "must be a number");
|
|
16737
|
+
} else validators_pushIssue(issues, `${path}.admin_ui`, "must be an object or null");
|
|
15690
16738
|
if (void 0 !== value.indexes) {
|
|
15691
|
-
if (!Array.isArray(value.indexes)) return void
|
|
16739
|
+
if (!Array.isArray(value.indexes)) return void validators_pushIssue(issues, `${path}.indexes`, "must be an array");
|
|
15692
16740
|
value.indexes.forEach((entry, i)=>{
|
|
15693
16741
|
const idxPath = `${path}.indexes[${i}]`;
|
|
15694
|
-
if (!
|
|
15695
|
-
if (void 0 === entry.type)
|
|
15696
|
-
else if ("string" != typeof entry.type || !DB_TABLE_INDEX_TYPES.has(entry.type))
|
|
16742
|
+
if (!validators_isPlainObject(entry)) return void validators_pushIssue(issues, idxPath, "must be an object");
|
|
16743
|
+
if (void 0 === entry.type) validators_pushIssue(issues, `${idxPath}.type`, "is required");
|
|
16744
|
+
else if ("string" != typeof entry.type || !DB_TABLE_INDEX_TYPES.has(entry.type)) validators_pushIssue(issues, `${idxPath}.type`, 'must be "b_tree", "trigram", or "full_text"');
|
|
15697
16745
|
});
|
|
15698
16746
|
}
|
|
15699
16747
|
}
|
|
@@ -15702,8 +16750,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15702
16750
|
if (void 0 === services) return issues;
|
|
15703
16751
|
if (!Array.isArray(services)) return issues;
|
|
15704
16752
|
services.forEach((service, index)=>{
|
|
15705
|
-
if (!
|
|
15706
|
-
if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source))
|
|
16753
|
+
if (!validators_isPlainObject(service) || "string" != typeof service.source) return;
|
|
16754
|
+
if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source)) validators_pushIssue(issues, `${path}[${index}].source`, "must be a known runtime service source path");
|
|
15707
16755
|
});
|
|
15708
16756
|
return issues;
|
|
15709
16757
|
}
|
|
@@ -15711,9 +16759,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15711
16759
|
const issues = [];
|
|
15712
16760
|
if (void 0 === services || !Array.isArray(services)) return issues;
|
|
15713
16761
|
services.forEach((service, index)=>{
|
|
15714
|
-
if (!
|
|
15715
|
-
if (!
|
|
15716
|
-
if (true === service.client_config.allow_remember_me && void 0 === service.client_config.remember_me_days)
|
|
16762
|
+
if (!validators_isPlainObject(service) || "/runtime/services/anvil/users.yml" !== service.source) return;
|
|
16763
|
+
if (!validators_isPlainObject(service.client_config)) return;
|
|
16764
|
+
if (true === service.client_config.allow_remember_me && void 0 === service.client_config.remember_me_days) validators_pushIssue(issues, `${path}[${index}].client_config.remember_me_days`, "must be set if allow_remember_me is true");
|
|
15717
16765
|
});
|
|
15718
16766
|
return issues;
|
|
15719
16767
|
}
|
|
@@ -15721,25 +16769,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15721
16769
|
const issues = [];
|
|
15722
16770
|
if (void 0 === value) return issues;
|
|
15723
16771
|
if (!Array.isArray(value)) {
|
|
15724
|
-
|
|
16772
|
+
validators_pushIssue(issues, path, "must be an array");
|
|
15725
16773
|
return issues;
|
|
15726
16774
|
}
|
|
15727
16775
|
value.forEach((task, index)=>{
|
|
15728
16776
|
const taskPath = `${path}[${index}]`;
|
|
15729
|
-
if (!
|
|
15730
|
-
if ("string" != typeof task.job_id || 0 === task.job_id.length)
|
|
15731
|
-
|
|
15732
|
-
if (!
|
|
16777
|
+
if (!validators_isPlainObject(task)) return void validators_pushIssue(issues, taskPath, "must be an object");
|
|
16778
|
+
if ("string" != typeof task.job_id || 0 === task.job_id.length) validators_pushIssue(issues, `${taskPath}.job_id`, "must be a non-empty string");
|
|
16779
|
+
validators_validateIdentifier(task.task_name, `${taskPath}.task_name`, issues);
|
|
16780
|
+
if (!validators_isPlainObject(task.time_spec)) return void validators_pushIssue(issues, `${taskPath}.time_spec`, "must be an object");
|
|
15733
16781
|
const timeSpec = task.time_spec;
|
|
15734
|
-
if ("string" != typeof timeSpec.every || !SCHEDULE_EVERY_VALUES.has(timeSpec.every))
|
|
16782
|
+
if ("string" != typeof timeSpec.every || !SCHEDULE_EVERY_VALUES.has(timeSpec.every)) validators_pushIssue(issues, `${taskPath}.time_spec.every`, 'must be one of "minute", "hour", "day", "week", or "month"');
|
|
15735
16783
|
const interval = timeSpec.n;
|
|
15736
|
-
if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0))
|
|
15737
|
-
if (void 0 !== timeSpec.at) if (
|
|
16784
|
+
if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0)) validators_pushIssue(issues, `${taskPath}.time_spec.n`, "must be a positive integer");
|
|
16785
|
+
if (void 0 !== timeSpec.at) if (validators_isPlainObject(timeSpec.at)) {
|
|
15738
16786
|
const at = timeSpec.at;
|
|
15739
|
-
if (void 0 !== at.minute && !Number.isInteger(at.minute))
|
|
15740
|
-
if (void 0 !== at.hour && !Number.isInteger(at.hour))
|
|
15741
|
-
if (void 0 !== at.day && !Number.isInteger(at.day))
|
|
15742
|
-
} else
|
|
16787
|
+
if (void 0 !== at.minute && !Number.isInteger(at.minute)) validators_pushIssue(issues, `${taskPath}.time_spec.at.minute`, "must be an integer");
|
|
16788
|
+
if (void 0 !== at.hour && !Number.isInteger(at.hour)) validators_pushIssue(issues, `${taskPath}.time_spec.at.hour`, "must be an integer");
|
|
16789
|
+
if (void 0 !== at.day && !Number.isInteger(at.day)) validators_pushIssue(issues, `${taskPath}.time_spec.at.day`, "must be an integer");
|
|
16790
|
+
} else validators_pushIssue(issues, `${taskPath}.time_spec.at`, "must be an object");
|
|
15743
16791
|
});
|
|
15744
16792
|
return issues;
|
|
15745
16793
|
}
|
|
@@ -15751,13 +16799,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15751
16799
|
dependencies: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(external_valibot_namespaceObject.any())),
|
|
15752
16800
|
runtime_options: RuntimeOptionsSchema,
|
|
15753
16801
|
services: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(AnvilServiceEntrySchema)),
|
|
15754
|
-
startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.looseObject({
|
|
16802
|
+
startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.looseObject({
|
|
15755
16803
|
type: external_valibot_namespaceObject.picklist([
|
|
15756
16804
|
"module",
|
|
15757
16805
|
"form"
|
|
15758
16806
|
]),
|
|
15759
16807
|
module: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.string())
|
|
15760
|
-
})),
|
|
16808
|
+
}))),
|
|
15761
16809
|
startup_form: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.string())),
|
|
15762
16810
|
db_schema: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
|
|
15763
16811
|
scheduled_tasks: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
|
|
@@ -15848,6 +16896,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15848
16896
|
const startIndex = normalizedPath.lastIndexOf(clientCodePrefix);
|
|
15849
16897
|
const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
|
|
15850
16898
|
if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
|
|
16899
|
+
if (relativeToClientCode.endsWith(".html")) return "form_template.html";
|
|
16900
|
+
if (relativeToClientCode.endsWith(".py")) return "python";
|
|
16901
|
+
}
|
|
16902
|
+
const serverCodePrefix = "server_code/";
|
|
16903
|
+
if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
|
|
16904
|
+
const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
|
|
16905
|
+
const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
|
|
16906
|
+
if (relativeToServerCode.endsWith(".py")) return "python";
|
|
15851
16907
|
}
|
|
15852
16908
|
return null;
|
|
15853
16909
|
}
|
|
@@ -15867,7 +16923,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15867
16923
|
currentDir = parentDir;
|
|
15868
16924
|
}
|
|
15869
16925
|
}
|
|
15870
|
-
function validatePath(filePath,
|
|
16926
|
+
function validatePath(filePath, fileContent) {
|
|
15871
16927
|
const target = inferValidationTarget(filePath);
|
|
15872
16928
|
const existingPath = resolveExistingPath(filePath);
|
|
15873
16929
|
const appRoot = findAnvilAppRoot(filePath);
|
|
@@ -15894,7 +16950,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15894
16950
|
}
|
|
15895
16951
|
]
|
|
15896
16952
|
};
|
|
15897
|
-
const result = validateAnvilYaml(
|
|
16953
|
+
const result = validateAnvilYaml(fileContent);
|
|
15898
16954
|
return result.ok ? {
|
|
15899
16955
|
ok: true,
|
|
15900
16956
|
target
|
|
@@ -15920,7 +16976,62 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15920
16976
|
]
|
|
15921
16977
|
};
|
|
15922
16978
|
}
|
|
15923
|
-
const result = validateFormTemplate(
|
|
16979
|
+
const result = validateFormTemplate(fileContent);
|
|
16980
|
+
return result.ok ? {
|
|
16981
|
+
ok: true,
|
|
16982
|
+
target
|
|
16983
|
+
} : {
|
|
16984
|
+
...result,
|
|
16985
|
+
target
|
|
16986
|
+
};
|
|
16987
|
+
}
|
|
16988
|
+
if ("form_template.html" === target) {
|
|
16989
|
+
if (existingPath && appRoot) {
|
|
16990
|
+
const clientCodeRoot = external_path_default().join(appRoot, "client_code");
|
|
16991
|
+
const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
|
|
16992
|
+
const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
|
|
16993
|
+
if (!isInsideClientCode || !existingPath.endsWith(".html")) return {
|
|
16994
|
+
ok: false,
|
|
16995
|
+
target,
|
|
16996
|
+
message: "File is not a client_code HTML form",
|
|
16997
|
+
issues: [
|
|
16998
|
+
{
|
|
16999
|
+
path: "",
|
|
17000
|
+
message: `expected a .html file under ${clientCodeRoot}`
|
|
17001
|
+
}
|
|
17002
|
+
]
|
|
17003
|
+
};
|
|
17004
|
+
}
|
|
17005
|
+
const result = validateFormTemplateHtml(fileContent);
|
|
17006
|
+
return result.ok ? {
|
|
17007
|
+
ok: true,
|
|
17008
|
+
target
|
|
17009
|
+
} : {
|
|
17010
|
+
...result,
|
|
17011
|
+
target
|
|
17012
|
+
};
|
|
17013
|
+
}
|
|
17014
|
+
if ("python" === target) {
|
|
17015
|
+
if (existingPath && appRoot) {
|
|
17016
|
+
const clientCodeRoot = external_path_default().join(appRoot, "client_code");
|
|
17017
|
+
const serverCodeRoot = external_path_default().join(appRoot, "server_code");
|
|
17018
|
+
const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
|
|
17019
|
+
const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
|
|
17020
|
+
const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
|
|
17021
|
+
const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
|
|
17022
|
+
if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
|
|
17023
|
+
ok: false,
|
|
17024
|
+
target,
|
|
17025
|
+
message: "File is not an Anvil app Python file",
|
|
17026
|
+
issues: [
|
|
17027
|
+
{
|
|
17028
|
+
path: "",
|
|
17029
|
+
message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
|
|
17030
|
+
}
|
|
17031
|
+
]
|
|
17032
|
+
};
|
|
17033
|
+
}
|
|
17034
|
+
const result = validatePython(filePath, fileContent, appRoot);
|
|
15924
17035
|
return result.ok ? {
|
|
15925
17036
|
ok: true,
|
|
15926
17037
|
target
|
|
@@ -15936,7 +17047,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15936
17047
|
issues: [
|
|
15937
17048
|
{
|
|
15938
17049
|
path: "",
|
|
15939
|
-
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml,
|
|
17050
|
+
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
|
|
15940
17051
|
}
|
|
15941
17052
|
]
|
|
15942
17053
|
};
|
|
@@ -15975,9 +17086,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15975
17086
|
};
|
|
15976
17087
|
return null;
|
|
15977
17088
|
}
|
|
17089
|
+
function formatValidationFailure(fileName, validationResult) {
|
|
17090
|
+
const issueSummary = validationResult.issues.map((issue)=>` - ${formatValidationPath(issue.path)}: ${issue.message}`).join("\n");
|
|
17091
|
+
return issueSummary ? `${fileName} validation failed: ${validationResult.message}\n${issueSummary}` : `${fileName} validation failed: ${validationResult.message}`;
|
|
17092
|
+
}
|
|
15978
17093
|
async function loadFormTemplateData(relativePath, templateInfo, context) {
|
|
15979
17094
|
const { repoPath, stagedOnly = false } = context;
|
|
15980
|
-
const {
|
|
17095
|
+
const { format } = templateInfo;
|
|
15981
17096
|
try {
|
|
15982
17097
|
const content = await readFileContent(repoPath, relativePath, stagedOnly);
|
|
15983
17098
|
if ("yaml" === format) {
|
|
@@ -15990,26 +17105,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15990
17105
|
reason: issueSummary ? `${validationResult.message}. ${issueSummary}` : validationResult.message
|
|
15991
17106
|
};
|
|
15992
17107
|
}
|
|
15993
|
-
const parsed = external_js_yaml_default().load(content);
|
|
15994
|
-
const template = {
|
|
15995
|
-
...parsed,
|
|
15996
|
-
is_package: "package" === kind
|
|
15997
|
-
};
|
|
15998
|
-
return {
|
|
15999
|
-
ok: true,
|
|
16000
|
-
data: {
|
|
16001
|
-
template
|
|
16002
|
-
}
|
|
16003
|
-
};
|
|
16004
17108
|
}
|
|
16005
|
-
const { frontmatter, html } = parseHtmlWithFrontmatter(content);
|
|
16006
|
-
const template = frontmatter ?? {};
|
|
16007
17109
|
return {
|
|
16008
17110
|
ok: true,
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
}
|
|
17111
|
+
source: (0, form_template_parser_namespaceObject.parseFormTemplateSource)(content, {
|
|
17112
|
+
format
|
|
17113
|
+
})
|
|
16013
17114
|
};
|
|
16014
17115
|
} catch (error) {
|
|
16015
17116
|
return {
|
|
@@ -16716,20 +17817,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16716
17817
|
]
|
|
16717
17818
|
});
|
|
16718
17819
|
}
|
|
16719
|
-
function buildFormDataFromTemplate(template, html, options) {
|
|
16720
|
-
const { class_name: _templateClassName, code: _templateCode, is_package: _templateIsPackage, ...templateRest } = template && "object" == typeof template ? template : {};
|
|
16721
|
-
const formData = {
|
|
16722
|
-
...templateRest,
|
|
16723
|
-
class_name: options.className,
|
|
16724
|
-
code: options.code,
|
|
16725
|
-
is_package: options.isPackage
|
|
16726
|
-
};
|
|
16727
|
-
if (void 0 !== html) {
|
|
16728
|
-
formData.save_as_html = true;
|
|
16729
|
-
formData.serialized_html = html;
|
|
16730
|
-
}
|
|
16731
|
-
return formData;
|
|
16732
|
-
}
|
|
16733
17820
|
async function buildFormTemplateData(relativePath, templateInfo, context) {
|
|
16734
17821
|
const { repoPath, stagedOnly = false } = context;
|
|
16735
17822
|
const { kind } = templateInfo;
|
|
@@ -16753,10 +17840,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16753
17840
|
reason: "package" === kind ? "Form __init__.py file not found - cannot save template without code" : "Form .py file not found - cannot save template without code"
|
|
16754
17841
|
}
|
|
16755
17842
|
};
|
|
16756
|
-
const
|
|
16757
|
-
const html = loadResult.data.html;
|
|
16758
|
-
const templateObject = templateData && "object" == typeof templateData && !Array.isArray(templateData) ? templateData : {};
|
|
16759
|
-
const formData = buildFormDataFromTemplate(templateObject, html, {
|
|
17843
|
+
const formData = (0, form_template_parser_namespaceObject.buildFormTemplateSavePayload)(loadResult.source, {
|
|
16760
17844
|
className,
|
|
16761
17845
|
code: codeContent,
|
|
16762
17846
|
isPackage: "package" === kind
|
|
@@ -16941,7 +18025,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16941
18025
|
}
|
|
16942
18026
|
};
|
|
16943
18027
|
const validationResult = validateAnvilYaml(yamlContent);
|
|
16944
|
-
if (!validationResult.ok) throw new Error("anvil.yaml
|
|
18028
|
+
if (!validationResult.ok) throw new Error(formatValidationFailure("anvil.yaml", validationResult));
|
|
16945
18029
|
const changes = [];
|
|
16946
18030
|
for (const [key, value] of Object.entries(parsedYaml))if (!previousYaml || !deepEqual(previousYaml[key], value)) changes.push({
|
|
16947
18031
|
key,
|
|
@@ -17048,6 +18132,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17048
18132
|
}
|
|
17049
18133
|
}
|
|
17050
18134
|
function authDiagnostic(error) {
|
|
18135
|
+
if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
|
|
18136
|
+
code: error.type,
|
|
18137
|
+
recoverability: "requires_user_action",
|
|
18138
|
+
requiresUserAction: true,
|
|
18139
|
+
suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
|
|
18140
|
+
};
|
|
17051
18141
|
return {
|
|
17052
18142
|
code: error.type,
|
|
17053
18143
|
recoverability: "requires_user_action",
|
|
@@ -17110,6 +18200,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17110
18200
|
class SaveProcessor extends Emitter {
|
|
17111
18201
|
hasPendingChanges = false;
|
|
17112
18202
|
debounceTimer = null;
|
|
18203
|
+
saveLoopPromise = null;
|
|
17113
18204
|
saveSequence = 0;
|
|
17114
18205
|
SAVE_DEBOUNCE_MS = 1000;
|
|
17115
18206
|
MAX_RETRIES = 3;
|
|
@@ -17133,6 +18224,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17133
18224
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
17134
18225
|
if (isFirstChange) logger_logger.progress("sync", "Change detected");
|
|
17135
18226
|
this.debounceTimer = setTimeout(()=>{
|
|
18227
|
+
this.debounceTimer = null;
|
|
17136
18228
|
this.processSaveBatchWithEvents().catch((error)=>{
|
|
17137
18229
|
logger_logger.error(`Save failed: ${errors_getErrorMessage(error)}`);
|
|
17138
18230
|
});
|
|
@@ -17157,6 +18249,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17157
18249
|
}
|
|
17158
18250
|
}
|
|
17159
18251
|
async processSaveBatchWithEvents() {
|
|
18252
|
+
if (this.saveLoopPromise) return this.saveLoopPromise;
|
|
18253
|
+
this.saveLoopPromise = this.drainSaveQueue();
|
|
18254
|
+
try {
|
|
18255
|
+
await this.saveLoopPromise;
|
|
18256
|
+
} finally{
|
|
18257
|
+
this.saveLoopPromise = null;
|
|
18258
|
+
}
|
|
18259
|
+
}
|
|
18260
|
+
async drainSaveQueue() {
|
|
18261
|
+
while(this.hasPendingChanges && !this.config.isCleanedUp() && !this.config.isPaused())await this.processOneSaveBatchWithEvents();
|
|
18262
|
+
}
|
|
18263
|
+
async processOneSaveBatchWithEvents() {
|
|
17160
18264
|
if (this.config.isCleanedUp()) return;
|
|
17161
18265
|
logger_logger.progressUpdate("sync", "Processing");
|
|
17162
18266
|
if (!this.hasPendingChanges) return void logger_logger.progressEnd("sync");
|
|
@@ -17174,6 +18278,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17174
18278
|
newCommitId: this.config.getCommitId(),
|
|
17175
18279
|
fileCount
|
|
17176
18280
|
});
|
|
18281
|
+
if (result.anvilYamlDependenciesChanged) this.emit("anvil-yaml-dependencies-changed", void 0);
|
|
17177
18282
|
this.emit("save-complete", {
|
|
17178
18283
|
newCommitId: this.config.getCommitId(),
|
|
17179
18284
|
fileCount
|
|
@@ -17264,13 +18369,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17264
18369
|
}
|
|
17265
18370
|
async getStagedChanges() {
|
|
17266
18371
|
try {
|
|
17267
|
-
return await getStagedFileChanges(this.config.gitService.getGit());
|
|
18372
|
+
return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
|
|
17268
18373
|
} catch (e) {
|
|
17269
18374
|
logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
|
|
17270
18375
|
return [];
|
|
17271
18376
|
}
|
|
17272
18377
|
}
|
|
17273
18378
|
async getUnstagedChanges(status) {
|
|
18379
|
+
const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
|
|
17274
18380
|
const renames = status.renamed;
|
|
17275
18381
|
const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
|
|
17276
18382
|
const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
|
|
@@ -17290,7 +18396,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17290
18396
|
const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
|
|
17291
18397
|
const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
|
|
17292
18398
|
return [
|
|
17293
|
-
...status.modified.map((f)=>({
|
|
18399
|
+
...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
|
|
17294
18400
|
path: f,
|
|
17295
18401
|
type: "change"
|
|
17296
18402
|
})),
|
|
@@ -17361,11 +18467,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17361
18467
|
async processBatchedChanges(changes) {
|
|
17362
18468
|
const saveArray = [];
|
|
17363
18469
|
const originalFilePaths = [];
|
|
18470
|
+
let anvilYamlDependenciesChanged = false;
|
|
17364
18471
|
const deduplicatedChanges = deduplicateFormChanges(changes);
|
|
17365
18472
|
for (const change of deduplicatedChanges){
|
|
17366
18473
|
if ("anvil.yaml" === change.path) {
|
|
17367
18474
|
const previousAnvilYaml = await this.getHeadAnvilYaml();
|
|
17368
18475
|
const anvilChanges = await getAnvilYamlChanges(this.config.repoPath, previousAnvilYaml);
|
|
18476
|
+
const previousDependencies = normalizeAnvilYamlDependenciesValue(previousAnvilYaml?.dependencies);
|
|
18477
|
+
const currentDependencies = await readAnvilYamlDependencies(this.config.repoPath);
|
|
18478
|
+
if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) anvilYamlDependenciesChanged = true;
|
|
17369
18479
|
if (0 === anvilChanges.length) {
|
|
17370
18480
|
logger_logger.verbose(external_chalk_default().gray(" Ignoring anvil.yaml: No changes detected"));
|
|
17371
18481
|
continue;
|
|
@@ -17417,7 +18527,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17417
18527
|
}
|
|
17418
18528
|
await this.sendSavesToAnvil(saveArray, originalFilePaths);
|
|
17419
18529
|
return {
|
|
17420
|
-
skipped: false
|
|
18530
|
+
skipped: false,
|
|
18531
|
+
anvilYamlDependenciesChanged
|
|
17421
18532
|
};
|
|
17422
18533
|
}
|
|
17423
18534
|
logSaveResult(change, savePath, content) {
|
|
@@ -17493,7 +18604,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17493
18604
|
await this.handleConflict(json_resp, originalFilePaths, retryCount);
|
|
17494
18605
|
return;
|
|
17495
18606
|
}
|
|
17496
|
-
if (200 === resp.status) return void await this.handleSuccess(json_resp);
|
|
18607
|
+
if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
|
|
17497
18608
|
logger_logger.error(`Unexpected response status: ${resp.status}`);
|
|
17498
18609
|
throw {
|
|
17499
18610
|
type: "server_error",
|
|
@@ -17596,13 +18707,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17596
18707
|
reason: "Unknown conflict"
|
|
17597
18708
|
};
|
|
17598
18709
|
}
|
|
17599
|
-
async handleSuccess(json_resp) {
|
|
18710
|
+
async handleSuccess(json_resp, originalFilePaths) {
|
|
17600
18711
|
const version = json_resp.version;
|
|
17601
|
-
|
|
18712
|
+
const oldCommitId = json_resp.previous_commit ?? null;
|
|
18713
|
+
const commitIdBeforePostSaveSync = this.config.getCommitId();
|
|
18714
|
+
this.emit("saved", {
|
|
18715
|
+
oldCommitId,
|
|
18716
|
+
newCommitId: version,
|
|
18717
|
+
branch: this.config.getCurrentBranch()
|
|
18718
|
+
});
|
|
17602
18719
|
if (this.postSaveCallback) {
|
|
17603
18720
|
logger_logger.progressUpdate("sync", "Syncing back");
|
|
17604
|
-
await this.postSaveCallback();
|
|
18721
|
+
await this.postSaveCallback(new Set(originalFilePaths));
|
|
17605
18722
|
}
|
|
18723
|
+
if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
|
|
17606
18724
|
}
|
|
17607
18725
|
async rerouteChanges(changes) {
|
|
17608
18726
|
logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
|
|
@@ -17656,76 +18774,114 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17656
18774
|
}
|
|
17657
18775
|
class SyncManager extends Emitter {
|
|
17658
18776
|
config;
|
|
17659
|
-
|
|
18777
|
+
syncQueue = Promise.resolve();
|
|
17660
18778
|
constructor(config){
|
|
17661
18779
|
super();
|
|
17662
18780
|
this.config = config;
|
|
17663
18781
|
}
|
|
17664
18782
|
async syncRemoteChanges(knownLocalChanges) {
|
|
17665
|
-
|
|
17666
|
-
|
|
18783
|
+
const runSync = ()=>this.performSyncRemoteChanges(knownLocalChanges);
|
|
18784
|
+
const queuedSync = this.syncQueue.then(runSync, runSync);
|
|
18785
|
+
this.syncQueue = queuedSync.catch(()=>{});
|
|
18786
|
+
return queuedSync;
|
|
18787
|
+
}
|
|
18788
|
+
async performSyncRemoteChanges(knownLocalChanges) {
|
|
18789
|
+
const oldCommitId = this.config.getCommitId();
|
|
18790
|
+
let locallyChangedFiles;
|
|
18791
|
+
let initialStatus;
|
|
18792
|
+
if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
|
|
18793
|
+
else {
|
|
18794
|
+
initialStatus = await this.config.gitService.getStatus();
|
|
18795
|
+
locallyChangedFiles = buildLocalChangesSet(initialStatus);
|
|
18796
|
+
}
|
|
18797
|
+
let syncMode;
|
|
18798
|
+
if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
|
|
18799
|
+
else {
|
|
18800
|
+
await this.fetchAndResetFromAnvil(locallyChangedFiles);
|
|
18801
|
+
syncMode = "preservation";
|
|
18802
|
+
}
|
|
18803
|
+
if ("clean-move" === syncMode) return {
|
|
18804
|
+
localOnlyChanges: [],
|
|
18805
|
+
conflicts: []
|
|
18806
|
+
};
|
|
18807
|
+
const newCommitId = await this.config.gitService.getCommitId();
|
|
18808
|
+
this.config.setCommitId(newCommitId);
|
|
18809
|
+
const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
|
|
18810
|
+
const statusAfter = await this.config.gitService.getStatus();
|
|
18811
|
+
const newChanges = extractChangesFromStatus(statusAfter);
|
|
18812
|
+
if (0 === newChanges.length) {
|
|
18813
|
+
logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
|
|
17667
18814
|
return {
|
|
17668
18815
|
localOnlyChanges: [],
|
|
17669
18816
|
conflicts: []
|
|
17670
18817
|
};
|
|
17671
18818
|
}
|
|
17672
|
-
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
18819
|
+
const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
|
|
18820
|
+
await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
|
|
18821
|
+
logConflictResolution(result);
|
|
18822
|
+
return {
|
|
18823
|
+
localOnlyChanges: result.localOnlyChanges,
|
|
18824
|
+
conflicts: result.conflicts
|
|
18825
|
+
};
|
|
18826
|
+
}
|
|
18827
|
+
async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
|
|
18828
|
+
const oldCommitId = this.config.getCommitId();
|
|
18829
|
+
await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
|
|
18830
|
+
}
|
|
18831
|
+
async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
|
|
18832
|
+
return this.withFetchedAnvilRef(async (tempRef)=>{
|
|
18833
|
+
const localHead = await this.config.gitService.getCommitId();
|
|
18834
|
+
if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
|
|
17677
18835
|
else {
|
|
17678
|
-
const
|
|
17679
|
-
|
|
18836
|
+
const statusBeforeHardReset = await this.config.gitService.getStatus();
|
|
18837
|
+
if (!this.isCleanForFastForward(statusBeforeHardReset)) {
|
|
18838
|
+
for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
|
|
18839
|
+
await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
|
|
18840
|
+
return "preservation";
|
|
18841
|
+
}
|
|
18842
|
+
await this.config.gitService.reset(tempRef, "hard");
|
|
17680
18843
|
}
|
|
17681
|
-
await this.fetchAndResetFromAnvil();
|
|
17682
18844
|
const newCommitId = await this.config.gitService.getCommitId();
|
|
17683
18845
|
this.config.setCommitId(newCommitId);
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
if (0 === newChanges.length) {
|
|
17688
|
-
logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
|
|
17689
|
-
return {
|
|
17690
|
-
localOnlyChanges: [],
|
|
17691
|
-
conflicts: []
|
|
17692
|
-
};
|
|
17693
|
-
}
|
|
17694
|
-
const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
|
|
17695
|
-
await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
|
|
17696
|
-
logConflictResolution(result);
|
|
17697
|
-
return {
|
|
17698
|
-
localOnlyChanges: result.localOnlyChanges,
|
|
17699
|
-
conflicts: result.conflicts
|
|
17700
|
-
};
|
|
17701
|
-
} finally{
|
|
17702
|
-
this.syncInProgress = false;
|
|
17703
|
-
}
|
|
18846
|
+
await this.config.editorYaml.reload();
|
|
18847
|
+
return "clean-move";
|
|
18848
|
+
});
|
|
17704
18849
|
}
|
|
17705
|
-
async
|
|
18850
|
+
async withFetchedAnvilRef(callback) {
|
|
17706
18851
|
const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
|
|
17707
18852
|
this.config.setAuthToken(validToken);
|
|
17708
18853
|
const httpUrl = anvil_api_getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
|
|
17709
18854
|
const currentBranch = this.config.getCurrentBranch();
|
|
17710
18855
|
const tempRef = `anvil-sync-temp-${Date.now()}`;
|
|
17711
|
-
|
|
17712
|
-
|
|
18856
|
+
try {
|
|
18857
|
+
await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
|
|
18858
|
+
return await callback(tempRef);
|
|
18859
|
+
} finally{
|
|
18860
|
+
await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
|
|
18861
|
+
}
|
|
18862
|
+
}
|
|
18863
|
+
async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
|
|
17713
18864
|
await this.config.gitService.reset(tempRef, "mixed");
|
|
17714
|
-
await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
|
|
17715
18865
|
const newCommitId = await this.config.gitService.getCommitId();
|
|
17716
18866
|
await this.config.gitService.checkout([
|
|
17717
18867
|
".anvil_editor.yaml",
|
|
17718
18868
|
"anvil.yaml"
|
|
17719
18869
|
]);
|
|
17720
18870
|
await this.config.editorYaml.reload();
|
|
17721
|
-
await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
|
|
18871
|
+
await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
|
|
17722
18872
|
await this.discardFormattingOnlyYamlChanges();
|
|
17723
18873
|
}
|
|
17724
|
-
|
|
18874
|
+
isCleanForFastForward(status) {
|
|
18875
|
+
return status.isClean && 0 === status.modified.length && 0 === status.notAdded.length && 0 === status.created.length && 0 === status.deleted.length && 0 === status.staged.length && 0 === status.renamed.length;
|
|
18876
|
+
}
|
|
18877
|
+
async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
|
|
17725
18878
|
try {
|
|
17726
18879
|
const status = await this.config.gitService.getStatus();
|
|
17727
18880
|
const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
|
|
17728
|
-
await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles
|
|
18881
|
+
await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles, {
|
|
18882
|
+
oldCommitId,
|
|
18883
|
+
localChangesToPreserve
|
|
18884
|
+
});
|
|
17729
18885
|
} catch (e) {
|
|
17730
18886
|
logger_logger.verbose(external_chalk_default().gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
|
|
17731
18887
|
}
|
|
@@ -17838,6 +18994,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17838
18994
|
environment;
|
|
17839
18995
|
envBranchChangeMode;
|
|
17840
18996
|
isSwitchingForEnvironment = false;
|
|
18997
|
+
environmentSwitchTargetBranch = null;
|
|
18998
|
+
dependencyWatchEnabled;
|
|
18999
|
+
isRefreshingDependencies = false;
|
|
19000
|
+
hasPendingDependencyRefresh = false;
|
|
17841
19001
|
pendingEnvironmentBranchSwitch = null;
|
|
17842
19002
|
BRANCH_CHANGE_SETTLE_MS = 2000;
|
|
17843
19003
|
LOCAL_CHANGE_POLL_MS = 2000;
|
|
@@ -17855,6 +19015,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17855
19015
|
this.stagedOnly = options.stagedOnly ?? false;
|
|
17856
19016
|
this.environment = options.environment;
|
|
17857
19017
|
this.envBranchChangeMode = options.envBranchChangeMode ?? "manual";
|
|
19018
|
+
this.dependencyWatchEnabled = options.dependencyWatch ?? true;
|
|
17858
19019
|
this.editSession = "anvil-sync-" + Math.random().toString(36).substring(2, 15);
|
|
17859
19020
|
this.gitService = new git_GitService(this.repoPath);
|
|
17860
19021
|
this.editorYaml = new EditorYamlManager(this.repoPath);
|
|
@@ -17874,6 +19035,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17874
19035
|
async initialize() {
|
|
17875
19036
|
await this.editorYaml.load();
|
|
17876
19037
|
this.initializeProcessors();
|
|
19038
|
+
if (this.dependencyWatchEnabled) await this.refreshDependencies("startup");
|
|
17877
19039
|
try {
|
|
17878
19040
|
await this.connectWebSocket();
|
|
17879
19041
|
} catch (error) {
|
|
@@ -17893,7 +19055,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17893
19055
|
getAuthToken: ()=>this.authToken,
|
|
17894
19056
|
setAuthToken: (token)=>{
|
|
17895
19057
|
this.authToken = token;
|
|
17896
|
-
this.wsClient?.updateAuthToken(token);
|
|
19058
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
17897
19059
|
},
|
|
17898
19060
|
getCurrentBranch: ()=>this.currentBranch,
|
|
17899
19061
|
getCommitId: ()=>this.commitId,
|
|
@@ -17913,7 +19075,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17913
19075
|
getAuthToken: ()=>this.authToken,
|
|
17914
19076
|
setAuthToken: (token)=>{
|
|
17915
19077
|
this.authToken = token;
|
|
17916
|
-
this.wsClient?.updateAuthToken(token);
|
|
19078
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
17917
19079
|
},
|
|
17918
19080
|
getCurrentBranch: ()=>this.currentBranch,
|
|
17919
19081
|
getCommitId: ()=>this.commitId,
|
|
@@ -17926,15 +19088,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17926
19088
|
isPaused: ()=>this.isPausedForUserInput
|
|
17927
19089
|
});
|
|
17928
19090
|
this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
|
|
17929
|
-
this.saveProcessor.setPostSaveCallback(()=>this.
|
|
19091
|
+
this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
|
|
17930
19092
|
this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
|
|
17931
19093
|
this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
|
|
17932
19094
|
this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
|
|
17933
19095
|
this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
|
|
19096
|
+
this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
|
|
19097
|
+
this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
|
|
19098
|
+
this.queueDependencyRefresh("anvil.yaml dependencies changed");
|
|
19099
|
+
});
|
|
17934
19100
|
this.saveProcessor.on("validation-failed", (data)=>this.emit("validation-failed", data));
|
|
17935
19101
|
this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
|
|
17936
19102
|
this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
|
|
17937
19103
|
}
|
|
19104
|
+
async syncAfterSave(savedFilePaths) {
|
|
19105
|
+
const status = await this.gitService.getStatus();
|
|
19106
|
+
const locallyChangedFiles = buildLocalChangesSet(status);
|
|
19107
|
+
if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
|
|
19108
|
+
return this.syncManager.syncRemoteChanges(locallyChangedFiles);
|
|
19109
|
+
}
|
|
17938
19110
|
async connectWebSocket() {
|
|
17939
19111
|
this.wsClient = new WebSocketClient({
|
|
17940
19112
|
appId: this.appId,
|
|
@@ -17943,7 +19115,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17943
19115
|
currentBranch: this.currentBranch,
|
|
17944
19116
|
editSession: this.editSession,
|
|
17945
19117
|
username: this.username,
|
|
17946
|
-
environment: this.environment
|
|
19118
|
+
environment: this.environment,
|
|
19119
|
+
dependencyWatch: this.dependencyWatchEnabled,
|
|
19120
|
+
dependencySubscriptions: this.dependencyWatchEnabled ? await this.getDependencyWatchSubscriptions() : []
|
|
17947
19121
|
});
|
|
17948
19122
|
this.wsClient.on("env-state", ({ envs })=>{
|
|
17949
19123
|
for (const env of envs)this.handleEnvironmentBinding(env, "snapshot");
|
|
@@ -17951,7 +19125,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17951
19125
|
this.wsClient.on("env-update", (env)=>{
|
|
17952
19126
|
this.handleEnvironmentBinding(env, "update");
|
|
17953
19127
|
});
|
|
17954
|
-
this.wsClient.on("repo-update", async ({ ref, commitHash, saveSeq })=>{
|
|
19128
|
+
this.wsClient.on("repo-update", async ({ appId, ref, commitHash, saveSeq })=>{
|
|
19129
|
+
if (appId !== this.appId) return void await this.handleDependencyRepoUpdate(appId, ref, commitHash);
|
|
17955
19130
|
if (saveSeq?.session === this.editSession) return void logger_logger.debug("Ignoring update from our own session");
|
|
17956
19131
|
const branchName = ref.replace(/^refs\/heads\//, "");
|
|
17957
19132
|
if (branchName !== this.currentBranch) return void logger_logger.debug(`Ignoring update to different branch: ${branchName} (current: ${this.currentBranch})`);
|
|
@@ -17973,6 +19148,55 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17973
19148
|
});
|
|
17974
19149
|
await this.wsClient.connect();
|
|
17975
19150
|
}
|
|
19151
|
+
async handleDependencyRepoUpdate(appId, ref, commitHash) {
|
|
19152
|
+
if (!this.dependencyWatchEnabled) return;
|
|
19153
|
+
const branchName = ref.replace(/^refs\/heads\//, "");
|
|
19154
|
+
const subscription = this.wsClient?.getDependencySubscriptions().find((candidate)=>candidate.appId === appId && candidate.branch === branchName);
|
|
19155
|
+
if (!subscription) return void logger_logger.debug(`Ignoring update for unwatched dependency app ${appId} on ${branchName}`);
|
|
19156
|
+
logger_logger.verbose(external_chalk_default().cyan("Dependency update detected: ") + external_chalk_default().bold(subscription.packageName ?? appId) + external_chalk_default().gray(` (${branchName}, ${commitHash.substring(0, 8)})`));
|
|
19157
|
+
await this.queueDependencyRefresh(`dependency ${appId} updated`);
|
|
19158
|
+
}
|
|
19159
|
+
async queueDependencyRefresh(reason) {
|
|
19160
|
+
if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
|
|
19161
|
+
if (this.isRefreshingDependencies) {
|
|
19162
|
+
this.hasPendingDependencyRefresh = true;
|
|
19163
|
+
return;
|
|
19164
|
+
}
|
|
19165
|
+
this.isRefreshingDependencies = true;
|
|
19166
|
+
try {
|
|
19167
|
+
do {
|
|
19168
|
+
this.hasPendingDependencyRefresh = false;
|
|
19169
|
+
await this.refreshDependencies(reason);
|
|
19170
|
+
}while (this.hasPendingDependencyRefresh && !this.isCleanedUp);
|
|
19171
|
+
} finally{
|
|
19172
|
+
this.isRefreshingDependencies = false;
|
|
19173
|
+
}
|
|
19174
|
+
}
|
|
19175
|
+
async refreshDependencies(reason) {
|
|
19176
|
+
if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
|
|
19177
|
+
logger_logger.progress("dependency-refresh", "startup" === reason ? "Fetching dependencies" : "Refreshing dependencies");
|
|
19178
|
+
try {
|
|
19179
|
+
const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
|
|
19180
|
+
const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
|
|
19181
|
+
includeDocs: true
|
|
19182
|
+
});
|
|
19183
|
+
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
19184
|
+
const nextSubscriptions = await this.getDependencyWatchSubscriptions();
|
|
19185
|
+
this.reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions);
|
|
19186
|
+
logger_logger.progressEnd("dependency-refresh", response.dependencies.length ? `Fetched ${response.dependencies.length} dependencies` : "Dependencies already up to date");
|
|
19187
|
+
} catch (error) {
|
|
19188
|
+
logger_logger.progressEnd("dependency-refresh", "Failed");
|
|
19189
|
+
logger_logger.warn(external_chalk_default().yellow(`Failed to refresh dependencies: ${errors_getErrorMessage(error)}`));
|
|
19190
|
+
}
|
|
19191
|
+
}
|
|
19192
|
+
async getDependencyWatchSubscriptions() {
|
|
19193
|
+
return getDependencyWatchSubscriptionsFromManifest(await readDepsCacheManifest(this.repoPath));
|
|
19194
|
+
}
|
|
19195
|
+
reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions) {
|
|
19196
|
+
if (dependencySubscriptionsEqual(previousSubscriptions, nextSubscriptions)) return;
|
|
19197
|
+
this.wsClient?.updateDependencySubscriptions(nextSubscriptions);
|
|
19198
|
+
if (this.wsClient?.isConnected() && previousSubscriptions.length > 0) this.wsClient.reconnect();
|
|
19199
|
+
}
|
|
17976
19200
|
async handleEnvironmentBinding(env, source) {
|
|
17977
19201
|
if (!this.environment?.envPid || env.envPid !== this.environment.envPid) return;
|
|
17978
19202
|
this.environment = {
|
|
@@ -17995,6 +19219,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17995
19219
|
async switchToEnvironmentBranch(targetBranch, source) {
|
|
17996
19220
|
if (this.isCleanedUp) return;
|
|
17997
19221
|
this.isSwitchingForEnvironment = true;
|
|
19222
|
+
this.environmentSwitchTargetBranch = targetBranch;
|
|
17998
19223
|
const oldPausedState = this.isPausedForUserInput;
|
|
17999
19224
|
try {
|
|
18000
19225
|
const status = await this.gitService.getStatus();
|
|
@@ -18019,6 +19244,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18019
19244
|
]);
|
|
18020
19245
|
}
|
|
18021
19246
|
logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
|
|
19247
|
+
this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
|
|
19248
|
+
this.wsClient?.updateAuthToken?.(this.authToken);
|
|
18022
19249
|
const fetchUrl = anvil_api_getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
|
|
18023
19250
|
const envRemoteRef = environment_environmentRemoteRefForBranch(targetBranch);
|
|
18024
19251
|
await this.gitService.fetch(fetchUrl, environment_environmentFetchRefSpecForBranch(targetBranch));
|
|
@@ -18034,6 +19261,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18034
19261
|
this.lastBranchChangeTime = Date.now();
|
|
18035
19262
|
this.fileWatcher?.setCurrentBranch(targetBranch);
|
|
18036
19263
|
this.wsClient?.updateBranch(targetBranch);
|
|
19264
|
+
this.queueDependencyRefresh("branch changed");
|
|
18037
19265
|
await this.initializeLocalChangeFingerprint();
|
|
18038
19266
|
this.emit("branch-changed", {
|
|
18039
19267
|
oldBranch,
|
|
@@ -18053,6 +19281,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18053
19281
|
} finally{
|
|
18054
19282
|
this.isPausedForUserInput = oldPausedState;
|
|
18055
19283
|
this.isSwitchingForEnvironment = false;
|
|
19284
|
+
this.environmentSwitchTargetBranch = null;
|
|
18056
19285
|
}
|
|
18057
19286
|
const pendingSwitch = this.pendingEnvironmentBranchSwitch;
|
|
18058
19287
|
this.pendingEnvironmentBranchSwitch = null;
|
|
@@ -18104,7 +19333,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18104
19333
|
});
|
|
18105
19334
|
}
|
|
18106
19335
|
async syncRemoteChanges(knownLocalChanges) {
|
|
18107
|
-
|
|
19336
|
+
const previousDependencies = this.dependencyWatchEnabled ? await readAnvilYamlDependencies(this.repoPath) : void 0;
|
|
19337
|
+
const result = await this.syncManager.syncRemoteChanges(knownLocalChanges);
|
|
19338
|
+
if (this.dependencyWatchEnabled && void 0 !== previousDependencies) {
|
|
19339
|
+
const currentDependencies = await readAnvilYamlDependencies(this.repoPath);
|
|
19340
|
+
if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) await this.queueDependencyRefresh("anvil.yaml dependencies changed");
|
|
19341
|
+
}
|
|
19342
|
+
return result;
|
|
18108
19343
|
}
|
|
18109
19344
|
cleanup() {
|
|
18110
19345
|
this.isCleanedUp = true;
|
|
@@ -18134,15 +19369,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18134
19369
|
getCurrentBranch: async ()=>this.gitService.getCurrentBranch()
|
|
18135
19370
|
});
|
|
18136
19371
|
this.fileWatcher.on("branch-change", ({ newBranch })=>{
|
|
18137
|
-
|
|
18138
|
-
logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
|
|
18139
|
-
this.currentBranch = newBranch;
|
|
18140
|
-
this.lastBranchChangeTime = Date.now();
|
|
18141
|
-
this.wsClient?.updateBranch(newBranch);
|
|
18142
|
-
this.emit("branch-changed", {
|
|
18143
|
-
oldBranch,
|
|
18144
|
-
newBranch
|
|
18145
|
-
});
|
|
19372
|
+
this.handleFileWatcherBranchChange(newBranch);
|
|
18146
19373
|
});
|
|
18147
19374
|
this.fileWatcher.on("index-change", ()=>{
|
|
18148
19375
|
this.saveProcessor?.queueSave();
|
|
@@ -18159,6 +19386,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18159
19386
|
this.startLocalChangeFallbackPolling();
|
|
18160
19387
|
await new Promise(()=>{});
|
|
18161
19388
|
}
|
|
19389
|
+
handleFileWatcherBranchChange(newBranch) {
|
|
19390
|
+
if (this.isSwitchingForEnvironment && newBranch === this.environmentSwitchTargetBranch) return void logger_logger.debug(`Ignoring FileWatcher branch-change during environment switch: ${newBranch}`);
|
|
19391
|
+
const oldBranch = this.currentBranch;
|
|
19392
|
+
logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
|
|
19393
|
+
this.currentBranch = newBranch;
|
|
19394
|
+
this.lastBranchChangeTime = Date.now();
|
|
19395
|
+
this.wsClient?.updateBranch(newBranch);
|
|
19396
|
+
this.queueDependencyRefresh("branch changed");
|
|
19397
|
+
this.emit("external-branch-changed", {
|
|
19398
|
+
oldBranch,
|
|
19399
|
+
newBranch
|
|
19400
|
+
});
|
|
19401
|
+
this.emit("branch-changed", {
|
|
19402
|
+
oldBranch,
|
|
19403
|
+
newBranch
|
|
19404
|
+
});
|
|
19405
|
+
}
|
|
18162
19406
|
startWatchingInBackground(onError) {
|
|
18163
19407
|
this.startWatching().catch((error)=>{
|
|
18164
19408
|
this.cleanup();
|
|
@@ -18284,6 +19528,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18284
19528
|
return false;
|
|
18285
19529
|
}
|
|
18286
19530
|
}
|
|
19531
|
+
function dependencySubscriptionsEqual(left, right) {
|
|
19532
|
+
return subscriptionKeys(left).join("\n") === subscriptionKeys(right).join("\n");
|
|
19533
|
+
}
|
|
19534
|
+
function subscriptionKeys(subscriptions) {
|
|
19535
|
+
return subscriptions.map((subscription)=>`${subscription.appId}\0${subscription.branch}`).sort();
|
|
19536
|
+
}
|
|
18287
19537
|
async function validation_validateAnvilApp(repoPath) {
|
|
18288
19538
|
const anvilYamlPath = external_path_default().join(repoPath, "anvil.yaml");
|
|
18289
19539
|
if (!external_fs_.existsSync(anvilYamlPath)) throw createValidationError.missingYaml(repoPath);
|
|
@@ -18501,6 +19751,367 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18501
19751
|
}
|
|
18502
19752
|
require("inquirer");
|
|
18503
19753
|
require("ora");
|
|
19754
|
+
const defaultGitAuthDoctorDeps = {
|
|
19755
|
+
getAppAuthBinding: git_auth_getAppAuthBinding,
|
|
19756
|
+
getAccountsForUrl: auth_getAccountsForUrl,
|
|
19757
|
+
hasTokensForUrl: auth_hasTokensForUrl,
|
|
19758
|
+
runCredentialFill: runGitCredentialFill
|
|
19759
|
+
};
|
|
19760
|
+
function getHighestStatus(statuses) {
|
|
19761
|
+
if (statuses.includes("error")) return "error";
|
|
19762
|
+
if (statuses.includes("warning")) return "warning";
|
|
19763
|
+
return "ok";
|
|
19764
|
+
}
|
|
19765
|
+
function finding(status, code, message) {
|
|
19766
|
+
return {
|
|
19767
|
+
status,
|
|
19768
|
+
code,
|
|
19769
|
+
message
|
|
19770
|
+
};
|
|
19771
|
+
}
|
|
19772
|
+
function redactSecrets(value) {
|
|
19773
|
+
return value.replace(/^password=.*$/gim, "password=<redacted>").replace(/(https?:\/\/git:)[^@\s]+@/g, "$1<redacted>@").replace(/(access[_-]?token=)[^&\s]+/gi, "$1<redacted>").replace(/(refresh[_-]?token=)[^&\s]+/gi, "$1<redacted>");
|
|
19774
|
+
}
|
|
19775
|
+
function splitShellCommand(command) {
|
|
19776
|
+
const tokens = [];
|
|
19777
|
+
let current = "";
|
|
19778
|
+
let state = "normal";
|
|
19779
|
+
for(let i = 0; i < command.length; i++){
|
|
19780
|
+
const ch = command[i];
|
|
19781
|
+
if ("single" === state) {
|
|
19782
|
+
if ("'" === ch) state = "normal";
|
|
19783
|
+
else current += ch;
|
|
19784
|
+
continue;
|
|
19785
|
+
}
|
|
19786
|
+
if ("double" === state) {
|
|
19787
|
+
if ("\"" === ch) state = "normal";
|
|
19788
|
+
else if ("\\" === ch && i + 1 < command.length && [
|
|
19789
|
+
"$",
|
|
19790
|
+
"`",
|
|
19791
|
+
"\"",
|
|
19792
|
+
"\\",
|
|
19793
|
+
"\n"
|
|
19794
|
+
].includes(command[i + 1])) current += command[++i];
|
|
19795
|
+
else current += ch;
|
|
19796
|
+
continue;
|
|
19797
|
+
}
|
|
19798
|
+
if (/\s/.test(ch)) {
|
|
19799
|
+
if (current) {
|
|
19800
|
+
tokens.push(current);
|
|
19801
|
+
current = "";
|
|
19802
|
+
}
|
|
19803
|
+
} else if ("'" === ch) state = "single";
|
|
19804
|
+
else if ("\"" === ch) state = "double";
|
|
19805
|
+
else if ("\\" === ch && i + 1 < command.length) current += command[++i];
|
|
19806
|
+
else current += ch;
|
|
19807
|
+
}
|
|
19808
|
+
if ("normal" !== state) throw new Error("Unterminated quote in helper command");
|
|
19809
|
+
if (current) tokens.push(current);
|
|
19810
|
+
return tokens;
|
|
19811
|
+
}
|
|
19812
|
+
function parseCredentialHelperCommand(command) {
|
|
19813
|
+
const trimmed = command.trim();
|
|
19814
|
+
if (!trimmed || !trimmed.startsWith("!")) return {};
|
|
19815
|
+
const tokens = splitShellCommand(trimmed.slice(1).trim());
|
|
19816
|
+
return {
|
|
19817
|
+
execPath: tokens[0],
|
|
19818
|
+
helperPath: tokens[1]
|
|
19819
|
+
};
|
|
19820
|
+
}
|
|
19821
|
+
function readHelperModulePath(helperPath) {
|
|
19822
|
+
try {
|
|
19823
|
+
const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
|
|
19824
|
+
const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
|
|
19825
|
+
return match?.[2];
|
|
19826
|
+
} catch {
|
|
19827
|
+
return;
|
|
19828
|
+
}
|
|
19829
|
+
}
|
|
19830
|
+
function inspectCredentialHelperCommand(raw) {
|
|
19831
|
+
const details = {
|
|
19832
|
+
raw
|
|
19833
|
+
};
|
|
19834
|
+
try {
|
|
19835
|
+
const parsed = parseCredentialHelperCommand(raw);
|
|
19836
|
+
details.execPath = parsed.execPath;
|
|
19837
|
+
details.helperPath = parsed.helperPath;
|
|
19838
|
+
} catch (e) {
|
|
19839
|
+
details.parseError = e.message;
|
|
19840
|
+
return details;
|
|
19841
|
+
}
|
|
19842
|
+
if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
|
|
19843
|
+
if (details.helperPath) {
|
|
19844
|
+
details.helperExists = external_fs_default().existsSync(details.helperPath);
|
|
19845
|
+
if (details.helperExists) {
|
|
19846
|
+
try {
|
|
19847
|
+
const stat = external_fs_default().statSync(details.helperPath);
|
|
19848
|
+
details.helperExecutable = (73 & stat.mode) !== 0;
|
|
19849
|
+
} catch {
|
|
19850
|
+
details.helperExecutable = false;
|
|
19851
|
+
}
|
|
19852
|
+
details.modulePath = readHelperModulePath(details.helperPath);
|
|
19853
|
+
if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
|
|
19854
|
+
}
|
|
19855
|
+
}
|
|
19856
|
+
return details;
|
|
19857
|
+
}
|
|
19858
|
+
function detectAnvilRemote(remote) {
|
|
19859
|
+
const fetchUrl = remote.refs.fetch;
|
|
19860
|
+
const out = {
|
|
19861
|
+
name: remote.name,
|
|
19862
|
+
fetchUrl
|
|
19863
|
+
};
|
|
19864
|
+
if (!fetchUrl) return out;
|
|
19865
|
+
const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
|
|
19866
|
+
if (httpMatch) {
|
|
19867
|
+
const [, protocol, host, appId] = httpMatch;
|
|
19868
|
+
return {
|
|
19869
|
+
...out,
|
|
19870
|
+
appId,
|
|
19871
|
+
anvilUrl: config_normalizeAnvilUrl(`${protocol}://${host}`),
|
|
19872
|
+
transport: "https"
|
|
19873
|
+
};
|
|
19874
|
+
}
|
|
19875
|
+
const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
|
|
19876
|
+
if (sshMatch) {
|
|
19877
|
+
const [, , host, appId] = sshMatch;
|
|
19878
|
+
return {
|
|
19879
|
+
...out,
|
|
19880
|
+
appId,
|
|
19881
|
+
anvilUrl: config_normalizeAnvilUrl(host),
|
|
19882
|
+
transport: "ssh"
|
|
19883
|
+
};
|
|
19884
|
+
}
|
|
19885
|
+
return out;
|
|
19886
|
+
}
|
|
19887
|
+
async function getLocalConfigValues(repoPath, key) {
|
|
19888
|
+
try {
|
|
19889
|
+
const output = await esm_default(repoPath).raw([
|
|
19890
|
+
"config",
|
|
19891
|
+
"--local",
|
|
19892
|
+
"--get-all",
|
|
19893
|
+
key
|
|
19894
|
+
]);
|
|
19895
|
+
const values = output.split(/\r?\n/);
|
|
19896
|
+
if ("" === values[values.length - 1]) values.pop();
|
|
19897
|
+
return values;
|
|
19898
|
+
} catch {
|
|
19899
|
+
return [];
|
|
19900
|
+
}
|
|
19901
|
+
}
|
|
19902
|
+
function buildCredentialInput(anvilUrl, appId) {
|
|
19903
|
+
const url = new URL(anvilUrl);
|
|
19904
|
+
return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
|
|
19905
|
+
}
|
|
19906
|
+
async function runGitCredentialFill(repoPath, input) {
|
|
19907
|
+
return new Promise((resolve)=>{
|
|
19908
|
+
const child = (0, external_child_process_namespaceObject.spawn)("git", [
|
|
19909
|
+
"credential",
|
|
19910
|
+
"fill"
|
|
19911
|
+
], {
|
|
19912
|
+
cwd: repoPath,
|
|
19913
|
+
env: {
|
|
19914
|
+
...process.env,
|
|
19915
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
19916
|
+
},
|
|
19917
|
+
stdio: [
|
|
19918
|
+
"pipe",
|
|
19919
|
+
"pipe",
|
|
19920
|
+
"pipe"
|
|
19921
|
+
]
|
|
19922
|
+
});
|
|
19923
|
+
let stdout = "";
|
|
19924
|
+
let stderr = "";
|
|
19925
|
+
child.stdout.setEncoding("utf8");
|
|
19926
|
+
child.stderr.setEncoding("utf8");
|
|
19927
|
+
child.stdout.on("data", (chunk)=>{
|
|
19928
|
+
stdout += chunk;
|
|
19929
|
+
});
|
|
19930
|
+
child.stderr.on("data", (chunk)=>{
|
|
19931
|
+
stderr += chunk;
|
|
19932
|
+
});
|
|
19933
|
+
child.on("error", (error)=>{
|
|
19934
|
+
resolve({
|
|
19935
|
+
exitCode: null,
|
|
19936
|
+
stdout: redactSecrets(stdout),
|
|
19937
|
+
stderr: redactSecrets(stderr),
|
|
19938
|
+
error: redactSecrets(error.message)
|
|
19939
|
+
});
|
|
19940
|
+
});
|
|
19941
|
+
child.on("close", (code)=>{
|
|
19942
|
+
resolve({
|
|
19943
|
+
exitCode: code,
|
|
19944
|
+
stdout: redactSecrets(stdout),
|
|
19945
|
+
stderr: redactSecrets(stderr)
|
|
19946
|
+
});
|
|
19947
|
+
});
|
|
19948
|
+
child.stdin.end(input);
|
|
19949
|
+
});
|
|
19950
|
+
}
|
|
19951
|
+
async function buildAppReport(repoPath, remote, deps) {
|
|
19952
|
+
const appId = remote.appId;
|
|
19953
|
+
const anvilUrl = remote.anvilUrl;
|
|
19954
|
+
const url = new URL(anvilUrl);
|
|
19955
|
+
const credentialScope = `${url.protocol}//${url.host}`;
|
|
19956
|
+
const helperConfigKey = `credential.${credentialScope}.helper`;
|
|
19957
|
+
const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
|
|
19958
|
+
const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
|
|
19959
|
+
const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
|
|
19960
|
+
const findings = [];
|
|
19961
|
+
if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
|
|
19962
|
+
else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
|
|
19963
|
+
for (const helper of helperDetails){
|
|
19964
|
+
if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
|
|
19965
|
+
if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
|
|
19966
|
+
if (helper.helperPath) {
|
|
19967
|
+
if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
|
|
19968
|
+
else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
|
|
19969
|
+
} else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
|
|
19970
|
+
if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
|
|
19971
|
+
if (helper.modulePath && "cli.js" === external_path_default().basename(helper.modulePath)) findings.push(finding("error", "helper_module_cli_entry", `Generated helper points at the CLI entry instead of the API entry: ${helper.modulePath}`));
|
|
19972
|
+
}
|
|
19973
|
+
const binding = await deps.getAppAuthBinding(repoPath, appId);
|
|
19974
|
+
if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
|
|
19975
|
+
if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
|
|
19976
|
+
const authUrl = binding.url ? config_normalizeAnvilUrl(binding.url) : anvilUrl;
|
|
19977
|
+
let accounts = [];
|
|
19978
|
+
let authUsername = binding.username;
|
|
19979
|
+
let hasUsableAuth = false;
|
|
19980
|
+
try {
|
|
19981
|
+
accounts = await deps.getAccountsForUrl(authUrl);
|
|
19982
|
+
if (authUsername || 1 !== accounts.length) {
|
|
19983
|
+
if (!authUsername && accounts.length > 1) findings.push(finding("error", "multiple_accounts_without_binding", `Multiple accounts are logged in for ${authUrl}; bind one with anvil checkout --user.`));
|
|
19984
|
+
} else authUsername = accounts[0];
|
|
19985
|
+
hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
|
|
19986
|
+
if (!hasUsableAuth) findings.push(finding("error", "missing_usable_auth", authUsername ? `No usable auth tokens found for ${authUrl} as ${authUsername}.` : `No usable auth tokens found for ${authUrl}.`));
|
|
19987
|
+
} catch (e) {
|
|
19988
|
+
findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
|
|
19989
|
+
}
|
|
19990
|
+
const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
|
|
19991
|
+
const credentialSimulation = {
|
|
19992
|
+
status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
|
|
19993
|
+
exitCode: fillResult.exitCode,
|
|
19994
|
+
stdout: redactSecrets(fillResult.stdout),
|
|
19995
|
+
stderr: redactSecrets(fillResult.stderr),
|
|
19996
|
+
error: fillResult.error ? redactSecrets(fillResult.error) : void 0
|
|
19997
|
+
};
|
|
19998
|
+
if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
|
|
19999
|
+
const report = {
|
|
20000
|
+
appId,
|
|
20001
|
+
anvilUrl,
|
|
20002
|
+
remoteName: remote.name,
|
|
20003
|
+
remoteUrl: remote.fetchUrl,
|
|
20004
|
+
credentialScope,
|
|
20005
|
+
helperConfigKey,
|
|
20006
|
+
helperValues,
|
|
20007
|
+
helperDetails,
|
|
20008
|
+
binding: {
|
|
20009
|
+
url: binding.url,
|
|
20010
|
+
username: binding.username,
|
|
20011
|
+
hasUrl: !!binding.url,
|
|
20012
|
+
hasUsername: !!binding.username
|
|
20013
|
+
},
|
|
20014
|
+
auth: {
|
|
20015
|
+
url: authUrl,
|
|
20016
|
+
username: authUsername,
|
|
20017
|
+
accounts,
|
|
20018
|
+
hasUsableAuth
|
|
20019
|
+
},
|
|
20020
|
+
credentialSimulation,
|
|
20021
|
+
findings
|
|
20022
|
+
};
|
|
20023
|
+
return report;
|
|
20024
|
+
}
|
|
20025
|
+
async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
|
|
20026
|
+
const deps = options.deps ?? defaultGitAuthDoctorDeps;
|
|
20027
|
+
const inputPath = external_path_default().resolve(repoPathInput);
|
|
20028
|
+
const git = esm_default(inputPath);
|
|
20029
|
+
const repoPath = (await git.raw([
|
|
20030
|
+
"rev-parse",
|
|
20031
|
+
"--show-toplevel"
|
|
20032
|
+
])).trim();
|
|
20033
|
+
const rawGitDir = (await git.raw([
|
|
20034
|
+
"rev-parse",
|
|
20035
|
+
"--git-dir"
|
|
20036
|
+
])).trim();
|
|
20037
|
+
const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
|
|
20038
|
+
auth_setRepoContext(repoPath);
|
|
20039
|
+
const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
|
|
20040
|
+
const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
|
|
20041
|
+
const findings = [];
|
|
20042
|
+
if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
|
|
20043
|
+
const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
|
|
20044
|
+
const status = getHighestStatus([
|
|
20045
|
+
...findings.map((item)=>item.status),
|
|
20046
|
+
...apps.flatMap((app)=>app.findings.map((item)=>item.status))
|
|
20047
|
+
]);
|
|
20048
|
+
return {
|
|
20049
|
+
repoPath,
|
|
20050
|
+
gitDir,
|
|
20051
|
+
remotes,
|
|
20052
|
+
apps,
|
|
20053
|
+
findings,
|
|
20054
|
+
status
|
|
20055
|
+
};
|
|
20056
|
+
}
|
|
20057
|
+
function formatStatus(status) {
|
|
20058
|
+
if ("ok" === status) return external_chalk_default().green("[ok]");
|
|
20059
|
+
if ("warning" === status) return external_chalk_default().yellow("[warn]");
|
|
20060
|
+
return external_chalk_default().red("[error]");
|
|
20061
|
+
}
|
|
20062
|
+
function formatGitAuthDoctorReport(report) {
|
|
20063
|
+
const lines = [];
|
|
20064
|
+
lines.push(external_chalk_default().bold("Git auth doctor"));
|
|
20065
|
+
lines.push(`Repository: ${report.repoPath}`);
|
|
20066
|
+
lines.push(`Git dir: ${report.gitDir}`);
|
|
20067
|
+
lines.push(`Overall: ${formatStatus(report.status)}`);
|
|
20068
|
+
lines.push("");
|
|
20069
|
+
lines.push(external_chalk_default().bold("Detected remotes:"));
|
|
20070
|
+
if (0 === report.remotes.length) lines.push(" (none)");
|
|
20071
|
+
else for (const remote of report.remotes){
|
|
20072
|
+
const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
|
|
20073
|
+
lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
|
|
20074
|
+
}
|
|
20075
|
+
for (const app of report.apps){
|
|
20076
|
+
lines.push("");
|
|
20077
|
+
lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
|
|
20078
|
+
lines.push(` URL: ${app.anvilUrl}`);
|
|
20079
|
+
lines.push(` Credential scope: ${app.credentialScope}`);
|
|
20080
|
+
lines.push(` Helper config: ${app.helperConfigKey}`);
|
|
20081
|
+
if (0 === app.helperValues.length) lines.push(" (none)");
|
|
20082
|
+
else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
|
|
20083
|
+
for (const helper of app.helperDetails){
|
|
20084
|
+
lines.push(` Helper command: ${helper.raw}`);
|
|
20085
|
+
lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
|
|
20086
|
+
lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
|
|
20087
|
+
if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
|
|
20088
|
+
}
|
|
20089
|
+
lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
|
|
20090
|
+
lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
|
|
20091
|
+
lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
|
|
20092
|
+
if (app.credentialSimulation) {
|
|
20093
|
+
lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
|
|
20094
|
+
if (app.credentialSimulation.stdout.trim()) {
|
|
20095
|
+
lines.push(" stdout:");
|
|
20096
|
+
for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
|
|
20097
|
+
}
|
|
20098
|
+
if (app.credentialSimulation.stderr.trim()) {
|
|
20099
|
+
lines.push(" stderr:");
|
|
20100
|
+
for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
|
|
20101
|
+
}
|
|
20102
|
+
}
|
|
20103
|
+
if (app.findings.length > 0) {
|
|
20104
|
+
lines.push(" Findings:");
|
|
20105
|
+
for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
|
|
20106
|
+
}
|
|
20107
|
+
}
|
|
20108
|
+
if (report.findings.length > 0) {
|
|
20109
|
+
lines.push("");
|
|
20110
|
+
lines.push(external_chalk_default().bold("Repository findings:"));
|
|
20111
|
+
for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
|
|
20112
|
+
}
|
|
20113
|
+
return lines.join("\n");
|
|
20114
|
+
}
|
|
18504
20115
|
const gitCredential_defaultDeps = {
|
|
18505
20116
|
getValidAuthToken: auth_getValidAuthToken,
|
|
18506
20117
|
getAccountsForUrl: auth_getAccountsForUrl,
|
|
@@ -18803,7 +20414,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18803
20414
|
authToken,
|
|
18804
20415
|
currentBranch,
|
|
18805
20416
|
commitId,
|
|
18806
|
-
username
|
|
20417
|
+
username,
|
|
20418
|
+
dependencyWatch: false
|
|
18807
20419
|
});
|
|
18808
20420
|
await session.initialize();
|
|
18809
20421
|
await session.syncRemoteChanges();
|
|
@@ -18900,7 +20512,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18900
20512
|
stagedOnly,
|
|
18901
20513
|
username,
|
|
18902
20514
|
environment,
|
|
18903
|
-
envBranchChangeMode: options.envBranchChangeMode ?? "manual"
|
|
20515
|
+
envBranchChangeMode: options.envBranchChangeMode ?? "manual",
|
|
20516
|
+
dependencyWatch: options.dependencyWatch ?? true
|
|
18904
20517
|
});
|
|
18905
20518
|
session.on("branch-changed", (data)=>{
|
|
18906
20519
|
logger_logger.debug("Event: branch-changed", data);
|
|
@@ -18956,6 +20569,7 @@ exports.ValidationIssue = __webpack_exports__.ValidationIssue;
|
|
|
18956
20569
|
exports.ValidationTarget = __webpack_exports__.ValidationTarget;
|
|
18957
20570
|
exports.WatchSession = __webpack_exports__.WatchSession;
|
|
18958
20571
|
exports.branchFromEnvironmentVersion = __webpack_exports__.branchFromEnvironmentVersion;
|
|
20572
|
+
exports.buildGitAuthDoctorReport = __webpack_exports__.buildGitAuthDoctorReport;
|
|
18959
20573
|
exports.checkUncommittedChanges = __webpack_exports__.checkUncommittedChanges;
|
|
18960
20574
|
exports.clearInMemoryTokens = __webpack_exports__.clearInMemoryTokens;
|
|
18961
20575
|
exports.createWatchDiagnostic = __webpack_exports__.createWatchDiagnostic;
|
|
@@ -18967,6 +20581,7 @@ exports.executeCheckout = __webpack_exports__.executeCheckout;
|
|
|
18967
20581
|
exports.executeGitCredentialOperation = __webpack_exports__.executeGitCredentialOperation;
|
|
18968
20582
|
exports.filterCandidates = __webpack_exports__.filterCandidates;
|
|
18969
20583
|
exports.formatCandidateLabel = __webpack_exports__.formatCandidateLabel;
|
|
20584
|
+
exports.formatGitAuthDoctorReport = __webpack_exports__.formatGitAuthDoctorReport;
|
|
18970
20585
|
exports.formatValidationPath = __webpack_exports__.formatValidationPath;
|
|
18971
20586
|
exports.getAccountsForUrl = __webpack_exports__.getAccountsForUrl;
|
|
18972
20587
|
exports.getAllConfig = __webpack_exports__.getAllConfig;
|
|
@@ -18982,6 +20597,7 @@ exports.getSettableConfigKeys = __webpack_exports__.getSettableConfigKeys;
|
|
|
18982
20597
|
exports.getValidAuthToken = __webpack_exports__.getValidAuthToken;
|
|
18983
20598
|
exports.getWebSocketUrl = __webpack_exports__.getWebSocketUrl;
|
|
18984
20599
|
exports.hasTokensForUrl = __webpack_exports__.hasTokensForUrl;
|
|
20600
|
+
exports.inspectCredentialHelperCommand = __webpack_exports__.inspectCredentialHelperCommand;
|
|
18985
20601
|
exports.isCommandAvailable = __webpack_exports__.isCommandAvailable;
|
|
18986
20602
|
exports.isDirectoryNonEmpty = __webpack_exports__.isDirectoryNonEmpty;
|
|
18987
20603
|
exports.isPathInsideGitRepo = __webpack_exports__.isPathInsideGitRepo;
|
|
@@ -18992,6 +20608,7 @@ exports.lookupRemoteInfoForAppId = __webpack_exports__.lookupRemoteInfoForAppId;
|
|
|
18992
20608
|
exports.normalizeAnvilUrl = __webpack_exports__.normalizeAnvilUrl;
|
|
18993
20609
|
exports.parseCheckoutInput = __webpack_exports__.parseCheckoutInput;
|
|
18994
20610
|
exports.parseConfigSetValue = __webpack_exports__.parseConfigSetValue;
|
|
20611
|
+
exports.parseCredentialHelperCommand = __webpack_exports__.parseCredentialHelperCommand;
|
|
18995
20612
|
exports.pollDeviceAuthorization = __webpack_exports__.pollDeviceAuthorization;
|
|
18996
20613
|
exports.preferredEditors = __webpack_exports__.preferredEditors;
|
|
18997
20614
|
exports.readEnvironmentPid = __webpack_exports__.readEnvironmentPid;
|
|
@@ -19011,6 +20628,7 @@ exports.validateAppId = __webpack_exports__.validateAppId;
|
|
|
19011
20628
|
exports.validateBranchSyncStatus = __webpack_exports__.validateBranchSyncStatus;
|
|
19012
20629
|
exports.validateCheckoutDestination = __webpack_exports__.validateCheckoutDestination;
|
|
19013
20630
|
exports.validateFormTemplate = __webpack_exports__.validateFormTemplate;
|
|
20631
|
+
exports.validateFormTemplateHtml = __webpack_exports__.validateFormTemplateHtml;
|
|
19014
20632
|
exports.validatePath = __webpack_exports__.validatePath;
|
|
19015
20633
|
exports.verifyAndStoreTokens = __webpack_exports__.verifyAndStoreTokens;
|
|
19016
20634
|
exports.verifyAuth = __webpack_exports__.verifyAuth;
|
|
@@ -19043,6 +20661,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
19043
20661
|
"ValidationTarget",
|
|
19044
20662
|
"WatchSession",
|
|
19045
20663
|
"branchFromEnvironmentVersion",
|
|
20664
|
+
"buildGitAuthDoctorReport",
|
|
19046
20665
|
"checkUncommittedChanges",
|
|
19047
20666
|
"clearInMemoryTokens",
|
|
19048
20667
|
"createWatchDiagnostic",
|
|
@@ -19054,6 +20673,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
19054
20673
|
"executeGitCredentialOperation",
|
|
19055
20674
|
"filterCandidates",
|
|
19056
20675
|
"formatCandidateLabel",
|
|
20676
|
+
"formatGitAuthDoctorReport",
|
|
19057
20677
|
"formatValidationPath",
|
|
19058
20678
|
"getAccountsForUrl",
|
|
19059
20679
|
"getAllConfig",
|
|
@@ -19069,6 +20689,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
19069
20689
|
"getValidAuthToken",
|
|
19070
20690
|
"getWebSocketUrl",
|
|
19071
20691
|
"hasTokensForUrl",
|
|
20692
|
+
"inspectCredentialHelperCommand",
|
|
19072
20693
|
"isCommandAvailable",
|
|
19073
20694
|
"isDirectoryNonEmpty",
|
|
19074
20695
|
"isPathInsideGitRepo",
|
|
@@ -19079,6 +20700,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
19079
20700
|
"normalizeAnvilUrl",
|
|
19080
20701
|
"parseCheckoutInput",
|
|
19081
20702
|
"parseConfigSetValue",
|
|
20703
|
+
"parseCredentialHelperCommand",
|
|
19082
20704
|
"pollDeviceAuthorization",
|
|
19083
20705
|
"preferredEditors",
|
|
19084
20706
|
"readEnvironmentPid",
|
|
@@ -19098,6 +20720,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
19098
20720
|
"validateBranchSyncStatus",
|
|
19099
20721
|
"validateCheckoutDestination",
|
|
19100
20722
|
"validateFormTemplate",
|
|
20723
|
+
"validateFormTemplateHtml",
|
|
19101
20724
|
"validatePath",
|
|
19102
20725
|
"verifyAndStoreTokens",
|
|
19103
20726
|
"verifyAuth",
|