@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/cli.js
CHANGED
|
@@ -8946,7 +8946,7 @@ var __webpack_exports__ = {};
|
|
|
8946
8946
|
commands,
|
|
8947
8947
|
format: "utf-8",
|
|
8948
8948
|
onError: esm_onError,
|
|
8949
|
-
parser
|
|
8949
|
+
parser: esm_parser
|
|
8950
8950
|
};
|
|
8951
8951
|
}
|
|
8952
8952
|
function checkIsRepoRootTask() {
|
|
@@ -8972,13 +8972,13 @@ var __webpack_exports__ = {};
|
|
|
8972
8972
|
commands,
|
|
8973
8973
|
format: "utf-8",
|
|
8974
8974
|
onError: esm_onError,
|
|
8975
|
-
parser
|
|
8975
|
+
parser: esm_parser
|
|
8976
8976
|
};
|
|
8977
8977
|
}
|
|
8978
8978
|
function isNotRepoMessage(error) {
|
|
8979
8979
|
return /(Not a git repository|Kein Git-Repository)/i.test(String(error));
|
|
8980
8980
|
}
|
|
8981
|
-
var CheckRepoActions, esm_onError,
|
|
8981
|
+
var CheckRepoActions, esm_onError, esm_parser;
|
|
8982
8982
|
var init_check_is_repo = __esm({
|
|
8983
8983
|
"src/lib/tasks/check-is-repo.ts" () {
|
|
8984
8984
|
"use strict";
|
|
@@ -8993,7 +8993,7 @@ var __webpack_exports__ = {};
|
|
|
8993
8993
|
if (128 === exitCode && isNotRepoMessage(error)) return done(Buffer.from("false"));
|
|
8994
8994
|
fail(error);
|
|
8995
8995
|
};
|
|
8996
|
-
|
|
8996
|
+
esm_parser = (text)=>"true" === text.trim();
|
|
8997
8997
|
}
|
|
8998
8998
|
});
|
|
8999
8999
|
function cleanSummaryParser(dryRun, text) {
|
|
@@ -13718,7 +13718,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13718
13718
|
}
|
|
13719
13719
|
function getCredentialHelperModulePath(options) {
|
|
13720
13720
|
const currentModulePath = options?.currentModulePath ?? __filename;
|
|
13721
|
-
if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
|
|
13721
|
+
if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
|
|
13722
13722
|
return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
|
|
13723
13723
|
}
|
|
13724
13724
|
async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
|
|
@@ -13895,6 +13895,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13895
13895
|
function setRepoContext(repoPath) {
|
|
13896
13896
|
repoContext = repoPath;
|
|
13897
13897
|
}
|
|
13898
|
+
const inFlightRefreshes = new Map();
|
|
13898
13899
|
async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
|
|
13899
13900
|
try {
|
|
13900
13901
|
const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
|
|
@@ -13948,53 +13949,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13948
13949
|
}
|
|
13949
13950
|
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
|
|
13950
13951
|
const normalized = normalizeAnvilUrl(anvilUrl);
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
|
|
13954
|
-
else {
|
|
13955
|
-
repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13956
|
-
tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
|
|
13957
|
-
}
|
|
13952
|
+
const source = await resolveTokenSource(normalized, username);
|
|
13953
|
+
const tokens = getSourceTokens(source);
|
|
13958
13954
|
if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
|
|
13959
13955
|
const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
|
|
13960
|
-
if (tokens.refreshToken && isExpired)
|
|
13956
|
+
if (tokens.refreshToken && isExpired) {
|
|
13957
|
+
const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
|
|
13958
|
+
const existingRefresh = inFlightRefreshes.get(refreshKey);
|
|
13959
|
+
if (existingRefresh) return existingRefresh;
|
|
13960
|
+
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
|
|
13961
|
+
inFlightRefreshes.delete(refreshKey);
|
|
13962
|
+
});
|
|
13963
|
+
inFlightRefreshes.set(refreshKey, refreshPromise);
|
|
13964
|
+
return refreshPromise;
|
|
13965
|
+
}
|
|
13966
|
+
if (tokens.authToken) return tokens.authToken;
|
|
13967
|
+
throw errors_createAuthError.required("No valid token available");
|
|
13968
|
+
}
|
|
13969
|
+
async function resolveTokenSource(normalizedUrl, username) {
|
|
13970
|
+
if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
|
|
13971
|
+
type: "memory",
|
|
13972
|
+
auth: inMemoryAuth
|
|
13973
|
+
};
|
|
13974
|
+
const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13975
|
+
if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
|
|
13976
|
+
type: "repo",
|
|
13977
|
+
auth: repoAuth,
|
|
13978
|
+
repoPath: repoContext
|
|
13979
|
+
};
|
|
13980
|
+
const tokens = getTokensAutoSelect(normalizedUrl, username);
|
|
13981
|
+
return {
|
|
13982
|
+
type: "store",
|
|
13983
|
+
tokens,
|
|
13984
|
+
accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13987
|
+
function getSourceTokens(source) {
|
|
13988
|
+
return "store" === source.type ? source.tokens : source.auth.tokens;
|
|
13989
|
+
}
|
|
13990
|
+
function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
|
|
13991
|
+
if (username) return username;
|
|
13992
|
+
const urlTokens = getUrlTokens(normalizedUrl);
|
|
13993
|
+
if (!urlTokens) return;
|
|
13994
|
+
const accounts = Object.keys(urlTokens);
|
|
13995
|
+
if (1 === accounts.length) return accounts[0];
|
|
13996
|
+
return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
|
|
13997
|
+
}
|
|
13998
|
+
function getRefreshKey(normalizedUrl, username, source, refreshToken) {
|
|
13999
|
+
switch(source.type){
|
|
14000
|
+
case "memory":
|
|
14001
|
+
return `memory:${normalizedUrl}:${source.auth.username}`;
|
|
14002
|
+
case "repo":
|
|
14003
|
+
return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
|
|
14004
|
+
case "store":
|
|
14005
|
+
return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
|
|
14006
|
+
}
|
|
14007
|
+
}
|
|
14008
|
+
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
|
|
14009
|
+
try {
|
|
14010
|
+
const tokens = getSourceTokens(source);
|
|
13961
14011
|
const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
|
|
13962
|
-
const tokenData = await refreshAccessToken(
|
|
13963
|
-
const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
|
|
14012
|
+
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
|
|
13964
14013
|
const newTokens = {
|
|
13965
14014
|
authToken: tokenData.access_token,
|
|
13966
14015
|
refreshToken: tokenData.refresh_token,
|
|
13967
|
-
authTokenExpiresAt:
|
|
14016
|
+
authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
|
|
13968
14017
|
clientId
|
|
13969
14018
|
};
|
|
13970
|
-
if (
|
|
13971
|
-
else if (
|
|
14019
|
+
if ("memory" === source.type) source.auth.tokens = newTokens;
|
|
14020
|
+
else if ("repo" === source.type) {
|
|
13972
14021
|
const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
|
|
13973
14022
|
if (!encryptionKey) throw errors_createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
|
|
13974
|
-
|
|
13975
|
-
await writeAuthToFile(
|
|
14023
|
+
source.auth.tokens = newTokens;
|
|
14024
|
+
await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
|
|
13976
14025
|
} else {
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
const urlTokens = getUrlTokens(normalized);
|
|
13980
|
-
if (urlTokens) {
|
|
13981
|
-
const accounts = Object.keys(urlTokens);
|
|
13982
|
-
accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
|
|
13983
|
-
}
|
|
13984
|
-
}
|
|
13985
|
-
if (!accountUsername) {
|
|
13986
|
-
await clearTokensForAccount(normalized, username, tokens.refreshToken);
|
|
14026
|
+
if (!source.accountUsername) {
|
|
14027
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13987
14028
|
throw errors_createAuthError.required("Cannot determine which account to refresh. Please log in again.");
|
|
13988
14029
|
}
|
|
13989
|
-
setAccountTokens(
|
|
14030
|
+
setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
|
|
13990
14031
|
}
|
|
13991
14032
|
return tokenData.access_token;
|
|
13992
14033
|
} catch (e) {
|
|
13993
|
-
await clearTokensForAccount(
|
|
14034
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13994
14035
|
throw e;
|
|
13995
14036
|
}
|
|
13996
|
-
if (tokens.authToken) return tokens.authToken;
|
|
13997
|
-
throw errors_createAuthError.required("No valid token available");
|
|
13998
14037
|
}
|
|
13999
14038
|
async function clearTokensForAccount(url, username, refreshToken) {
|
|
14000
14039
|
if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
|
|
@@ -14275,6 +14314,334 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14275
14314
|
} catch (_e) {}
|
|
14276
14315
|
return out;
|
|
14277
14316
|
}
|
|
14317
|
+
const promises_namespaceObject = require("fs/promises");
|
|
14318
|
+
var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
|
|
14319
|
+
function normalizeLineEndings(content) {
|
|
14320
|
+
return content.replace(/\r\n/g, "\n");
|
|
14321
|
+
}
|
|
14322
|
+
function pythonifyName(name) {
|
|
14323
|
+
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
14324
|
+
}
|
|
14325
|
+
function extractPythonName(relativePath, skipParts = 1) {
|
|
14326
|
+
const parts = relativePath.slice(0, -3).split("/");
|
|
14327
|
+
const isPackage = relativePath.endsWith("__init__.py");
|
|
14328
|
+
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
14329
|
+
return relevantParts.map(pythonifyName).join(".");
|
|
14330
|
+
}
|
|
14331
|
+
function deepEqual(a, b) {
|
|
14332
|
+
if (a === b) return true;
|
|
14333
|
+
if (null == a || null == b) return false;
|
|
14334
|
+
if (typeof a !== typeof b) return false;
|
|
14335
|
+
if ("object" != typeof a) return a === b;
|
|
14336
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
14337
|
+
if (Array.isArray(a)) {
|
|
14338
|
+
if (a.length !== b.length) return false;
|
|
14339
|
+
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14340
|
+
}
|
|
14341
|
+
const keysA = Object.keys(a);
|
|
14342
|
+
const keysB = Object.keys(b);
|
|
14343
|
+
if (keysA.length !== keysB.length) return false;
|
|
14344
|
+
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
14345
|
+
}
|
|
14346
|
+
function parseHtmlWithFrontmatter(htmlContent) {
|
|
14347
|
+
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14348
|
+
const match = htmlContent.match(frontmatterPattern);
|
|
14349
|
+
if (match) {
|
|
14350
|
+
const yamlStr = match[1];
|
|
14351
|
+
const html = match[2];
|
|
14352
|
+
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
14353
|
+
return {
|
|
14354
|
+
frontmatter,
|
|
14355
|
+
html
|
|
14356
|
+
};
|
|
14357
|
+
}
|
|
14358
|
+
return {
|
|
14359
|
+
frontmatter: {},
|
|
14360
|
+
html: htmlContent
|
|
14361
|
+
};
|
|
14362
|
+
}
|
|
14363
|
+
const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
|
|
14364
|
+
async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
|
|
14365
|
+
const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
|
|
14366
|
+
try {
|
|
14367
|
+
const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
|
|
14368
|
+
if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
|
|
14369
|
+
const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
|
|
14370
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
14371
|
+
return true;
|
|
14372
|
+
} catch (error) {
|
|
14373
|
+
if ("ENOENT" !== error.code) throw error;
|
|
14374
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
14375
|
+
return true;
|
|
14376
|
+
}
|
|
14377
|
+
}
|
|
14378
|
+
const ANVIL_DEPS_DIR = ".anvil/deps";
|
|
14379
|
+
const ANVIL_DEPS_MANIFEST = "manifest.json";
|
|
14380
|
+
const DEPS_MANIFEST_VERSION = 1;
|
|
14381
|
+
function formatFetchError(prefix, status, errorBody) {
|
|
14382
|
+
if (!errorBody) return `${prefix}: ${status}`;
|
|
14383
|
+
try {
|
|
14384
|
+
const parsed = JSON.parse(errorBody);
|
|
14385
|
+
if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
|
|
14386
|
+
} catch {}
|
|
14387
|
+
return `${prefix}: ${status} ${errorBody}`;
|
|
14388
|
+
}
|
|
14389
|
+
async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
|
|
14390
|
+
const { commit, includeDocs = true, knownDependencies = [] } = options;
|
|
14391
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
14392
|
+
const body = {
|
|
14393
|
+
include_docs: includeDocs,
|
|
14394
|
+
known_dependencies: knownDependencies
|
|
14395
|
+
};
|
|
14396
|
+
if (commit) body.commit = commit;
|
|
14397
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
|
|
14398
|
+
method: "POST",
|
|
14399
|
+
headers: {
|
|
14400
|
+
Authorization: `Bearer ${token}`,
|
|
14401
|
+
"Content-Type": "application/json"
|
|
14402
|
+
},
|
|
14403
|
+
body: JSON.stringify(body)
|
|
14404
|
+
});
|
|
14405
|
+
if (!resp.ok) {
|
|
14406
|
+
const errorBody = await resp.text();
|
|
14407
|
+
throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
|
|
14408
|
+
}
|
|
14409
|
+
return await resp.json();
|
|
14410
|
+
}
|
|
14411
|
+
async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
|
|
14412
|
+
const includeDocs = options.includeDocs ?? true;
|
|
14413
|
+
const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
|
|
14414
|
+
const response = await fetchAllDependencies(appId, anvilUrl, {
|
|
14415
|
+
commit: options.commit,
|
|
14416
|
+
includeDocs,
|
|
14417
|
+
knownDependencies
|
|
14418
|
+
});
|
|
14419
|
+
if (!response.resolved_dependencies.length) {
|
|
14420
|
+
await clearDepsCache(projectRoot);
|
|
14421
|
+
return {
|
|
14422
|
+
response,
|
|
14423
|
+
addedGitignoreEntry: false
|
|
14424
|
+
};
|
|
14425
|
+
}
|
|
14426
|
+
const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
|
|
14427
|
+
await writeAllDepsToCache(projectRoot, response, {
|
|
14428
|
+
includeDocs
|
|
14429
|
+
});
|
|
14430
|
+
return {
|
|
14431
|
+
response,
|
|
14432
|
+
addedGitignoreEntry
|
|
14433
|
+
};
|
|
14434
|
+
}
|
|
14435
|
+
function depsDir(projectRoot) {
|
|
14436
|
+
return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
|
|
14437
|
+
}
|
|
14438
|
+
function depsManifestPath(projectRoot) {
|
|
14439
|
+
return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
|
|
14440
|
+
}
|
|
14441
|
+
function isResolvedDependencyManifestEntry(value) {
|
|
14442
|
+
if (!value || "object" != typeof value) return false;
|
|
14443
|
+
const entry = value;
|
|
14444
|
+
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);
|
|
14445
|
+
}
|
|
14446
|
+
function isCachedDependencyManifestEntry(value) {
|
|
14447
|
+
if (!isResolvedDependencyManifestEntry(value)) return false;
|
|
14448
|
+
const entry = value;
|
|
14449
|
+
return "string" == typeof entry.package_name && entry.package_name.length > 0;
|
|
14450
|
+
}
|
|
14451
|
+
function isDepsCacheManifest(value) {
|
|
14452
|
+
if (!value || "object" != typeof value) return false;
|
|
14453
|
+
const manifest = value;
|
|
14454
|
+
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);
|
|
14455
|
+
}
|
|
14456
|
+
async function readDepsCacheManifest(projectRoot) {
|
|
14457
|
+
try {
|
|
14458
|
+
const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
|
|
14459
|
+
const parsed = JSON.parse(raw);
|
|
14460
|
+
return isDepsCacheManifest(parsed) ? parsed : void 0;
|
|
14461
|
+
} catch {
|
|
14462
|
+
return;
|
|
14463
|
+
}
|
|
14464
|
+
}
|
|
14465
|
+
async function getKnownDependenciesForCache(projectRoot, includeDocs) {
|
|
14466
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14467
|
+
if (!manifest || manifest.includeDocs !== includeDocs) return [];
|
|
14468
|
+
return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
|
|
14469
|
+
app_id,
|
|
14470
|
+
commit_id
|
|
14471
|
+
}));
|
|
14472
|
+
}
|
|
14473
|
+
function getDependencyWatchSubscriptionsFromManifest(manifest) {
|
|
14474
|
+
if (!manifest) return [];
|
|
14475
|
+
const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
|
|
14476
|
+
dep.app_id,
|
|
14477
|
+
dep
|
|
14478
|
+
]));
|
|
14479
|
+
return manifest.resolved_dependencies.flatMap((dep)=>{
|
|
14480
|
+
if (!dep.branch) return [];
|
|
14481
|
+
return [
|
|
14482
|
+
{
|
|
14483
|
+
appId: dep.app_id,
|
|
14484
|
+
branch: dep.branch,
|
|
14485
|
+
packageName: cachedByAppId.get(dep.app_id)?.package_name
|
|
14486
|
+
}
|
|
14487
|
+
];
|
|
14488
|
+
});
|
|
14489
|
+
}
|
|
14490
|
+
function normalizeAnvilYamlDependenciesFromContent(content) {
|
|
14491
|
+
const parsed = external_js_yaml_default().load(content);
|
|
14492
|
+
if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
|
|
14493
|
+
return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
|
|
14494
|
+
}
|
|
14495
|
+
function normalizeAnvilYamlDependenciesValue(value) {
|
|
14496
|
+
if (!Array.isArray(value)) return [];
|
|
14497
|
+
return normalizeForComparison(value);
|
|
14498
|
+
}
|
|
14499
|
+
async function readAnvilYamlDependencies(projectRoot) {
|
|
14500
|
+
try {
|
|
14501
|
+
const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
|
|
14502
|
+
return normalizeAnvilYamlDependenciesFromContent(content);
|
|
14503
|
+
} catch {
|
|
14504
|
+
return [];
|
|
14505
|
+
}
|
|
14506
|
+
}
|
|
14507
|
+
function anvilYamlDependenciesEqual(left, right) {
|
|
14508
|
+
return deepEqual(left, right);
|
|
14509
|
+
}
|
|
14510
|
+
function normalizeForComparison(value) {
|
|
14511
|
+
if (Array.isArray(value)) return value.map(normalizeForComparison);
|
|
14512
|
+
if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
|
|
14513
|
+
key,
|
|
14514
|
+
normalizeForComparison(entryValue)
|
|
14515
|
+
]));
|
|
14516
|
+
return value;
|
|
14517
|
+
}
|
|
14518
|
+
function cachedManifestEntry(dep, packageName) {
|
|
14519
|
+
return {
|
|
14520
|
+
app_id: dep.app_id,
|
|
14521
|
+
commit_id: dep.commit_id,
|
|
14522
|
+
...dep.branch ? {
|
|
14523
|
+
branch: dep.branch
|
|
14524
|
+
} : {},
|
|
14525
|
+
package_name: packageName
|
|
14526
|
+
};
|
|
14527
|
+
}
|
|
14528
|
+
function safeDependencyPath(filePath, kind) {
|
|
14529
|
+
const normalizedPath = external_path_default().normalize(filePath);
|
|
14530
|
+
if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
|
|
14531
|
+
return normalizedPath;
|
|
14532
|
+
}
|
|
14533
|
+
async function writeDepToCache(projectRoot, dep) {
|
|
14534
|
+
const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
|
|
14535
|
+
await promises_default().rm(depDir, {
|
|
14536
|
+
recursive: true,
|
|
14537
|
+
force: true
|
|
14538
|
+
});
|
|
14539
|
+
await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
|
|
14540
|
+
recursive: true
|
|
14541
|
+
});
|
|
14542
|
+
await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
|
|
14543
|
+
recursive: true
|
|
14544
|
+
});
|
|
14545
|
+
await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
|
|
14546
|
+
package_name: dep.package_name
|
|
14547
|
+
}));
|
|
14548
|
+
for (const mod of dep.modules ?? [])if (mod.is_package) {
|
|
14549
|
+
const modDir = external_path_default().join(depDir, "client_code", mod.name);
|
|
14550
|
+
await promises_default().mkdir(modDir, {
|
|
14551
|
+
recursive: true
|
|
14552
|
+
});
|
|
14553
|
+
await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
|
|
14554
|
+
} else await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${mod.name}.py`), mod.code);
|
|
14555
|
+
for (const form of dep.forms ?? []){
|
|
14556
|
+
const { code, class_name, is_package, ...formTemplate } = form;
|
|
14557
|
+
if (is_package) {
|
|
14558
|
+
const formDir = external_path_default().join(depDir, "client_code", class_name);
|
|
14559
|
+
await promises_default().mkdir(formDir, {
|
|
14560
|
+
recursive: true
|
|
14561
|
+
});
|
|
14562
|
+
await promises_default().writeFile(external_path_default().join(formDir, "__init__.py"), code);
|
|
14563
|
+
await promises_default().writeFile(external_path_default().join(formDir, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
|
|
14564
|
+
} else {
|
|
14565
|
+
await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.py`), code);
|
|
14566
|
+
await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.yaml`), external_js_yaml_default().dump(formTemplate));
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14569
|
+
for (const mod of dep.server_modules ?? [])if (mod.is_package) {
|
|
14570
|
+
const modDir = external_path_default().join(depDir, "server_code", mod.name);
|
|
14571
|
+
await promises_default().mkdir(modDir, {
|
|
14572
|
+
recursive: true
|
|
14573
|
+
});
|
|
14574
|
+
await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
|
|
14575
|
+
} else await promises_default().writeFile(external_path_default().join(depDir, "server_code", `${mod.name}.py`), mod.code);
|
|
14576
|
+
for (const asset of dep.assets ?? []){
|
|
14577
|
+
const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
|
|
14578
|
+
await promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
14579
|
+
recursive: true
|
|
14580
|
+
});
|
|
14581
|
+
await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
|
|
14582
|
+
}
|
|
14583
|
+
for (const doc of dep.docs ?? []){
|
|
14584
|
+
const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
|
|
14585
|
+
await promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
14586
|
+
recursive: true
|
|
14587
|
+
});
|
|
14588
|
+
await promises_default().writeFile(targetPath, doc.content);
|
|
14589
|
+
}
|
|
14590
|
+
}
|
|
14591
|
+
async function writeAllDepsToCache(projectRoot, response, options = {}) {
|
|
14592
|
+
const includeDocs = options.includeDocs ?? true;
|
|
14593
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
14594
|
+
const canReuseExistingCache = manifest?.includeDocs === includeDocs;
|
|
14595
|
+
if (!canReuseExistingCache) await clearDepsCache(projectRoot);
|
|
14596
|
+
const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
|
|
14597
|
+
const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
|
|
14598
|
+
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), {
|
|
14599
|
+
recursive: true,
|
|
14600
|
+
force: true
|
|
14601
|
+
});
|
|
14602
|
+
for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
|
|
14603
|
+
const returnedByAppId = new Map(response.dependencies.map((dep)=>[
|
|
14604
|
+
dep.app_id,
|
|
14605
|
+
dep
|
|
14606
|
+
]));
|
|
14607
|
+
const previousByAppId = new Map(previousCached.map((dep)=>[
|
|
14608
|
+
dep.app_id,
|
|
14609
|
+
dep
|
|
14610
|
+
]));
|
|
14611
|
+
const cachedDependencies = response.resolved_dependencies.map((dep)=>{
|
|
14612
|
+
const returned = returnedByAppId.get(dep.app_id);
|
|
14613
|
+
if (returned) return cachedManifestEntry(returned, returned.package_name);
|
|
14614
|
+
const previous = previousByAppId.get(dep.app_id);
|
|
14615
|
+
if (!previous) return;
|
|
14616
|
+
return cachedManifestEntry(dep, previous.package_name);
|
|
14617
|
+
}).filter((dep)=>void 0 !== dep);
|
|
14618
|
+
const newManifest = {
|
|
14619
|
+
version: DEPS_MANIFEST_VERSION,
|
|
14620
|
+
includeDocs,
|
|
14621
|
+
resolved_dependencies: response.resolved_dependencies,
|
|
14622
|
+
cached_dependencies: cachedDependencies
|
|
14623
|
+
};
|
|
14624
|
+
await promises_default().mkdir(depsDir(projectRoot), {
|
|
14625
|
+
recursive: true
|
|
14626
|
+
});
|
|
14627
|
+
await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
|
|
14628
|
+
}
|
|
14629
|
+
async function clearDepsCache(projectRoot) {
|
|
14630
|
+
await promises_default().rm(depsDir(projectRoot), {
|
|
14631
|
+
recursive: true,
|
|
14632
|
+
force: true
|
|
14633
|
+
});
|
|
14634
|
+
}
|
|
14635
|
+
async function getCachedPackages(projectRoot) {
|
|
14636
|
+
try {
|
|
14637
|
+
const entries = await promises_default().readdir(depsDir(projectRoot), {
|
|
14638
|
+
withFileTypes: true
|
|
14639
|
+
});
|
|
14640
|
+
return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
|
|
14641
|
+
} catch {
|
|
14642
|
+
return [];
|
|
14643
|
+
}
|
|
14644
|
+
}
|
|
14278
14645
|
class GitService {
|
|
14279
14646
|
git;
|
|
14280
14647
|
repoPath;
|
|
@@ -14361,6 +14728,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14361
14728
|
throw errors_createGitError.commandFailed("status", e.message);
|
|
14362
14729
|
}
|
|
14363
14730
|
}
|
|
14731
|
+
async discardModeOnlyChanges(paths) {
|
|
14732
|
+
const discarded = [];
|
|
14733
|
+
for (const relativePath of paths)try {
|
|
14734
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14735
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14736
|
+
const contentUnchanged = await this.hasSameContentAsHead(relativePath);
|
|
14737
|
+
if (!contentUnchanged) continue;
|
|
14738
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14739
|
+
discarded.push(relativePath);
|
|
14740
|
+
} catch (e) {
|
|
14741
|
+
throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
|
|
14742
|
+
}
|
|
14743
|
+
return discarded;
|
|
14744
|
+
}
|
|
14745
|
+
async discardStagedModeOnlyChanges(paths) {
|
|
14746
|
+
const discarded = [];
|
|
14747
|
+
for (const relativePath of paths)try {
|
|
14748
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14749
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14750
|
+
const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
|
|
14751
|
+
if (!indexContentUnchanged) continue;
|
|
14752
|
+
await this.git.raw([
|
|
14753
|
+
"reset",
|
|
14754
|
+
"-q",
|
|
14755
|
+
"HEAD",
|
|
14756
|
+
"--",
|
|
14757
|
+
relativePath
|
|
14758
|
+
]);
|
|
14759
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14760
|
+
discarded.push(relativePath);
|
|
14761
|
+
} catch (e) {
|
|
14762
|
+
throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
|
|
14763
|
+
}
|
|
14764
|
+
return discarded;
|
|
14765
|
+
}
|
|
14766
|
+
async getHeadFileMode(relativePath) {
|
|
14767
|
+
const output = await this.git.raw([
|
|
14768
|
+
"ls-tree",
|
|
14769
|
+
"HEAD",
|
|
14770
|
+
"--",
|
|
14771
|
+
relativePath
|
|
14772
|
+
]);
|
|
14773
|
+
const match = output.match(/^(\d{6})\s/);
|
|
14774
|
+
return match?.[1] ?? null;
|
|
14775
|
+
}
|
|
14776
|
+
async hasSameContentAsHead(relativePath) {
|
|
14777
|
+
const headHash = (await this.git.revparse([
|
|
14778
|
+
`HEAD:${relativePath}`
|
|
14779
|
+
])).trim();
|
|
14780
|
+
const worktreeHash = (await this.git.raw([
|
|
14781
|
+
"hash-object",
|
|
14782
|
+
"--",
|
|
14783
|
+
relativePath
|
|
14784
|
+
])).trim();
|
|
14785
|
+
return headHash === worktreeHash;
|
|
14786
|
+
}
|
|
14787
|
+
async hasSameIndexContentAsHead(relativePath) {
|
|
14788
|
+
const headHash = (await this.git.revparse([
|
|
14789
|
+
`HEAD:${relativePath}`
|
|
14790
|
+
])).trim();
|
|
14791
|
+
const indexHash = (await this.git.revparse([
|
|
14792
|
+
`:${relativePath}`
|
|
14793
|
+
])).trim();
|
|
14794
|
+
return headHash === indexHash;
|
|
14795
|
+
}
|
|
14796
|
+
async restoreExecutableBit(relativePath, executable) {
|
|
14797
|
+
const filePath = external_path_default().join(this.repoPath, relativePath);
|
|
14798
|
+
const stat = await external_fs_.promises.stat(filePath);
|
|
14799
|
+
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14800
|
+
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14801
|
+
}
|
|
14364
14802
|
async hasUncommittedChanges() {
|
|
14365
14803
|
const status = await this.getStatus();
|
|
14366
14804
|
return !status.isClean;
|
|
@@ -14500,6 +14938,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14500
14938
|
throw errors_createGitError.commandFailed("rebase --abort", e.message);
|
|
14501
14939
|
}
|
|
14502
14940
|
}
|
|
14941
|
+
async isAncestor(ancestorRef, descendantRef) {
|
|
14942
|
+
try {
|
|
14943
|
+
const mergeBase = (await this.git.raw([
|
|
14944
|
+
"merge-base",
|
|
14945
|
+
ancestorRef,
|
|
14946
|
+
descendantRef
|
|
14947
|
+
])).trim();
|
|
14948
|
+
const ancestorCommit = (await this.git.revparse([
|
|
14949
|
+
ancestorRef
|
|
14950
|
+
])).trim();
|
|
14951
|
+
return mergeBase === ancestorCommit;
|
|
14952
|
+
} catch {
|
|
14953
|
+
return false;
|
|
14954
|
+
}
|
|
14955
|
+
}
|
|
14956
|
+
async mergeFastForward(ref) {
|
|
14957
|
+
try {
|
|
14958
|
+
await this.git.raw([
|
|
14959
|
+
"merge",
|
|
14960
|
+
"--ff-only",
|
|
14961
|
+
ref
|
|
14962
|
+
]);
|
|
14963
|
+
} catch (e) {
|
|
14964
|
+
throw errors_createGitError.commandFailed("merge --ff-only", e.message);
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14503
14967
|
async deleteRef(ref) {
|
|
14504
14968
|
try {
|
|
14505
14969
|
await this.git.raw([
|
|
@@ -14544,11 +15008,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14544
15008
|
} catch (error) {}
|
|
14545
15009
|
}
|
|
14546
15010
|
}
|
|
14547
|
-
async function getStagedFileChanges(git) {
|
|
15011
|
+
async function getStagedFileChanges(git, gitService) {
|
|
14548
15012
|
try {
|
|
14549
15013
|
const status = await git.status();
|
|
14550
15014
|
const renames = status.renamed || [];
|
|
14551
15015
|
const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
|
|
15016
|
+
const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
|
|
14552
15017
|
const stagedRenames = [];
|
|
14553
15018
|
for (const f of stagedFiles)if ("R" === f.index) {
|
|
14554
15019
|
const rename = renames.find((r)=>r.to === f.path);
|
|
@@ -14561,19 +15026,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14561
15026
|
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
14562
15027
|
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
14563
15028
|
const changes = [];
|
|
14564
|
-
for (const f of stagedFiles)
|
|
14565
|
-
if (
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
15029
|
+
for (const f of stagedFiles){
|
|
15030
|
+
if ("R" !== f.index) {
|
|
15031
|
+
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
15032
|
+
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
15033
|
+
path: f.path,
|
|
15034
|
+
type: "unlink"
|
|
15035
|
+
});
|
|
15036
|
+
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
15037
|
+
path: f.path,
|
|
15038
|
+
type: "add"
|
|
15039
|
+
});
|
|
15040
|
+
else changes.push({
|
|
15041
|
+
path: f.path,
|
|
15042
|
+
type: "change"
|
|
15043
|
+
});
|
|
15044
|
+
}
|
|
15045
|
+
}
|
|
14577
15046
|
}
|
|
14578
15047
|
changes.push(...stagedRenames);
|
|
14579
15048
|
return changes;
|
|
@@ -14581,50 +15050,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14581
15050
|
throw errors_createGitError.commandFailed("status", e.message);
|
|
14582
15051
|
}
|
|
14583
15052
|
}
|
|
14584
|
-
function normalizeLineEndings(content) {
|
|
14585
|
-
return content.replace(/\r\n/g, "\n");
|
|
14586
|
-
}
|
|
14587
|
-
function pythonifyName(name) {
|
|
14588
|
-
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
14589
|
-
}
|
|
14590
|
-
function extractPythonName(relativePath, skipParts = 1) {
|
|
14591
|
-
const parts = relativePath.slice(0, -3).split("/");
|
|
14592
|
-
const isPackage = relativePath.endsWith("__init__.py");
|
|
14593
|
-
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
14594
|
-
return relevantParts.map(pythonifyName).join(".");
|
|
14595
|
-
}
|
|
14596
|
-
function deepEqual(a, b) {
|
|
14597
|
-
if (a === b) return true;
|
|
14598
|
-
if (null == a || null == b) return false;
|
|
14599
|
-
if (typeof a !== typeof b) return false;
|
|
14600
|
-
if ("object" != typeof a) return a === b;
|
|
14601
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
14602
|
-
if (Array.isArray(a)) {
|
|
14603
|
-
if (a.length !== b.length) return false;
|
|
14604
|
-
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14605
|
-
}
|
|
14606
|
-
const keysA = Object.keys(a);
|
|
14607
|
-
const keysB = Object.keys(b);
|
|
14608
|
-
if (keysA.length !== keysB.length) return false;
|
|
14609
|
-
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
14610
|
-
}
|
|
14611
|
-
function parseHtmlWithFrontmatter(htmlContent) {
|
|
14612
|
-
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14613
|
-
const match = htmlContent.match(frontmatterPattern);
|
|
14614
|
-
if (match) {
|
|
14615
|
-
const yamlStr = match[1];
|
|
14616
|
-
const html = match[2];
|
|
14617
|
-
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
14618
|
-
return {
|
|
14619
|
-
frontmatter,
|
|
14620
|
-
html
|
|
14621
|
-
};
|
|
14622
|
-
}
|
|
14623
|
-
return {
|
|
14624
|
-
frontmatter: {},
|
|
14625
|
-
html: htmlContent
|
|
14626
|
-
};
|
|
14627
|
-
}
|
|
14628
15053
|
const external_chokidar_namespaceObject = require("chokidar");
|
|
14629
15054
|
var external_chokidar_default = /*#__PURE__*/ __webpack_require__.n(external_chokidar_namespaceObject);
|
|
14630
15055
|
class FileWatcher extends Emitter {
|
|
@@ -14774,6 +15199,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14774
15199
|
editSession;
|
|
14775
15200
|
username;
|
|
14776
15201
|
environment;
|
|
15202
|
+
dependencyWatchEnabled;
|
|
15203
|
+
dependencySubscriptions;
|
|
15204
|
+
activeDependencySubscriptions = new Map();
|
|
15205
|
+
watchedPrimaryBranch = null;
|
|
15206
|
+
nextSubscriptionId = 1;
|
|
15207
|
+
pendingDependencySubscriptionById = new Map();
|
|
14777
15208
|
reconnectAttempts = 0;
|
|
14778
15209
|
connectAttempts = 0;
|
|
14779
15210
|
reconnectTimer = null;
|
|
@@ -14796,6 +15227,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14796
15227
|
this.editSession = options.editSession;
|
|
14797
15228
|
this.username = options.username;
|
|
14798
15229
|
this.environment = options.environment;
|
|
15230
|
+
this.dependencyWatchEnabled = options.dependencyWatch ?? true;
|
|
15231
|
+
this.dependencySubscriptions = dedupeDependencySubscriptions(options.dependencySubscriptions ?? []);
|
|
14799
15232
|
this.sessionId = Math.random().toString(36).substring(2, 10);
|
|
14800
15233
|
this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
|
|
14801
15234
|
}
|
|
@@ -14821,6 +15254,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14821
15254
|
const subscribeMsg = this.buildSubscribeMessage(this.currentBranch);
|
|
14822
15255
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing: ${JSON.stringify(subscribeMsg)}`);
|
|
14823
15256
|
this.ws?.send(JSON.stringify(subscribeMsg));
|
|
15257
|
+
this.watchedPrimaryBranch = this.currentBranch;
|
|
15258
|
+
this.activeDependencySubscriptions.clear();
|
|
15259
|
+
this.subscribeToDependencies();
|
|
14824
15260
|
this.emit("connected", void 0);
|
|
14825
15261
|
});
|
|
14826
15262
|
this.ws.on("unexpected-response", (request, response)=>{
|
|
@@ -14843,6 +15279,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14843
15279
|
try {
|
|
14844
15280
|
const msg = JSON.parse(data.toString());
|
|
14845
15281
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
|
|
15282
|
+
this.handleDependencySubscriptionReply(msg);
|
|
14846
15283
|
this.handleMessage(msg);
|
|
14847
15284
|
} catch (error) {
|
|
14848
15285
|
logger_logger.error(external_chalk_default().red(`Failed to parse WebSocket message: ${error.message}`));
|
|
@@ -14893,7 +15330,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14893
15330
|
if (!ref_update) return void logger_logger.debug("Ignoring REPO_UPDATE with no ref_update");
|
|
14894
15331
|
const { ref } = ref_update;
|
|
14895
15332
|
const commitHash = "string" == typeof commit ? commit : commit?.v;
|
|
15333
|
+
const appId = this.getMessageAppId(msg);
|
|
14896
15334
|
this.emit("repo-update", {
|
|
15335
|
+
appId,
|
|
14897
15336
|
ref,
|
|
14898
15337
|
commitHash: commitHash || "unknown",
|
|
14899
15338
|
saveSeq: ref_update?.["save-seq"]
|
|
@@ -14908,11 +15347,26 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14908
15347
|
}
|
|
14909
15348
|
updateBranch(branch) {
|
|
14910
15349
|
this.currentBranch = branch;
|
|
14911
|
-
if (this.ws?.readyState === external_ws_default().OPEN)
|
|
15350
|
+
if (this.ws?.readyState === external_ws_default().OPEN) {
|
|
15351
|
+
this.ws.send(JSON.stringify(this.buildSubscribeMessage(branch)));
|
|
15352
|
+
this.watchedPrimaryBranch = branch;
|
|
15353
|
+
this.subscribeToDependencies();
|
|
15354
|
+
}
|
|
14912
15355
|
}
|
|
14913
15356
|
updateEnvironment(environment) {
|
|
14914
15357
|
this.environment = environment;
|
|
14915
15358
|
}
|
|
15359
|
+
updateDependencySubscriptions(subscriptions) {
|
|
15360
|
+
const previousSubscriptions = this.dependencySubscriptions;
|
|
15361
|
+
this.dependencySubscriptions = dedupeDependencySubscriptions(subscriptions);
|
|
15362
|
+
if (this.ws?.readyState === external_ws_default().OPEN) {
|
|
15363
|
+
this.unsubscribeStaleDependencies(previousSubscriptions, this.dependencySubscriptions);
|
|
15364
|
+
this.subscribeToDependencies();
|
|
15365
|
+
}
|
|
15366
|
+
}
|
|
15367
|
+
getDependencySubscriptions() {
|
|
15368
|
+
return this.dependencySubscriptions;
|
|
15369
|
+
}
|
|
14916
15370
|
getEditSession() {
|
|
14917
15371
|
return this.editSession;
|
|
14918
15372
|
}
|
|
@@ -14923,7 +15377,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14923
15377
|
clearTimeout(this.reconnectTimer);
|
|
14924
15378
|
this.reconnectTimer = null;
|
|
14925
15379
|
}
|
|
15380
|
+
if (this.ws?.readyState === external_ws_default().OPEN) {
|
|
15381
|
+
this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(this.appId)));
|
|
15382
|
+
this.unwatchDependencySubscriptions([
|
|
15383
|
+
...this.activeDependencySubscriptions.values()
|
|
15384
|
+
]);
|
|
15385
|
+
}
|
|
15386
|
+
this.teardownSocket();
|
|
15387
|
+
}
|
|
15388
|
+
reconnect() {
|
|
15389
|
+
if (this.isClosing) return;
|
|
14926
15390
|
this.teardownSocket();
|
|
15391
|
+
this.connect().catch((error)=>{
|
|
15392
|
+
this.emit("error", {
|
|
15393
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
15394
|
+
});
|
|
15395
|
+
});
|
|
14927
15396
|
}
|
|
14928
15397
|
isConnected() {
|
|
14929
15398
|
return this.ws?.readyState === external_ws_default().OPEN;
|
|
@@ -14956,7 +15425,73 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14956
15425
|
} : {}
|
|
14957
15426
|
};
|
|
14958
15427
|
}
|
|
14959
|
-
|
|
15428
|
+
buildUnsubscribeMessage(appId) {
|
|
15429
|
+
return {
|
|
15430
|
+
cmd: "UNWATCH_APP",
|
|
15431
|
+
app: appId
|
|
15432
|
+
};
|
|
15433
|
+
}
|
|
15434
|
+
subscribeToDependencies() {
|
|
15435
|
+
if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
|
|
15436
|
+
for (const subscription of this.dependencySubscriptions){
|
|
15437
|
+
const key = this.dependencySubscriptionKey(subscription);
|
|
15438
|
+
if (this.activeDependencySubscriptions.has(key)) continue;
|
|
15439
|
+
const id = this.nextSubscriptionId++;
|
|
15440
|
+
this.pendingDependencySubscriptionById.set(id, subscription);
|
|
15441
|
+
this.activeDependencySubscriptions.set(key, subscription);
|
|
15442
|
+
const subscribeMsg = {
|
|
15443
|
+
id,
|
|
15444
|
+
cmd: "WATCH_APP",
|
|
15445
|
+
app: subscription.appId,
|
|
15446
|
+
only_branch: subscription.branch
|
|
15447
|
+
};
|
|
15448
|
+
logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing dependency: ${JSON.stringify(subscribeMsg)}`);
|
|
15449
|
+
logger_logger.verbose(external_chalk_default().gray(`Watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch})`));
|
|
15450
|
+
this.ws.send(JSON.stringify(subscribeMsg));
|
|
15451
|
+
}
|
|
15452
|
+
}
|
|
15453
|
+
handleDependencySubscriptionReply(msg) {
|
|
15454
|
+
if ("number" != typeof msg.id) return;
|
|
15455
|
+
const subscription = this.pendingDependencySubscriptionById.get(msg.id);
|
|
15456
|
+
if (!subscription) return;
|
|
15457
|
+
this.pendingDependencySubscriptionById.delete(msg.id);
|
|
15458
|
+
if (!msg.error) return;
|
|
15459
|
+
this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
|
|
15460
|
+
logger_logger.warn(external_chalk_default().yellow(`Could not watch dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch}): ${this.formatDependencySubscriptionError(msg.error)}`));
|
|
15461
|
+
}
|
|
15462
|
+
formatDependencySubscriptionError(error) {
|
|
15463
|
+
if ("string" == typeof error) return error;
|
|
15464
|
+
if (error && "object" == typeof error && "string" == typeof error.message) return error.message;
|
|
15465
|
+
return JSON.stringify(error);
|
|
15466
|
+
}
|
|
15467
|
+
getMessageAppId(msg) {
|
|
15468
|
+
if ("string" == typeof msg.app) return msg.app;
|
|
15469
|
+
if ("string" == typeof msg.app_id) return msg.app_id;
|
|
15470
|
+
if ("string" == typeof msg.ref_update?.app) return msg.ref_update.app;
|
|
15471
|
+
if ("string" == typeof msg.ref_update?.app_id) return msg.ref_update.app_id;
|
|
15472
|
+
return this.appId;
|
|
15473
|
+
}
|
|
15474
|
+
unsubscribeStaleDependencies(previousSubscriptions, nextSubscriptions) {
|
|
15475
|
+
const nextAppIds = new Set(nextSubscriptions.map((subscription)=>subscription.appId));
|
|
15476
|
+
const staleSubscriptions = previousSubscriptions.filter((subscription)=>!nextAppIds.has(subscription.appId));
|
|
15477
|
+
this.unwatchDependencySubscriptions(staleSubscriptions);
|
|
15478
|
+
const nextKeys = new Set(nextSubscriptions.map((subscription)=>this.dependencySubscriptionKey(subscription)));
|
|
15479
|
+
for (const subscription of previousSubscriptions){
|
|
15480
|
+
const key = this.dependencySubscriptionKey(subscription);
|
|
15481
|
+
if (!nextKeys.has(key)) this.activeDependencySubscriptions.delete(key);
|
|
15482
|
+
}
|
|
15483
|
+
}
|
|
15484
|
+
unwatchDependencySubscriptions(subscriptions) {
|
|
15485
|
+
if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
|
|
15486
|
+
const unwatchedAppIds = new Set();
|
|
15487
|
+
for (const subscription of subscriptions)if (!unwatchedAppIds.has(subscription.appId)) {
|
|
15488
|
+
unwatchedAppIds.add(subscription.appId);
|
|
15489
|
+
logger_logger.verbose(external_chalk_default().gray(`Stopped watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId})`));
|
|
15490
|
+
this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(subscription.appId)));
|
|
15491
|
+
}
|
|
15492
|
+
for (const subscription of subscriptions)this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
|
|
15493
|
+
}
|
|
15494
|
+
getConnectionContext() {
|
|
14960
15495
|
return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
|
|
14961
15496
|
}
|
|
14962
15497
|
getConnectionTarget(wsUrl) {
|
|
@@ -15027,6 +15562,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15027
15562
|
}
|
|
15028
15563
|
teardownSocket() {
|
|
15029
15564
|
this.stopHeartbeat();
|
|
15565
|
+
this.pendingDependencySubscriptionById.clear();
|
|
15566
|
+
this.activeDependencySubscriptions.clear();
|
|
15567
|
+
this.watchedPrimaryBranch = null;
|
|
15030
15568
|
const socket = this.ws;
|
|
15031
15569
|
this.ws = null;
|
|
15032
15570
|
if (!socket) return;
|
|
@@ -15042,6 +15580,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15042
15580
|
logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
|
|
15043
15581
|
}
|
|
15044
15582
|
}
|
|
15583
|
+
dependencySubscriptionKey(subscription) {
|
|
15584
|
+
return `${subscription.appId}\0${subscription.branch}`;
|
|
15585
|
+
}
|
|
15045
15586
|
}
|
|
15046
15587
|
function normalizeEnvironmentBinding(msg) {
|
|
15047
15588
|
return {
|
|
@@ -15050,6 +15591,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15050
15591
|
tableMappingId: msg.tableMappingId ?? msg.table_mapping_id ?? null
|
|
15051
15592
|
};
|
|
15052
15593
|
}
|
|
15594
|
+
function dedupeDependencySubscriptions(subscriptions) {
|
|
15595
|
+
const seen = new Set();
|
|
15596
|
+
const deduped = [];
|
|
15597
|
+
for (const subscription of subscriptions){
|
|
15598
|
+
const key = `${subscription.appId}\0${subscription.branch}`;
|
|
15599
|
+
if (!seen.has(key)) {
|
|
15600
|
+
seen.add(key);
|
|
15601
|
+
deduped.push(subscription);
|
|
15602
|
+
}
|
|
15603
|
+
}
|
|
15604
|
+
return deduped;
|
|
15605
|
+
}
|
|
15053
15606
|
async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
|
|
15054
15607
|
try {
|
|
15055
15608
|
const files = await gitService.diffNames(oldCommitId, newCommitId);
|
|
@@ -15100,7 +15653,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15100
15653
|
...status.modified,
|
|
15101
15654
|
...status.notAdded,
|
|
15102
15655
|
...status.created,
|
|
15103
|
-
...status.deleted
|
|
15656
|
+
...status.deleted,
|
|
15657
|
+
...(status.renamed ?? []).flatMap((r)=>[
|
|
15658
|
+
r.from,
|
|
15659
|
+
r.to
|
|
15660
|
+
])
|
|
15104
15661
|
]);
|
|
15105
15662
|
}
|
|
15106
15663
|
function extractChangesFromStatus(status) {
|
|
@@ -15142,12 +15699,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15142
15699
|
result.localOnlyChanges.forEach((c)=>logger_logger.verbose(external_chalk_default().gray(` ${c.path}`)));
|
|
15143
15700
|
}
|
|
15144
15701
|
}
|
|
15145
|
-
async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
|
|
15702
|
+
async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles, options = {}) {
|
|
15146
15703
|
const filesToRemove = [];
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15704
|
+
const localChangesToPreserve = options.localChangesToPreserve ?? new Set();
|
|
15705
|
+
for (const file of unstagedFiles)if (!localChangesToPreserve.has(file)) {
|
|
15706
|
+
try {
|
|
15707
|
+
await gitService.show(`HEAD:${file}`);
|
|
15708
|
+
continue;
|
|
15709
|
+
} catch (e) {}
|
|
15710
|
+
if (options.oldCommitId) {
|
|
15711
|
+
try {
|
|
15712
|
+
await gitService.show(`${options.oldCommitId}:${file}`);
|
|
15713
|
+
filesToRemove.push(file);
|
|
15714
|
+
} catch (e) {}
|
|
15715
|
+
continue;
|
|
15716
|
+
}
|
|
15717
|
+
if (remotelyChangedFiles.has(file)) filesToRemove.push(file);
|
|
15151
15718
|
}
|
|
15152
15719
|
if (filesToRemove.length > 0) try {
|
|
15153
15720
|
await gitService.clean(filesToRemove);
|
|
@@ -15159,6 +15726,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15159
15726
|
return filesToRemove;
|
|
15160
15727
|
}
|
|
15161
15728
|
const external_crypto_namespaceObject = require("crypto");
|
|
15729
|
+
const form_template_parser_namespaceObject = require("@anvil-works/form-template-parser");
|
|
15162
15730
|
const BUILTIN_COMPONENT_TYPES = new Set([
|
|
15163
15731
|
"Label",
|
|
15164
15732
|
"RichText",
|
|
@@ -15189,59 +15757,156 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15189
15757
|
"CustomComponent",
|
|
15190
15758
|
"LayoutSlot",
|
|
15191
15759
|
"FlowPanel",
|
|
15192
|
-
"HtmlTemplate"
|
|
15193
|
-
|
|
15194
|
-
const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
|
|
15195
|
-
"/runtime/services/tables.yml",
|
|
15196
|
-
"/runtime/services/google.yml",
|
|
15197
|
-
"/runtime/services/uplink.yml",
|
|
15198
|
-
"/runtime/services/stripe.yml",
|
|
15199
|
-
"/runtime/services/segment.yml",
|
|
15200
|
-
"/runtime/services/facebook.yml",
|
|
15201
|
-
"/runtime/services/anvil/users.yml",
|
|
15202
|
-
"/runtime/services/anvil/secrets.yml",
|
|
15203
|
-
"/runtime/services/anvil/saml.yml",
|
|
15204
|
-
"/runtime/services/anvil/microsoft.yml",
|
|
15205
|
-
"/runtime/services/anvil/email.yml",
|
|
15206
|
-
"/runtime/services/anvil/files.yml"
|
|
15760
|
+
"HtmlTemplate",
|
|
15761
|
+
"HtmlComponent"
|
|
15207
15762
|
]);
|
|
15208
15763
|
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15209
|
-
const
|
|
15210
|
-
|
|
15211
|
-
"
|
|
15212
|
-
"
|
|
15213
|
-
"
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
"
|
|
15217
|
-
"
|
|
15218
|
-
"
|
|
15219
|
-
"
|
|
15220
|
-
"
|
|
15221
|
-
"
|
|
15222
|
-
"
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
"
|
|
15226
|
-
"
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
"
|
|
15230
|
-
"
|
|
15231
|
-
"
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
"
|
|
15235
|
-
"
|
|
15236
|
-
"
|
|
15237
|
-
"
|
|
15238
|
-
"
|
|
15764
|
+
const PYTHON_KEYWORDS = new Set([
|
|
15765
|
+
"False",
|
|
15766
|
+
"None",
|
|
15767
|
+
"True",
|
|
15768
|
+
"and",
|
|
15769
|
+
"as",
|
|
15770
|
+
"assert",
|
|
15771
|
+
"async",
|
|
15772
|
+
"await",
|
|
15773
|
+
"break",
|
|
15774
|
+
"class",
|
|
15775
|
+
"continue",
|
|
15776
|
+
"def",
|
|
15777
|
+
"del",
|
|
15778
|
+
"elif",
|
|
15779
|
+
"else",
|
|
15780
|
+
"except",
|
|
15781
|
+
"finally",
|
|
15782
|
+
"for",
|
|
15783
|
+
"from",
|
|
15784
|
+
"global",
|
|
15785
|
+
"if",
|
|
15786
|
+
"import",
|
|
15787
|
+
"in",
|
|
15788
|
+
"is",
|
|
15789
|
+
"lambda",
|
|
15790
|
+
"nonlocal",
|
|
15791
|
+
"not",
|
|
15792
|
+
"or",
|
|
15793
|
+
"pass",
|
|
15794
|
+
"raise",
|
|
15795
|
+
"return",
|
|
15796
|
+
"try",
|
|
15797
|
+
"while",
|
|
15798
|
+
"with",
|
|
15799
|
+
"yield",
|
|
15800
|
+
"match",
|
|
15801
|
+
"case"
|
|
15239
15802
|
]);
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15803
|
+
function isValidPythonIdentifier(value) {
|
|
15804
|
+
return IDENTIFIER_RE.test(value) && !PYTHON_KEYWORDS.has(value);
|
|
15805
|
+
}
|
|
15806
|
+
function isParserGeneratedComponentName(name) {
|
|
15807
|
+
if (!name.startsWith("$")) return false;
|
|
15808
|
+
const suffix = name.slice(1);
|
|
15809
|
+
return suffix.length > 0 && isValidPythonIdentifier(suffix);
|
|
15810
|
+
}
|
|
15811
|
+
function pushIssue(issues, path, message) {
|
|
15812
|
+
issues.push({
|
|
15813
|
+
path,
|
|
15814
|
+
message
|
|
15815
|
+
});
|
|
15816
|
+
}
|
|
15817
|
+
function validateComponentName(value, path, issues) {
|
|
15818
|
+
if (void 0 === value || "string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
|
|
15819
|
+
if (isParserGeneratedComponentName(value)) return;
|
|
15820
|
+
if (!IDENTIFIER_RE.test(value)) return void pushIssue(issues, path, "must be a valid identifier");
|
|
15821
|
+
if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15822
|
+
}
|
|
15823
|
+
const componentTypeSpec_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15824
|
+
const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
|
|
15825
|
+
const componentTypeSpec_PYTHON_KEYWORDS = new Set([
|
|
15826
|
+
"False",
|
|
15827
|
+
"None",
|
|
15828
|
+
"True",
|
|
15829
|
+
"and",
|
|
15830
|
+
"as",
|
|
15831
|
+
"assert",
|
|
15832
|
+
"async",
|
|
15833
|
+
"await",
|
|
15834
|
+
"break",
|
|
15835
|
+
"class",
|
|
15836
|
+
"continue",
|
|
15837
|
+
"def",
|
|
15838
|
+
"del",
|
|
15839
|
+
"elif",
|
|
15840
|
+
"else",
|
|
15841
|
+
"except",
|
|
15842
|
+
"finally",
|
|
15843
|
+
"for",
|
|
15844
|
+
"from",
|
|
15845
|
+
"global",
|
|
15846
|
+
"if",
|
|
15847
|
+
"import",
|
|
15848
|
+
"in",
|
|
15849
|
+
"is",
|
|
15850
|
+
"lambda",
|
|
15851
|
+
"nonlocal",
|
|
15852
|
+
"not",
|
|
15853
|
+
"or",
|
|
15854
|
+
"pass",
|
|
15855
|
+
"raise",
|
|
15856
|
+
"return",
|
|
15857
|
+
"try",
|
|
15858
|
+
"while",
|
|
15859
|
+
"with",
|
|
15860
|
+
"yield",
|
|
15861
|
+
"match",
|
|
15862
|
+
"case"
|
|
15243
15863
|
]);
|
|
15244
|
-
const
|
|
15864
|
+
const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)";
|
|
15865
|
+
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)";
|
|
15866
|
+
function isValidIdentifierSegment(value) {
|
|
15867
|
+
return value.length > 0 && componentTypeSpec_IDENTIFIER_RE.test(value) && !componentTypeSpec_PYTHON_KEYWORDS.has(value);
|
|
15868
|
+
}
|
|
15869
|
+
function isValidLegacyFormTypeReference(typeName) {
|
|
15870
|
+
if (!typeName.startsWith("form:")) return false;
|
|
15871
|
+
const rest = typeName.slice(5);
|
|
15872
|
+
if (0 === rest.length) return false;
|
|
15873
|
+
let classPath = rest;
|
|
15874
|
+
if (rest.startsWith("dep_")) {
|
|
15875
|
+
const splitIndex = rest.indexOf(":");
|
|
15876
|
+
if (splitIndex <= 0) return false;
|
|
15877
|
+
const depId = rest.slice(0, splitIndex);
|
|
15878
|
+
if (!DEP_ID_RE.test(depId)) return false;
|
|
15879
|
+
classPath = rest.slice(splitIndex + 1);
|
|
15880
|
+
}
|
|
15881
|
+
if (0 === classPath.length) return false;
|
|
15882
|
+
return classPath.split(".").every((part)=>isValidIdentifierSegment(part));
|
|
15883
|
+
}
|
|
15884
|
+
function isValidPackageQualifiedComponentType(typeName) {
|
|
15885
|
+
if (typeName.startsWith("anvil.")) return false;
|
|
15886
|
+
if (!typeName.includes(".")) return false;
|
|
15887
|
+
const parts = typeName.split(".");
|
|
15888
|
+
if (parts.length < 2) return false;
|
|
15889
|
+
return parts.every((part)=>isValidIdentifierSegment(part));
|
|
15890
|
+
}
|
|
15891
|
+
function componentTypeSpec_pushIssue(issues, path, message) {
|
|
15892
|
+
issues.push({
|
|
15893
|
+
path,
|
|
15894
|
+
message
|
|
15895
|
+
});
|
|
15896
|
+
}
|
|
15897
|
+
function validateComponentTypeSpec(value, path, issues, allowedSingleSegmentTypes) {
|
|
15898
|
+
if ("string" != typeof value || 0 === value.length) return void componentTypeSpec_pushIssue(issues, path, "must be a non-empty string");
|
|
15899
|
+
const typeName = value;
|
|
15900
|
+
if (typeName.startsWith("form:")) {
|
|
15901
|
+
if (!isValidLegacyFormTypeReference(typeName)) componentTypeSpec_pushIssue(issues, path, LEGACY_FORM_TYPE_REFERENCE_MESSAGE);
|
|
15902
|
+
return;
|
|
15903
|
+
}
|
|
15904
|
+
if (isValidPackageQualifiedComponentType(typeName)) return;
|
|
15905
|
+
if (!isValidIdentifierSegment(typeName)) return void componentTypeSpec_pushIssue(issues, path, "must be a valid identifier");
|
|
15906
|
+
if (!allowedSingleSegmentTypes.has(typeName)) componentTypeSpec_pushIssue(issues, path, COMPONENT_TYPE_REFERENCE_MESSAGE);
|
|
15907
|
+
}
|
|
15908
|
+
const formTemplateValidation_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15909
|
+
const formTemplateValidation_PYTHON_KEYWORDS = new Set([
|
|
15245
15910
|
"False",
|
|
15246
15911
|
"None",
|
|
15247
15912
|
"True",
|
|
@@ -15295,58 +15960,29 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15295
15960
|
"padding",
|
|
15296
15961
|
"spacing"
|
|
15297
15962
|
]);
|
|
15298
|
-
function isValidFormReference(typeName) {
|
|
15299
|
-
if (!typeName.startsWith("form:")) return false;
|
|
15300
|
-
const rest = typeName.slice(5);
|
|
15301
|
-
if (0 === rest.length) return false;
|
|
15302
|
-
let classPath = rest;
|
|
15303
|
-
if (rest.startsWith("dep_")) {
|
|
15304
|
-
const splitIndex = rest.indexOf(":");
|
|
15305
|
-
if (splitIndex <= 0) return false;
|
|
15306
|
-
const depId = rest.slice(0, splitIndex);
|
|
15307
|
-
if (!DEP_ID_RE.test(depId)) return false;
|
|
15308
|
-
classPath = rest.slice(splitIndex + 1);
|
|
15309
|
-
}
|
|
15310
|
-
if (0 === classPath.length) return false;
|
|
15311
|
-
return classPath.split(".").every((part)=>IDENTIFIER_RE.test(part));
|
|
15312
|
-
}
|
|
15313
|
-
function isBuiltinComponentType(typeName) {
|
|
15314
|
-
return BUILTIN_COMPONENT_TYPES.has(typeName);
|
|
15315
|
-
}
|
|
15316
15963
|
function isPlainObject(value) {
|
|
15317
15964
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
15318
15965
|
}
|
|
15319
|
-
function
|
|
15966
|
+
function formTemplateValidation_pushIssue(issues, path, message) {
|
|
15320
15967
|
issues.push({
|
|
15321
15968
|
path,
|
|
15322
15969
|
message
|
|
15323
15970
|
});
|
|
15324
15971
|
}
|
|
15325
|
-
function formatValidationPath(path) {
|
|
15326
|
-
return path || "root";
|
|
15327
|
-
}
|
|
15328
15972
|
function validateIdentifier(value, path, issues) {
|
|
15329
|
-
if ("string" != typeof value || 0 === value.length) return void
|
|
15330
|
-
if (!
|
|
15331
|
-
if (
|
|
15973
|
+
if ("string" != typeof value || 0 === value.length) return void formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
|
|
15974
|
+
if (!formTemplateValidation_IDENTIFIER_RE.test(value)) return void formTemplateValidation_pushIssue(issues, path, "must be a valid identifier");
|
|
15975
|
+
if (formTemplateValidation_PYTHON_KEYWORDS.has(value)) formTemplateValidation_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15332
15976
|
}
|
|
15333
15977
|
function validateComponentType(value, path, issues) {
|
|
15334
|
-
|
|
15335
|
-
const typeName = value;
|
|
15336
|
-
if (typeName.startsWith("form:")) {
|
|
15337
|
-
if (!isValidFormReference(typeName)) pushIssue(issues, path, "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)");
|
|
15338
|
-
return;
|
|
15339
|
-
}
|
|
15340
|
-
if (!IDENTIFIER_RE.test(typeName)) return void pushIssue(issues, path, "must be a valid identifier");
|
|
15341
|
-
if (PYTHON_KEYWORDS.has(typeName)) return void pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15342
|
-
if (!isBuiltinComponentType(typeName)) pushIssue(issues, path, "must be a known Anvil component type (see componentIcons.ts) or a form: reference");
|
|
15978
|
+
validateComponentTypeSpec(value, path, issues, BUILTIN_COMPONENT_TYPES);
|
|
15343
15979
|
}
|
|
15344
15980
|
function validatePropertiesObject(value, path, issues) {
|
|
15345
|
-
if (void 0 !== value && !isPlainObject(value))
|
|
15981
|
+
if (void 0 !== value && !isPlainObject(value)) formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15346
15982
|
}
|
|
15347
15983
|
function validateEventBindingsObject(value, path, issues) {
|
|
15348
15984
|
if (void 0 === value) return;
|
|
15349
|
-
if (!isPlainObject(value)) return void
|
|
15985
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15350
15986
|
for (const [eventName, handlerName] of Object.entries(value)){
|
|
15351
15987
|
validateIdentifier(eventName, `${path}.${eventName}.name`, issues);
|
|
15352
15988
|
validateIdentifier(handlerName, `${path}.${eventName}`, issues);
|
|
@@ -15354,13 +15990,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15354
15990
|
}
|
|
15355
15991
|
function validateDataBindings(value, path, issues) {
|
|
15356
15992
|
if (void 0 === value) return;
|
|
15357
|
-
if (!Array.isArray(value)) return void
|
|
15993
|
+
if (!Array.isArray(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an array");
|
|
15358
15994
|
value.forEach((binding, index)=>{
|
|
15359
15995
|
const bindingPath = `${path}[${index}]`;
|
|
15360
|
-
if (!isPlainObject(binding)) return void
|
|
15361
|
-
if ("string" != typeof binding.property || 0 === binding.property.length)
|
|
15362
|
-
if ("string" != typeof binding.code || 0 === binding.code.length)
|
|
15363
|
-
if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback)
|
|
15996
|
+
if (!isPlainObject(binding)) return void formTemplateValidation_pushIssue(issues, bindingPath, "must be an object");
|
|
15997
|
+
if ("string" != typeof binding.property || 0 === binding.property.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
|
|
15998
|
+
if ("string" != typeof binding.code || 0 === binding.code.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
|
|
15999
|
+
if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) formTemplateValidation_pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
|
|
15364
16000
|
});
|
|
15365
16001
|
}
|
|
15366
16002
|
function validateComponentArray(components, pathPrefix, issues, seenNames) {
|
|
@@ -15368,146 +16004,563 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15368
16004
|
const childPath = `${pathPrefix}[${index}]`;
|
|
15369
16005
|
validateComponentNode(component, childPath, issues, seenNames);
|
|
15370
16006
|
if (isPlainObject(component) && "string" == typeof component.name && component.name.length > 0) {
|
|
15371
|
-
if (seenNames.has(component.name))
|
|
16007
|
+
if (seenNames.has(component.name)) formTemplateValidation_pushIssue(issues, `${childPath}.name`, "must be unique across form components");
|
|
15372
16008
|
seenNames.add(component.name);
|
|
15373
16009
|
}
|
|
15374
16010
|
});
|
|
15375
16011
|
}
|
|
15376
16012
|
function validateComponentNode(value, path, issues, seenNames) {
|
|
15377
|
-
if (!isPlainObject(value)) return void
|
|
16013
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15378
16014
|
validateComponentType(value.type, `${path}.type`, issues);
|
|
15379
|
-
|
|
15380
|
-
else validateIdentifier(value.name, `${path}.name`, issues);
|
|
16015
|
+
validateComponentName(value.name, `${path}.name`, issues);
|
|
15381
16016
|
validatePropertiesObject(value.properties, `${path}.properties`, issues);
|
|
15382
16017
|
validatePropertiesObject(value.layout_properties, `${path}.layout_properties`, issues);
|
|
15383
16018
|
validateEventBindingsObject(value.event_bindings, `${path}.event_bindings`, issues);
|
|
15384
16019
|
validateDataBindings(value.data_bindings, `${path}.data_bindings`, issues);
|
|
15385
16020
|
if (void 0 !== value.components) if (Array.isArray(value.components)) validateComponentArray(value.components, `${path}.components`, issues, seenNames);
|
|
15386
|
-
else
|
|
16021
|
+
else formTemplateValidation_pushIssue(issues, `${path}.components`, "must be an array");
|
|
15387
16022
|
}
|
|
15388
16023
|
function validatePropertyDefinition(value, path, issues) {
|
|
15389
|
-
if (!isPlainObject(value)) return void
|
|
16024
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15390
16025
|
validateIdentifier(value.name, `${path}.name`, issues);
|
|
15391
|
-
if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type))
|
|
15392
|
-
if (void 0 !== value.description && "string" != typeof value.description)
|
|
15393
|
-
if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop)
|
|
15394
|
-
if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback)
|
|
16026
|
+
if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type)) formTemplateValidation_pushIssue(issues, `${path}.type`, "must be a supported property type");
|
|
16027
|
+
if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
|
|
16028
|
+
if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop) formTemplateValidation_pushIssue(issues, `${path}.default_binding_prop`, "must be a boolean");
|
|
16029
|
+
if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback) formTemplateValidation_pushIssue(issues, `${path}.allow_binding_writeback`, "must be a boolean");
|
|
15395
16030
|
if (void 0 !== value.binding_writeback_events) if (Array.isArray(value.binding_writeback_events)) value.binding_writeback_events.forEach((eventName, index)=>{
|
|
15396
|
-
if ("string" != typeof eventName || 0 === eventName.length)
|
|
16031
|
+
if ("string" != typeof eventName || 0 === eventName.length) formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events[${index}]`, "must be a non-empty string");
|
|
15397
16032
|
});
|
|
15398
|
-
else
|
|
16033
|
+
else formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events`, "must be an array");
|
|
15399
16034
|
if ("enum" === value.type) {
|
|
15400
|
-
if (!Array.isArray(value.options) || value.options.some((option)=>"string" != typeof option))
|
|
16035
|
+
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");
|
|
15401
16036
|
}
|
|
15402
16037
|
}
|
|
15403
16038
|
function validateEventDefinition(value, path, issues) {
|
|
15404
|
-
if (!isPlainObject(value)) return void
|
|
16039
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
15405
16040
|
validateIdentifier(value.name, `${path}.name`, issues);
|
|
15406
|
-
if (void 0 !== value.description && "string" != typeof value.description)
|
|
15407
|
-
if (void 0 !== value.default_event && "boolean" != typeof value.default_event)
|
|
16041
|
+
if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
|
|
16042
|
+
if (void 0 !== value.default_event && "boolean" != typeof value.default_event) formTemplateValidation_pushIssue(issues, `${path}.default_event`, "must be a boolean");
|
|
15408
16043
|
if (void 0 !== value.parameters) if (Array.isArray(value.parameters)) value.parameters.forEach((parameter, index)=>{
|
|
15409
|
-
if (!isPlainObject(parameter)) return void
|
|
16044
|
+
if (!isPlainObject(parameter)) return void formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}]`, "must be an object");
|
|
15410
16045
|
validateIdentifier(parameter.name, `${path}.parameters[${index}].name`, issues);
|
|
15411
|
-
if (void 0 !== parameter.description && "string" != typeof parameter.description)
|
|
16046
|
+
if (void 0 !== parameter.description && "string" != typeof parameter.description) formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}].description`, "must be a string");
|
|
15412
16047
|
});
|
|
15413
|
-
else
|
|
16048
|
+
else formTemplateValidation_pushIssue(issues, `${path}.parameters`, "must be an array");
|
|
15414
16049
|
}
|
|
15415
16050
|
function validateSlotDefinition(value, path, issues, seenNames) {
|
|
15416
|
-
if (!isPlainObject(value)) return void
|
|
15417
|
-
if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0)
|
|
16051
|
+
if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
|
|
16052
|
+
if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0) formTemplateValidation_pushIssue(issues, `${path}.index`, "must be a non-negative integer");
|
|
15418
16053
|
validatePropertiesObject(value.set_layout_properties, `${path}.set_layout_properties`, issues);
|
|
15419
16054
|
if (isPlainObject(value.target)) {
|
|
15420
|
-
if ("container" !== value.target.type && "slot" !== value.target.type)
|
|
15421
|
-
validateIdentifier(value.target.name, `${path}.target.name`, issues);
|
|
15422
|
-
} else
|
|
16055
|
+
if ("container" !== value.target.type && "slot" !== value.target.type) formTemplateValidation_pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
|
|
16056
|
+
if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
|
|
16057
|
+
} else formTemplateValidation_pushIssue(issues, `${path}.target`, "must be an object");
|
|
15423
16058
|
if (void 0 !== value.template) validateComponentNode(value.template, `${path}.template`, issues, seenNames);
|
|
15424
16059
|
}
|
|
16060
|
+
function prefixedPath(pathPrefix, path) {
|
|
16061
|
+
return pathPrefix.length > 0 ? `${pathPrefix}.${path}` : path;
|
|
16062
|
+
}
|
|
16063
|
+
function validateFormTemplateMetadata(value, options = {}) {
|
|
16064
|
+
const issues = [];
|
|
16065
|
+
if (!isPlainObject(value)) {
|
|
16066
|
+
formTemplateValidation_pushIssue(issues, options.pathPrefix ?? "", "must be a YAML object");
|
|
16067
|
+
return issues;
|
|
16068
|
+
}
|
|
16069
|
+
const pathPrefix = options.pathPrefix ?? "";
|
|
16070
|
+
const path = (suffix)=>prefixedPath(pathPrefix, suffix);
|
|
16071
|
+
if (void 0 !== value.custom_component && "boolean" != typeof value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component"), "must be a boolean");
|
|
16072
|
+
if (void 0 !== value.custom_component_container && "boolean" != typeof value.custom_component_container) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "must be a boolean");
|
|
16073
|
+
if (true === value.custom_component_container && true !== value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "requires custom_component: true");
|
|
16074
|
+
const properties = value.properties;
|
|
16075
|
+
if (void 0 !== properties) if (Array.isArray(properties)) {
|
|
16076
|
+
const seenNames = new Set();
|
|
16077
|
+
let defaultBindingPropCount = 0;
|
|
16078
|
+
properties.forEach((property, index)=>{
|
|
16079
|
+
validatePropertyDefinition(property, path(`properties[${index}]`), issues);
|
|
16080
|
+
if (!isPlainObject(property) || "string" != typeof property.name) return;
|
|
16081
|
+
if (seenNames.has(property.name)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].name`), "must be unique");
|
|
16082
|
+
seenNames.add(property.name);
|
|
16083
|
+
if (property.default_binding_prop) defaultBindingPropCount++;
|
|
16084
|
+
});
|
|
16085
|
+
if (defaultBindingPropCount > 1) formTemplateValidation_pushIssue(issues, path("properties"), "can only define one default_binding_prop");
|
|
16086
|
+
} else formTemplateValidation_pushIssue(issues, path("properties"), "must be an array");
|
|
16087
|
+
const events = value.events;
|
|
16088
|
+
const eventNames = new Set();
|
|
16089
|
+
if (void 0 !== events) if (Array.isArray(events)) {
|
|
16090
|
+
let defaultEventCount = 0;
|
|
16091
|
+
events.forEach((event, index)=>{
|
|
16092
|
+
validateEventDefinition(event, path(`events[${index}]`), issues);
|
|
16093
|
+
if (!isPlainObject(event) || "string" != typeof event.name) return;
|
|
16094
|
+
if (eventNames.has(event.name)) formTemplateValidation_pushIssue(issues, path(`events[${index}].name`), "must be unique");
|
|
16095
|
+
eventNames.add(event.name);
|
|
16096
|
+
if (event.default_event) defaultEventCount++;
|
|
16097
|
+
});
|
|
16098
|
+
if (defaultEventCount > 1) formTemplateValidation_pushIssue(issues, path("events"), "can only define one default_event");
|
|
16099
|
+
} else formTemplateValidation_pushIssue(issues, path("events"), "must be an array");
|
|
16100
|
+
if (Array.isArray(properties)) properties.forEach((property, index)=>{
|
|
16101
|
+
if (!isPlainObject(property) || !Array.isArray(property.binding_writeback_events)) return;
|
|
16102
|
+
property.binding_writeback_events.forEach((eventName, eventIndex)=>{
|
|
16103
|
+
if ("string" == typeof eventName && !eventNames.has(eventName)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].binding_writeback_events[${eventIndex}]`), "must reference a defined event");
|
|
16104
|
+
});
|
|
16105
|
+
});
|
|
16106
|
+
if (void 0 !== value.slots) {
|
|
16107
|
+
const slotsPath = path("slots");
|
|
16108
|
+
if (isPlainObject(value.slots)) if ("empty-marker" === options.slotsPolicy) {
|
|
16109
|
+
if (Object.keys(value.slots).length > 0) formTemplateValidation_pushIssue(issues, slotsPath, "must be empty when defined in frontmatter");
|
|
16110
|
+
} else {
|
|
16111
|
+
const componentNames = options.componentNames ?? new Set();
|
|
16112
|
+
for (const [slotName, slotDef] of Object.entries(value.slots)){
|
|
16113
|
+
validateIdentifier(slotName, path(`slots.${slotName}.name`), issues);
|
|
16114
|
+
validateSlotDefinition(slotDef, path(`slots.${slotName}`), issues, componentNames);
|
|
16115
|
+
}
|
|
16116
|
+
}
|
|
16117
|
+
else formTemplateValidation_pushIssue(issues, slotsPath, "must be an object");
|
|
16118
|
+
}
|
|
16119
|
+
if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
|
|
16120
|
+
if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
|
|
16121
|
+
if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
|
|
16122
|
+
if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
|
|
16123
|
+
} else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
|
|
16124
|
+
return issues;
|
|
16125
|
+
}
|
|
15425
16126
|
function validateFormTemplateData(value) {
|
|
15426
16127
|
const issues = [];
|
|
15427
16128
|
const componentNames = new Set();
|
|
15428
16129
|
if (!isPlainObject(value)) {
|
|
15429
|
-
|
|
16130
|
+
formTemplateValidation_pushIssue(issues, "", "must be a YAML object");
|
|
15430
16131
|
return issues;
|
|
15431
16132
|
}
|
|
15432
16133
|
const hasContainer = void 0 !== value.container;
|
|
15433
16134
|
const hasLayout = void 0 !== value.layout;
|
|
15434
16135
|
const hasComponents = void 0 !== value.components;
|
|
15435
16136
|
const hasComponentsBySlot = void 0 !== value.components_by_slot;
|
|
15436
|
-
if (hasContainer && hasLayout)
|
|
15437
|
-
if (!hasContainer && !hasLayout)
|
|
15438
|
-
if (void 0 !== value.is_package && "boolean" != typeof value.is_package)
|
|
16137
|
+
if (hasContainer && hasLayout) formTemplateValidation_pushIssue(issues, "", "cannot define both container and layout");
|
|
16138
|
+
if (!hasContainer && !hasLayout) formTemplateValidation_pushIssue(issues, "", "must define either container or layout");
|
|
16139
|
+
if (void 0 !== value.is_package && "boolean" != typeof value.is_package) formTemplateValidation_pushIssue(issues, "is_package", "must be a boolean");
|
|
15439
16140
|
if (hasContainer) if (isPlainObject(value.container)) {
|
|
15440
16141
|
validateComponentType(value.container.type, "container.type", issues);
|
|
15441
16142
|
validatePropertiesObject(value.container.properties, "container.properties", issues);
|
|
15442
16143
|
validatePropertiesObject(value.container.layout_properties, "container.layout_properties", issues);
|
|
15443
16144
|
validateEventBindingsObject(value.container.event_bindings, "container.event_bindings", issues);
|
|
15444
16145
|
validateDataBindings(value.container.data_bindings, "container.data_bindings", issues);
|
|
15445
|
-
} else
|
|
16146
|
+
} else formTemplateValidation_pushIssue(issues, "container", "must be an object");
|
|
15446
16147
|
if (hasLayout) if (isPlainObject(value.layout)) {
|
|
15447
16148
|
validateComponentType(value.layout.type, "layout.type", issues);
|
|
15448
16149
|
validatePropertiesObject(value.layout.properties, "layout.properties", issues);
|
|
15449
16150
|
validateEventBindingsObject(value.layout.event_bindings, "layout.event_bindings", issues);
|
|
15450
16151
|
validateEventBindingsObject(value.layout.form_event_bindings, "layout.form_event_bindings", issues);
|
|
15451
16152
|
validateDataBindings(value.layout.data_bindings, "layout.data_bindings", issues);
|
|
15452
|
-
} else
|
|
15453
|
-
if (hasComponents && hasLayout)
|
|
15454
|
-
else if (hasComponents && !hasContainer)
|
|
16153
|
+
} else formTemplateValidation_pushIssue(issues, "layout", "must be an object");
|
|
16154
|
+
if (hasComponents && hasLayout) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
|
|
16155
|
+
else if (hasComponents && !hasContainer) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
|
|
15455
16156
|
else if (hasComponents) if (Array.isArray(value.components)) validateComponentArray(value.components, "components", issues, componentNames);
|
|
15456
|
-
else
|
|
15457
|
-
if (hasComponentsBySlot && hasContainer)
|
|
15458
|
-
else if (hasComponentsBySlot && !hasLayout)
|
|
16157
|
+
else formTemplateValidation_pushIssue(issues, "components", "must be an array");
|
|
16158
|
+
if (hasComponentsBySlot && hasContainer) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
|
|
16159
|
+
else if (hasComponentsBySlot && !hasLayout) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
|
|
15459
16160
|
else if (hasComponentsBySlot) if (isPlainObject(value.components_by_slot)) for (const [slotName, slotComponents] of Object.entries(value.components_by_slot)){
|
|
15460
16161
|
if (!Array.isArray(slotComponents)) {
|
|
15461
|
-
|
|
16162
|
+
formTemplateValidation_pushIssue(issues, `components_by_slot.${slotName}`, "must be an array");
|
|
15462
16163
|
continue;
|
|
15463
16164
|
}
|
|
15464
16165
|
validateComponentArray(slotComponents, `components_by_slot.${slotName}`, issues, componentNames);
|
|
15465
16166
|
}
|
|
15466
|
-
else
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15474
|
-
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
16167
|
+
else formTemplateValidation_pushIssue(issues, "components_by_slot", "must be an object");
|
|
16168
|
+
issues.push(...validateFormTemplateMetadata(value, {
|
|
16169
|
+
componentNames
|
|
16170
|
+
}));
|
|
16171
|
+
return issues;
|
|
16172
|
+
}
|
|
16173
|
+
const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
|
|
16174
|
+
"/runtime/services/tables.yml",
|
|
16175
|
+
"/runtime/services/google.yml",
|
|
16176
|
+
"/runtime/services/uplink.yml",
|
|
16177
|
+
"/runtime/services/stripe.yml",
|
|
16178
|
+
"/runtime/services/segment.yml",
|
|
16179
|
+
"/runtime/services/facebook.yml",
|
|
16180
|
+
"/runtime/services/anvil/users.yml",
|
|
16181
|
+
"/runtime/services/anvil/secrets.yml",
|
|
16182
|
+
"/runtime/services/anvil/saml.yml",
|
|
16183
|
+
"/runtime/services/anvil/microsoft.yml",
|
|
16184
|
+
"/runtime/services/anvil/email.yml",
|
|
16185
|
+
"/runtime/services/anvil/files.yml"
|
|
16186
|
+
]);
|
|
16187
|
+
const FRONTMATTER_ALLOWED_KEYS = new Set([
|
|
16188
|
+
"custom_component",
|
|
16189
|
+
"custom_component_container",
|
|
16190
|
+
"properties",
|
|
16191
|
+
"events",
|
|
16192
|
+
"toolbox_item",
|
|
16193
|
+
"layout_metadata",
|
|
16194
|
+
"item_type",
|
|
16195
|
+
"slots"
|
|
16196
|
+
]);
|
|
16197
|
+
const FRONTMATTER_DISALLOWED_KEYS = new Set([
|
|
16198
|
+
"container",
|
|
16199
|
+
"components",
|
|
16200
|
+
"layout",
|
|
16201
|
+
"components_by_slot",
|
|
16202
|
+
"code",
|
|
16203
|
+
"class_name",
|
|
16204
|
+
"is_package",
|
|
16205
|
+
"save_as_html",
|
|
16206
|
+
"serialized_html"
|
|
16207
|
+
]);
|
|
16208
|
+
function validateFormTemplateHtml_isPlainObject(value) {
|
|
16209
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
16210
|
+
}
|
|
16211
|
+
function validateFormTemplateHtml_pushIssue(issues, issuePath, message) {
|
|
16212
|
+
issues.push({
|
|
16213
|
+
path: issuePath,
|
|
16214
|
+
message
|
|
16215
|
+
});
|
|
16216
|
+
}
|
|
16217
|
+
function splitHtmlFormTemplateFrontmatter(htmlContent) {
|
|
16218
|
+
const match = htmlContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
16219
|
+
if (!match) return {
|
|
16220
|
+
frontmatter: null,
|
|
16221
|
+
body: htmlContent
|
|
16222
|
+
};
|
|
16223
|
+
return {
|
|
16224
|
+
frontmatter: match[1],
|
|
16225
|
+
body: htmlContent.slice(match[0].length)
|
|
16226
|
+
};
|
|
16227
|
+
}
|
|
16228
|
+
function validateHtmlFrontmatter(frontmatterYaml) {
|
|
16229
|
+
const issues = [];
|
|
16230
|
+
if (0 === frontmatterYaml.trim().length) {
|
|
16231
|
+
validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must not be empty; omit frontmatter entirely when unused");
|
|
16232
|
+
return issues;
|
|
16233
|
+
}
|
|
16234
|
+
let data;
|
|
16235
|
+
try {
|
|
16236
|
+
data = external_js_yaml_namespaceObject.load(frontmatterYaml);
|
|
16237
|
+
} catch (error) {
|
|
16238
|
+
validateFormTemplateHtml_pushIssue(issues, "frontmatter", error instanceof Error ? error.message : String(error));
|
|
16239
|
+
return issues;
|
|
16240
|
+
}
|
|
16241
|
+
if (!validateFormTemplateHtml_isPlainObject(data)) {
|
|
16242
|
+
validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must be a YAML object");
|
|
16243
|
+
return issues;
|
|
16244
|
+
}
|
|
16245
|
+
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");
|
|
16246
|
+
else if (!FRONTMATTER_ALLOWED_KEYS.has(key)) validateFormTemplateHtml_pushIssue(issues, `frontmatter.${key}`, "is not an allowed frontmatter field");
|
|
16247
|
+
issues.push(...validateFormTemplateMetadata(data, {
|
|
16248
|
+
pathPrefix: "frontmatter",
|
|
16249
|
+
slotsPolicy: "empty-marker"
|
|
16250
|
+
}));
|
|
16251
|
+
return issues;
|
|
16252
|
+
}
|
|
16253
|
+
function validateParsedHtmlTemplate(parsed) {
|
|
16254
|
+
return validateFormTemplateData(parsed);
|
|
16255
|
+
}
|
|
16256
|
+
function validateFormTemplateHtml(htmlContent) {
|
|
16257
|
+
const { frontmatter, body } = splitHtmlFormTemplateFrontmatter(htmlContent);
|
|
16258
|
+
if (null !== frontmatter) {
|
|
16259
|
+
const frontmatterIssues = validateHtmlFrontmatter(frontmatter);
|
|
16260
|
+
if (frontmatterIssues.length > 0) return {
|
|
16261
|
+
ok: false,
|
|
16262
|
+
message: "form_template.html is invalid for Anvil",
|
|
16263
|
+
issues: frontmatterIssues
|
|
16264
|
+
};
|
|
16265
|
+
}
|
|
16266
|
+
let parsed;
|
|
16267
|
+
try {
|
|
16268
|
+
parsed = (0, form_template_parser_namespaceObject.parseSerializedHtml)(body);
|
|
16269
|
+
} catch (error) {
|
|
16270
|
+
return {
|
|
16271
|
+
ok: false,
|
|
16272
|
+
message: "form_template.html could not be parsed",
|
|
16273
|
+
issues: [
|
|
16274
|
+
{
|
|
16275
|
+
path: "",
|
|
16276
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16277
|
+
}
|
|
16278
|
+
]
|
|
16279
|
+
};
|
|
16280
|
+
}
|
|
16281
|
+
const issues = validateParsedHtmlTemplate(parsed);
|
|
16282
|
+
if (issues.length > 0) return {
|
|
16283
|
+
ok: false,
|
|
16284
|
+
message: "form_template.html is invalid for Anvil",
|
|
16285
|
+
issues
|
|
16286
|
+
};
|
|
16287
|
+
return {
|
|
16288
|
+
ok: true
|
|
16289
|
+
};
|
|
16290
|
+
}
|
|
16291
|
+
const PYTHON_VALIDATOR_SCRIPT = String.raw`
|
|
16292
|
+
import ast
|
|
16293
|
+
import json
|
|
16294
|
+
import sys
|
|
16295
|
+
|
|
16296
|
+
|
|
16297
|
+
def issue_path(error):
|
|
16298
|
+
if getattr(error, "lineno", None) is None:
|
|
16299
|
+
return "root"
|
|
16300
|
+
if getattr(error, "offset", None) is None:
|
|
16301
|
+
return f"line {error.lineno}"
|
|
16302
|
+
return f"line {error.lineno}, column {error.offset}"
|
|
16303
|
+
|
|
16304
|
+
|
|
16305
|
+
def base_name(node):
|
|
16306
|
+
if isinstance(node, ast.Name):
|
|
16307
|
+
return node.id
|
|
16308
|
+
if isinstance(node, ast.Attribute):
|
|
16309
|
+
return node.attr
|
|
16310
|
+
return None
|
|
16311
|
+
|
|
16312
|
+
|
|
16313
|
+
def imports_template_from_anvil_designer(node, template_name):
|
|
16314
|
+
if node.level < 1:
|
|
16315
|
+
return False
|
|
16316
|
+
if node.module not in ("_anvil_designer", None):
|
|
16317
|
+
return False
|
|
16318
|
+
return any(alias.name == template_name for alias in node.names)
|
|
16319
|
+
|
|
16320
|
+
|
|
16321
|
+
file_path = sys.argv[1]
|
|
16322
|
+
class_name = sys.argv[2]
|
|
16323
|
+
template_name = f"{class_name}Template" if class_name else ""
|
|
16324
|
+
source = sys.stdin.read()
|
|
16325
|
+
issues = []
|
|
16326
|
+
|
|
16327
|
+
try:
|
|
16328
|
+
compile(source, file_path, "exec")
|
|
16329
|
+
except SyntaxError as error:
|
|
16330
|
+
issues.append({"path": issue_path(error), "message": error.msg})
|
|
16331
|
+
print(json.dumps({"issues": issues}))
|
|
16332
|
+
sys.exit(0)
|
|
16333
|
+
|
|
16334
|
+
if class_name:
|
|
16335
|
+
tree = ast.parse(source, filename=file_path)
|
|
16336
|
+
has_template_import = False
|
|
16337
|
+
has_form_class = False
|
|
16338
|
+
|
|
16339
|
+
for node in ast.walk(tree):
|
|
16340
|
+
if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
|
|
16341
|
+
has_template_import = True
|
|
16342
|
+
elif isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
16343
|
+
if any(base_name(base) == template_name for base in node.bases):
|
|
16344
|
+
has_form_class = True
|
|
16345
|
+
|
|
16346
|
+
if not has_template_import:
|
|
16347
|
+
issues.append({
|
|
16348
|
+
"path": "root",
|
|
16349
|
+
"message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
|
|
16350
|
+
})
|
|
16351
|
+
if not has_form_class:
|
|
16352
|
+
issues.append({
|
|
16353
|
+
"path": "root",
|
|
16354
|
+
"message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
|
|
16355
|
+
})
|
|
16356
|
+
|
|
16357
|
+
print(json.dumps({"issues": issues}))
|
|
16358
|
+
`;
|
|
16359
|
+
function findPythonInterpreter(env = process.env, probe = external_child_process_namespaceObject.spawnSync) {
|
|
16360
|
+
const candidates = [
|
|
16361
|
+
..."string" == typeof env.ANVIL_PYTHON && env.ANVIL_PYTHON.length > 0 ? [
|
|
16362
|
+
{
|
|
16363
|
+
command: env.ANVIL_PYTHON,
|
|
16364
|
+
args: []
|
|
16365
|
+
}
|
|
16366
|
+
] : [],
|
|
16367
|
+
{
|
|
16368
|
+
command: "uv",
|
|
16369
|
+
args: [
|
|
16370
|
+
"run",
|
|
16371
|
+
"--isolated",
|
|
16372
|
+
"--no-python-downloads",
|
|
16373
|
+
"python"
|
|
16374
|
+
]
|
|
16375
|
+
},
|
|
16376
|
+
{
|
|
16377
|
+
command: "python3",
|
|
16378
|
+
args: []
|
|
16379
|
+
},
|
|
16380
|
+
{
|
|
16381
|
+
command: "python",
|
|
16382
|
+
args: []
|
|
16383
|
+
}
|
|
16384
|
+
];
|
|
16385
|
+
for (const candidate of candidates){
|
|
16386
|
+
const result = probe(candidate.command, [
|
|
16387
|
+
...candidate.args,
|
|
16388
|
+
"--version"
|
|
16389
|
+
], {
|
|
16390
|
+
encoding: "utf8"
|
|
15500
16391
|
});
|
|
16392
|
+
if (!result.error && 0 === result.status) return candidate;
|
|
16393
|
+
}
|
|
16394
|
+
return null;
|
|
16395
|
+
}
|
|
16396
|
+
function isClientFormPython(filePath) {
|
|
16397
|
+
if ("__init__.py" === external_path_default().basename(filePath)) {
|
|
16398
|
+
const formDir = external_path_default().dirname(filePath);
|
|
16399
|
+
return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
|
|
16400
|
+
}
|
|
16401
|
+
const parsed = external_path_default().parse(filePath);
|
|
16402
|
+
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`));
|
|
16403
|
+
}
|
|
16404
|
+
function expectedFormClassName(filePath) {
|
|
16405
|
+
if (!isClientFormPython(filePath)) return null;
|
|
16406
|
+
if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
|
|
16407
|
+
return external_path_default().basename(filePath, ".py");
|
|
16408
|
+
}
|
|
16409
|
+
function isUnderDirectory(filePath, directoryPath) {
|
|
16410
|
+
const relativePath = external_path_default().relative(directoryPath, filePath);
|
|
16411
|
+
return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
|
|
16412
|
+
}
|
|
16413
|
+
function validatePython(filePath, fileContent, appRoot) {
|
|
16414
|
+
const interpreter = findPythonInterpreter();
|
|
16415
|
+
if (!interpreter) return {
|
|
16416
|
+
ok: false,
|
|
16417
|
+
message: "Python file could not be validated",
|
|
16418
|
+
issues: [
|
|
16419
|
+
{
|
|
16420
|
+
path: "root",
|
|
16421
|
+
message: "Python validation requires uv, python3, or python on PATH, or ANVIL_PYTHON to be set"
|
|
16422
|
+
}
|
|
16423
|
+
]
|
|
16424
|
+
};
|
|
16425
|
+
const absolutePath = external_path_default().resolve(filePath);
|
|
16426
|
+
const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
|
|
16427
|
+
const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
|
|
16428
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter.command, [
|
|
16429
|
+
...interpreter.args,
|
|
16430
|
+
"-c",
|
|
16431
|
+
PYTHON_VALIDATOR_SCRIPT,
|
|
16432
|
+
filePath,
|
|
16433
|
+
formClassName ?? ""
|
|
16434
|
+
], {
|
|
16435
|
+
encoding: "utf8",
|
|
16436
|
+
input: fileContent,
|
|
16437
|
+
maxBuffer: 1048576
|
|
16438
|
+
});
|
|
16439
|
+
if (result.error || 0 !== result.status) return {
|
|
16440
|
+
ok: false,
|
|
16441
|
+
message: "Python file could not be validated",
|
|
16442
|
+
issues: [
|
|
16443
|
+
{
|
|
16444
|
+
path: "root",
|
|
16445
|
+
message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
|
|
16446
|
+
}
|
|
16447
|
+
]
|
|
16448
|
+
};
|
|
16449
|
+
let parsed;
|
|
16450
|
+
try {
|
|
16451
|
+
parsed = JSON.parse(result.stdout);
|
|
16452
|
+
} catch (error) {
|
|
16453
|
+
return {
|
|
16454
|
+
ok: false,
|
|
16455
|
+
message: "Python file could not be validated",
|
|
16456
|
+
issues: [
|
|
16457
|
+
{
|
|
16458
|
+
path: "root",
|
|
16459
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16460
|
+
}
|
|
16461
|
+
]
|
|
16462
|
+
};
|
|
16463
|
+
}
|
|
16464
|
+
const issues = parsed.issues ?? [];
|
|
16465
|
+
if (issues.length > 0) return {
|
|
16466
|
+
ok: false,
|
|
16467
|
+
message: "Python file is invalid for Anvil",
|
|
16468
|
+
issues
|
|
16469
|
+
};
|
|
16470
|
+
return {
|
|
16471
|
+
ok: true
|
|
16472
|
+
};
|
|
16473
|
+
}
|
|
16474
|
+
const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16475
|
+
const DB_ACCESS_LEVELS = new Set([
|
|
16476
|
+
"none",
|
|
16477
|
+
"search",
|
|
16478
|
+
"full"
|
|
16479
|
+
]);
|
|
16480
|
+
const DB_BASIC_COLUMN_TYPES = new Set([
|
|
16481
|
+
"string",
|
|
16482
|
+
"number",
|
|
16483
|
+
"bool",
|
|
16484
|
+
"date",
|
|
16485
|
+
"datetime",
|
|
16486
|
+
"media",
|
|
16487
|
+
"simpleObject"
|
|
16488
|
+
]);
|
|
16489
|
+
const DB_LINK_COLUMN_TYPES = new Set([
|
|
16490
|
+
"link_single",
|
|
16491
|
+
"link_multiple"
|
|
16492
|
+
]);
|
|
16493
|
+
const DB_TABLE_INDEX_TYPES = new Set([
|
|
16494
|
+
"b_tree",
|
|
16495
|
+
"trigram",
|
|
16496
|
+
"full_text"
|
|
16497
|
+
]);
|
|
16498
|
+
const SCHEDULE_EVERY_VALUES = new Set([
|
|
16499
|
+
"minute",
|
|
16500
|
+
"hour",
|
|
16501
|
+
"day",
|
|
16502
|
+
"week",
|
|
16503
|
+
"month"
|
|
16504
|
+
]);
|
|
16505
|
+
const CLIENT_VERSIONS = new Set([
|
|
16506
|
+
"2",
|
|
16507
|
+
"3"
|
|
16508
|
+
]);
|
|
16509
|
+
const validators_PYTHON_KEYWORDS = new Set([
|
|
16510
|
+
"False",
|
|
16511
|
+
"None",
|
|
16512
|
+
"True",
|
|
16513
|
+
"and",
|
|
16514
|
+
"as",
|
|
16515
|
+
"assert",
|
|
16516
|
+
"async",
|
|
16517
|
+
"await",
|
|
16518
|
+
"break",
|
|
16519
|
+
"class",
|
|
16520
|
+
"continue",
|
|
16521
|
+
"def",
|
|
16522
|
+
"del",
|
|
16523
|
+
"elif",
|
|
16524
|
+
"else",
|
|
16525
|
+
"except",
|
|
16526
|
+
"finally",
|
|
16527
|
+
"for",
|
|
16528
|
+
"from",
|
|
16529
|
+
"global",
|
|
16530
|
+
"if",
|
|
16531
|
+
"import",
|
|
16532
|
+
"in",
|
|
16533
|
+
"is",
|
|
16534
|
+
"lambda",
|
|
16535
|
+
"nonlocal",
|
|
16536
|
+
"not",
|
|
16537
|
+
"or",
|
|
16538
|
+
"pass",
|
|
16539
|
+
"raise",
|
|
16540
|
+
"return",
|
|
16541
|
+
"try",
|
|
16542
|
+
"while",
|
|
16543
|
+
"with",
|
|
16544
|
+
"yield",
|
|
16545
|
+
"match",
|
|
16546
|
+
"case"
|
|
16547
|
+
]);
|
|
16548
|
+
function validators_isPlainObject(value) {
|
|
16549
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
16550
|
+
}
|
|
16551
|
+
function validators_pushIssue(issues, path, message) {
|
|
16552
|
+
issues.push({
|
|
16553
|
+
path,
|
|
16554
|
+
message
|
|
15501
16555
|
});
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
if (
|
|
15508
|
-
|
|
15509
|
-
|
|
15510
|
-
return issues;
|
|
16556
|
+
}
|
|
16557
|
+
function formatValidationPath(path) {
|
|
16558
|
+
return path || "root";
|
|
16559
|
+
}
|
|
16560
|
+
function validators_validateIdentifier(value, path, issues) {
|
|
16561
|
+
if ("string" != typeof value || 0 === value.length) return void validators_pushIssue(issues, path, "must be a non-empty string");
|
|
16562
|
+
if (!validators_IDENTIFIER_RE.test(value)) return void validators_pushIssue(issues, path, "must be a valid identifier");
|
|
16563
|
+
if (validators_PYTHON_KEYWORDS.has(value)) validators_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15511
16564
|
}
|
|
15512
16565
|
function validateFormTemplate(yamlContent) {
|
|
15513
16566
|
try {
|
|
@@ -15534,7 +16587,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15534
16587
|
};
|
|
15535
16588
|
}
|
|
15536
16589
|
}
|
|
15537
|
-
const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>
|
|
16590
|
+
const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>validators_isPlainObject(input), "must be an object");
|
|
15538
16591
|
const AnvilServiceEntrySchema = external_valibot_namespaceObject.looseObject({
|
|
15539
16592
|
source: external_valibot_namespaceObject.pipe(external_valibot_namespaceObject.string(), external_valibot_namespaceObject.nonEmpty()),
|
|
15540
16593
|
client_config: external_valibot_namespaceObject.optional(serviceConfigObject),
|
|
@@ -15562,14 +16615,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15562
16615
|
function validateRuntimeOptions(value, path) {
|
|
15563
16616
|
const issues = [];
|
|
15564
16617
|
if (void 0 === value) return issues;
|
|
15565
|
-
if (!
|
|
16618
|
+
if (!validators_isPlainObject(value)) return issues;
|
|
15566
16619
|
if (void 0 !== value.client_version) {
|
|
15567
|
-
if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version))
|
|
16620
|
+
if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version)) validators_pushIssue(issues, `${path}.client_version`, 'must be "2" or "3"');
|
|
15568
16621
|
}
|
|
15569
16622
|
if (void 0 !== value.server_spec && null !== value.server_spec) {
|
|
15570
|
-
if (!
|
|
15571
|
-
if ("requirements" in value.server_spec)
|
|
15572
|
-
if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base)
|
|
16623
|
+
if (!validators_isPlainObject(value.server_spec)) return issues;
|
|
16624
|
+
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");
|
|
16625
|
+
if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base) validators_pushIssue(issues, `${path}.server_spec.base`, "must be a string");
|
|
15573
16626
|
}
|
|
15574
16627
|
return issues;
|
|
15575
16628
|
}
|
|
@@ -15577,27 +16630,27 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15577
16630
|
const issues = [];
|
|
15578
16631
|
if (void 0 === value) return issues;
|
|
15579
16632
|
if (null === value) {
|
|
15580
|
-
|
|
16633
|
+
validators_pushIssue(issues, path, "must be an object, an empty array, or omitted");
|
|
15581
16634
|
return issues;
|
|
15582
16635
|
}
|
|
15583
16636
|
if (Array.isArray(value)) {
|
|
15584
16637
|
if (0 === value.length) return issues;
|
|
15585
|
-
|
|
16638
|
+
validators_pushIssue(issues, path, "must be an object mapping table names to schemas, or an empty array");
|
|
15586
16639
|
return issues;
|
|
15587
16640
|
}
|
|
15588
|
-
if (!
|
|
15589
|
-
|
|
16641
|
+
if (!validators_isPlainObject(value)) {
|
|
16642
|
+
validators_pushIssue(issues, path, "must be an object mapping table names to schemas");
|
|
15590
16643
|
return issues;
|
|
15591
16644
|
}
|
|
15592
16645
|
for (const tableName of Object.keys(value)){
|
|
15593
16646
|
const tablePath = `${path}.${tableName}`;
|
|
15594
|
-
if (!
|
|
15595
|
-
|
|
16647
|
+
if (!validators_IDENTIFIER_RE.test(tableName)) {
|
|
16648
|
+
validators_pushIssue(issues, tablePath, "must be a valid Python identifier");
|
|
15596
16649
|
continue;
|
|
15597
16650
|
}
|
|
15598
16651
|
const table = value[tableName];
|
|
15599
|
-
if (!
|
|
15600
|
-
|
|
16652
|
+
if (!validators_isPlainObject(table)) {
|
|
16653
|
+
validators_pushIssue(issues, tablePath, "must be an object");
|
|
15601
16654
|
continue;
|
|
15602
16655
|
}
|
|
15603
16656
|
for (const req of [
|
|
@@ -15605,61 +16658,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15605
16658
|
"server",
|
|
15606
16659
|
"title",
|
|
15607
16660
|
"columns"
|
|
15608
|
-
])if (void 0 === table[req])
|
|
16661
|
+
])if (void 0 === table[req]) validators_pushIssue(issues, `${tablePath}.${req}`, "is required");
|
|
15609
16662
|
if (void 0 !== table.client) {
|
|
15610
|
-
if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client))
|
|
16663
|
+
if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client)) validators_pushIssue(issues, `${tablePath}.client`, 'must be "none", "search", or "full"');
|
|
15611
16664
|
}
|
|
15612
16665
|
if (void 0 !== table.server) {
|
|
15613
|
-
if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server))
|
|
16666
|
+
if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server)) validators_pushIssue(issues, `${tablePath}.server`, 'must be "none", "search", or "full"');
|
|
15614
16667
|
}
|
|
15615
16668
|
if (void 0 !== table.title) {
|
|
15616
|
-
if ("string" != typeof table.title || 0 === table.title.length)
|
|
16669
|
+
if ("string" != typeof table.title || 0 === table.title.length) validators_pushIssue(issues, `${tablePath}.title`, "must be a non-empty string");
|
|
15617
16670
|
}
|
|
15618
16671
|
if (void 0 !== table.columns) if (Array.isArray(table.columns)) table.columns.forEach((column, index)=>{
|
|
15619
16672
|
validateDbSchemaColumn(column, `${tablePath}.columns[${index}]`, issues);
|
|
15620
16673
|
});
|
|
15621
|
-
else
|
|
16674
|
+
else validators_pushIssue(issues, `${tablePath}.columns`, "must be an array");
|
|
15622
16675
|
if (void 0 !== table.indexes) if (Array.isArray(table.indexes)) table.indexes.forEach((indexEntry, index)=>{
|
|
15623
16676
|
validateDbTableIndex(indexEntry, `${tablePath}.indexes[${index}]`, issues);
|
|
15624
16677
|
});
|
|
15625
|
-
else
|
|
16678
|
+
else validators_pushIssue(issues, `${tablePath}.indexes`, "must be an array");
|
|
15626
16679
|
}
|
|
15627
16680
|
return issues;
|
|
15628
16681
|
}
|
|
15629
16682
|
function validateDbTableIndex(value, path, issues) {
|
|
15630
|
-
if (!
|
|
15631
|
-
if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c))
|
|
15632
|
-
if (void 0 === value.type)
|
|
15633
|
-
else if ("string" != typeof value.type || !DB_TABLE_INDEX_TYPES.has(value.type))
|
|
16683
|
+
if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
|
|
16684
|
+
if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c)) validators_pushIssue(issues, `${path}.columns`, "must be an array of strings");
|
|
16685
|
+
if (void 0 === value.type) validators_pushIssue(issues, `${path}.type`, "is required");
|
|
16686
|
+
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"');
|
|
15634
16687
|
}
|
|
15635
16688
|
function validateDbSchemaColumn(value, path, issues) {
|
|
15636
|
-
if (!
|
|
15637
|
-
if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length)
|
|
15638
|
-
else if (!
|
|
15639
|
-
if (void 0 === value.type) return void
|
|
15640
|
-
if ("string" != typeof value.type) return void
|
|
16689
|
+
if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
|
|
16690
|
+
if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) validators_pushIssue(issues, `${path}.name`, "must be a non-empty string");
|
|
16691
|
+
else if (!validators_IDENTIFIER_RE.test(value.name)) validators_pushIssue(issues, `${path}.name`, "must be a valid Python identifier");
|
|
16692
|
+
if (void 0 === value.type) return void validators_pushIssue(issues, `${path}.type`, "is required");
|
|
16693
|
+
if ("string" != typeof value.type) return void validators_pushIssue(issues, `${path}.type`, "must be a string");
|
|
15641
16694
|
const colType = value.type;
|
|
15642
16695
|
if (DB_LINK_COLUMN_TYPES.has(colType)) {
|
|
15643
|
-
if (void 0 === value.target || "string" != typeof value.target || 0 === value.target.length)
|
|
15644
|
-
else if (!
|
|
15645
|
-
} else if (!DB_BASIC_COLUMN_TYPES.has(colType))
|
|
16696
|
+
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)");
|
|
16697
|
+
else if (!validators_IDENTIFIER_RE.test(value.target)) validators_pushIssue(issues, `${path}.target`, "must be a valid Python identifier");
|
|
16698
|
+
} else if (!DB_BASIC_COLUMN_TYPES.has(colType)) validators_pushIssue(issues, `${path}.type`, `must be a basic column type (${[
|
|
15646
16699
|
...DB_BASIC_COLUMN_TYPES
|
|
15647
16700
|
].join(", ")}) or ${[
|
|
15648
16701
|
...DB_LINK_COLUMN_TYPES
|
|
15649
16702
|
].join(", ")}`);
|
|
15650
|
-
if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden)
|
|
15651
|
-
if (void 0 !== value.admin_ui && null !== value.admin_ui) if (
|
|
16703
|
+
if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden) validators_pushIssue(issues, `${path}.client_hidden`, "must be a boolean");
|
|
16704
|
+
if (void 0 !== value.admin_ui && null !== value.admin_ui) if (validators_isPlainObject(value.admin_ui)) {
|
|
15652
16705
|
const ui = value.admin_ui;
|
|
15653
|
-
if (void 0 !== ui.width && "number" != typeof ui.width)
|
|
15654
|
-
if (void 0 !== ui.order && "number" != typeof ui.order)
|
|
15655
|
-
} else
|
|
16706
|
+
if (void 0 !== ui.width && "number" != typeof ui.width) validators_pushIssue(issues, `${path}.admin_ui.width`, "must be a number");
|
|
16707
|
+
if (void 0 !== ui.order && "number" != typeof ui.order) validators_pushIssue(issues, `${path}.admin_ui.order`, "must be a number");
|
|
16708
|
+
} else validators_pushIssue(issues, `${path}.admin_ui`, "must be an object or null");
|
|
15656
16709
|
if (void 0 !== value.indexes) {
|
|
15657
|
-
if (!Array.isArray(value.indexes)) return void
|
|
16710
|
+
if (!Array.isArray(value.indexes)) return void validators_pushIssue(issues, `${path}.indexes`, "must be an array");
|
|
15658
16711
|
value.indexes.forEach((entry, i)=>{
|
|
15659
16712
|
const idxPath = `${path}.indexes[${i}]`;
|
|
15660
|
-
if (!
|
|
15661
|
-
if (void 0 === entry.type)
|
|
15662
|
-
else if ("string" != typeof entry.type || !DB_TABLE_INDEX_TYPES.has(entry.type))
|
|
16713
|
+
if (!validators_isPlainObject(entry)) return void validators_pushIssue(issues, idxPath, "must be an object");
|
|
16714
|
+
if (void 0 === entry.type) validators_pushIssue(issues, `${idxPath}.type`, "is required");
|
|
16715
|
+
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"');
|
|
15663
16716
|
});
|
|
15664
16717
|
}
|
|
15665
16718
|
}
|
|
@@ -15668,8 +16721,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15668
16721
|
if (void 0 === services) return issues;
|
|
15669
16722
|
if (!Array.isArray(services)) return issues;
|
|
15670
16723
|
services.forEach((service, index)=>{
|
|
15671
|
-
if (!
|
|
15672
|
-
if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source))
|
|
16724
|
+
if (!validators_isPlainObject(service) || "string" != typeof service.source) return;
|
|
16725
|
+
if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source)) validators_pushIssue(issues, `${path}[${index}].source`, "must be a known runtime service source path");
|
|
15673
16726
|
});
|
|
15674
16727
|
return issues;
|
|
15675
16728
|
}
|
|
@@ -15677,9 +16730,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15677
16730
|
const issues = [];
|
|
15678
16731
|
if (void 0 === services || !Array.isArray(services)) return issues;
|
|
15679
16732
|
services.forEach((service, index)=>{
|
|
15680
|
-
if (!
|
|
15681
|
-
if (!
|
|
15682
|
-
if (true === service.client_config.allow_remember_me && void 0 === service.client_config.remember_me_days)
|
|
16733
|
+
if (!validators_isPlainObject(service) || "/runtime/services/anvil/users.yml" !== service.source) return;
|
|
16734
|
+
if (!validators_isPlainObject(service.client_config)) return;
|
|
16735
|
+
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");
|
|
15683
16736
|
});
|
|
15684
16737
|
return issues;
|
|
15685
16738
|
}
|
|
@@ -15687,25 +16740,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15687
16740
|
const issues = [];
|
|
15688
16741
|
if (void 0 === value) return issues;
|
|
15689
16742
|
if (!Array.isArray(value)) {
|
|
15690
|
-
|
|
16743
|
+
validators_pushIssue(issues, path, "must be an array");
|
|
15691
16744
|
return issues;
|
|
15692
16745
|
}
|
|
15693
16746
|
value.forEach((task, index)=>{
|
|
15694
16747
|
const taskPath = `${path}[${index}]`;
|
|
15695
|
-
if (!
|
|
15696
|
-
if ("string" != typeof task.job_id || 0 === task.job_id.length)
|
|
15697
|
-
|
|
15698
|
-
if (!
|
|
16748
|
+
if (!validators_isPlainObject(task)) return void validators_pushIssue(issues, taskPath, "must be an object");
|
|
16749
|
+
if ("string" != typeof task.job_id || 0 === task.job_id.length) validators_pushIssue(issues, `${taskPath}.job_id`, "must be a non-empty string");
|
|
16750
|
+
validators_validateIdentifier(task.task_name, `${taskPath}.task_name`, issues);
|
|
16751
|
+
if (!validators_isPlainObject(task.time_spec)) return void validators_pushIssue(issues, `${taskPath}.time_spec`, "must be an object");
|
|
15699
16752
|
const timeSpec = task.time_spec;
|
|
15700
|
-
if ("string" != typeof timeSpec.every || !SCHEDULE_EVERY_VALUES.has(timeSpec.every))
|
|
16753
|
+
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"');
|
|
15701
16754
|
const interval = timeSpec.n;
|
|
15702
|
-
if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0))
|
|
15703
|
-
if (void 0 !== timeSpec.at) if (
|
|
16755
|
+
if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0)) validators_pushIssue(issues, `${taskPath}.time_spec.n`, "must be a positive integer");
|
|
16756
|
+
if (void 0 !== timeSpec.at) if (validators_isPlainObject(timeSpec.at)) {
|
|
15704
16757
|
const at = timeSpec.at;
|
|
15705
|
-
if (void 0 !== at.minute && !Number.isInteger(at.minute))
|
|
15706
|
-
if (void 0 !== at.hour && !Number.isInteger(at.hour))
|
|
15707
|
-
if (void 0 !== at.day && !Number.isInteger(at.day))
|
|
15708
|
-
} else
|
|
16758
|
+
if (void 0 !== at.minute && !Number.isInteger(at.minute)) validators_pushIssue(issues, `${taskPath}.time_spec.at.minute`, "must be an integer");
|
|
16759
|
+
if (void 0 !== at.hour && !Number.isInteger(at.hour)) validators_pushIssue(issues, `${taskPath}.time_spec.at.hour`, "must be an integer");
|
|
16760
|
+
if (void 0 !== at.day && !Number.isInteger(at.day)) validators_pushIssue(issues, `${taskPath}.time_spec.at.day`, "must be an integer");
|
|
16761
|
+
} else validators_pushIssue(issues, `${taskPath}.time_spec.at`, "must be an object");
|
|
15709
16762
|
});
|
|
15710
16763
|
return issues;
|
|
15711
16764
|
}
|
|
@@ -15717,13 +16770,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15717
16770
|
dependencies: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(external_valibot_namespaceObject.any())),
|
|
15718
16771
|
runtime_options: RuntimeOptionsSchema,
|
|
15719
16772
|
services: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(AnvilServiceEntrySchema)),
|
|
15720
|
-
startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.looseObject({
|
|
16773
|
+
startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.looseObject({
|
|
15721
16774
|
type: external_valibot_namespaceObject.picklist([
|
|
15722
16775
|
"module",
|
|
15723
16776
|
"form"
|
|
15724
16777
|
]),
|
|
15725
16778
|
module: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.string())
|
|
15726
|
-
})),
|
|
16779
|
+
}))),
|
|
15727
16780
|
startup_form: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.string())),
|
|
15728
16781
|
db_schema: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
|
|
15729
16782
|
scheduled_tasks: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
|
|
@@ -15814,6 +16867,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15814
16867
|
const startIndex = normalizedPath.lastIndexOf(clientCodePrefix);
|
|
15815
16868
|
const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
|
|
15816
16869
|
if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
|
|
16870
|
+
if (relativeToClientCode.endsWith(".html")) return "form_template.html";
|
|
16871
|
+
if (relativeToClientCode.endsWith(".py")) return "python";
|
|
16872
|
+
}
|
|
16873
|
+
const serverCodePrefix = "server_code/";
|
|
16874
|
+
if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
|
|
16875
|
+
const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
|
|
16876
|
+
const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
|
|
16877
|
+
if (relativeToServerCode.endsWith(".py")) return "python";
|
|
15817
16878
|
}
|
|
15818
16879
|
return null;
|
|
15819
16880
|
}
|
|
@@ -15833,7 +16894,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15833
16894
|
currentDir = parentDir;
|
|
15834
16895
|
}
|
|
15835
16896
|
}
|
|
15836
|
-
function validatePath(filePath,
|
|
16897
|
+
function validatePath(filePath, fileContent) {
|
|
15837
16898
|
const target = inferValidationTarget(filePath);
|
|
15838
16899
|
const existingPath = resolveExistingPath(filePath);
|
|
15839
16900
|
const appRoot = findAnvilAppRoot(filePath);
|
|
@@ -15860,7 +16921,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15860
16921
|
}
|
|
15861
16922
|
]
|
|
15862
16923
|
};
|
|
15863
|
-
const result = validateAnvilYaml(
|
|
16924
|
+
const result = validateAnvilYaml(fileContent);
|
|
15864
16925
|
return result.ok ? {
|
|
15865
16926
|
ok: true,
|
|
15866
16927
|
target
|
|
@@ -15886,7 +16947,62 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15886
16947
|
]
|
|
15887
16948
|
};
|
|
15888
16949
|
}
|
|
15889
|
-
const result = validateFormTemplate(
|
|
16950
|
+
const result = validateFormTemplate(fileContent);
|
|
16951
|
+
return result.ok ? {
|
|
16952
|
+
ok: true,
|
|
16953
|
+
target
|
|
16954
|
+
} : {
|
|
16955
|
+
...result,
|
|
16956
|
+
target
|
|
16957
|
+
};
|
|
16958
|
+
}
|
|
16959
|
+
if ("form_template.html" === target) {
|
|
16960
|
+
if (existingPath && appRoot) {
|
|
16961
|
+
const clientCodeRoot = external_path_default().join(appRoot, "client_code");
|
|
16962
|
+
const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
|
|
16963
|
+
const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
|
|
16964
|
+
if (!isInsideClientCode || !existingPath.endsWith(".html")) return {
|
|
16965
|
+
ok: false,
|
|
16966
|
+
target,
|
|
16967
|
+
message: "File is not a client_code HTML form",
|
|
16968
|
+
issues: [
|
|
16969
|
+
{
|
|
16970
|
+
path: "",
|
|
16971
|
+
message: `expected a .html file under ${clientCodeRoot}`
|
|
16972
|
+
}
|
|
16973
|
+
]
|
|
16974
|
+
};
|
|
16975
|
+
}
|
|
16976
|
+
const result = validateFormTemplateHtml(fileContent);
|
|
16977
|
+
return result.ok ? {
|
|
16978
|
+
ok: true,
|
|
16979
|
+
target
|
|
16980
|
+
} : {
|
|
16981
|
+
...result,
|
|
16982
|
+
target
|
|
16983
|
+
};
|
|
16984
|
+
}
|
|
16985
|
+
if ("python" === target) {
|
|
16986
|
+
if (existingPath && appRoot) {
|
|
16987
|
+
const clientCodeRoot = external_path_default().join(appRoot, "client_code");
|
|
16988
|
+
const serverCodeRoot = external_path_default().join(appRoot, "server_code");
|
|
16989
|
+
const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
|
|
16990
|
+
const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
|
|
16991
|
+
const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
|
|
16992
|
+
const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
|
|
16993
|
+
if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
|
|
16994
|
+
ok: false,
|
|
16995
|
+
target,
|
|
16996
|
+
message: "File is not an Anvil app Python file",
|
|
16997
|
+
issues: [
|
|
16998
|
+
{
|
|
16999
|
+
path: "",
|
|
17000
|
+
message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
|
|
17001
|
+
}
|
|
17002
|
+
]
|
|
17003
|
+
};
|
|
17004
|
+
}
|
|
17005
|
+
const result = validatePython(filePath, fileContent, appRoot);
|
|
15890
17006
|
return result.ok ? {
|
|
15891
17007
|
ok: true,
|
|
15892
17008
|
target
|
|
@@ -15902,7 +17018,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15902
17018
|
issues: [
|
|
15903
17019
|
{
|
|
15904
17020
|
path: "",
|
|
15905
|
-
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml,
|
|
17021
|
+
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
|
|
15906
17022
|
}
|
|
15907
17023
|
]
|
|
15908
17024
|
};
|
|
@@ -15941,9 +17057,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15941
17057
|
};
|
|
15942
17058
|
return null;
|
|
15943
17059
|
}
|
|
17060
|
+
function formatValidationFailure(fileName, validationResult) {
|
|
17061
|
+
const issueSummary = validationResult.issues.map((issue)=>` - ${formatValidationPath(issue.path)}: ${issue.message}`).join("\n");
|
|
17062
|
+
return issueSummary ? `${fileName} validation failed: ${validationResult.message}\n${issueSummary}` : `${fileName} validation failed: ${validationResult.message}`;
|
|
17063
|
+
}
|
|
15944
17064
|
async function loadFormTemplateData(relativePath, templateInfo, context) {
|
|
15945
17065
|
const { repoPath, stagedOnly = false } = context;
|
|
15946
|
-
const {
|
|
17066
|
+
const { format } = templateInfo;
|
|
15947
17067
|
try {
|
|
15948
17068
|
const content = await readFileContent(repoPath, relativePath, stagedOnly);
|
|
15949
17069
|
if ("yaml" === format) {
|
|
@@ -15956,26 +17076,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15956
17076
|
reason: issueSummary ? `${validationResult.message}. ${issueSummary}` : validationResult.message
|
|
15957
17077
|
};
|
|
15958
17078
|
}
|
|
15959
|
-
const parsed = external_js_yaml_default().load(content);
|
|
15960
|
-
const template = {
|
|
15961
|
-
...parsed,
|
|
15962
|
-
is_package: "package" === kind
|
|
15963
|
-
};
|
|
15964
|
-
return {
|
|
15965
|
-
ok: true,
|
|
15966
|
-
data: {
|
|
15967
|
-
template
|
|
15968
|
-
}
|
|
15969
|
-
};
|
|
15970
17079
|
}
|
|
15971
|
-
const { frontmatter, html } = parseHtmlWithFrontmatter(content);
|
|
15972
|
-
const template = frontmatter ?? {};
|
|
15973
17080
|
return {
|
|
15974
17081
|
ok: true,
|
|
15975
|
-
|
|
15976
|
-
|
|
15977
|
-
|
|
15978
|
-
}
|
|
17082
|
+
source: (0, form_template_parser_namespaceObject.parseFormTemplateSource)(content, {
|
|
17083
|
+
format
|
|
17084
|
+
})
|
|
15979
17085
|
};
|
|
15980
17086
|
} catch (error) {
|
|
15981
17087
|
return {
|
|
@@ -16682,20 +17788,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16682
17788
|
]
|
|
16683
17789
|
});
|
|
16684
17790
|
}
|
|
16685
|
-
function buildFormDataFromTemplate(template, html, options) {
|
|
16686
|
-
const { class_name: _templateClassName, code: _templateCode, is_package: _templateIsPackage, ...templateRest } = template && "object" == typeof template ? template : {};
|
|
16687
|
-
const formData = {
|
|
16688
|
-
...templateRest,
|
|
16689
|
-
class_name: options.className,
|
|
16690
|
-
code: options.code,
|
|
16691
|
-
is_package: options.isPackage
|
|
16692
|
-
};
|
|
16693
|
-
if (void 0 !== html) {
|
|
16694
|
-
formData.save_as_html = true;
|
|
16695
|
-
formData.serialized_html = html;
|
|
16696
|
-
}
|
|
16697
|
-
return formData;
|
|
16698
|
-
}
|
|
16699
17791
|
async function buildFormTemplateData(relativePath, templateInfo, context) {
|
|
16700
17792
|
const { repoPath, stagedOnly = false } = context;
|
|
16701
17793
|
const { kind } = templateInfo;
|
|
@@ -16719,10 +17811,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16719
17811
|
reason: "package" === kind ? "Form __init__.py file not found - cannot save template without code" : "Form .py file not found - cannot save template without code"
|
|
16720
17812
|
}
|
|
16721
17813
|
};
|
|
16722
|
-
const
|
|
16723
|
-
const html = loadResult.data.html;
|
|
16724
|
-
const templateObject = templateData && "object" == typeof templateData && !Array.isArray(templateData) ? templateData : {};
|
|
16725
|
-
const formData = buildFormDataFromTemplate(templateObject, html, {
|
|
17814
|
+
const formData = (0, form_template_parser_namespaceObject.buildFormTemplateSavePayload)(loadResult.source, {
|
|
16726
17815
|
className,
|
|
16727
17816
|
code: codeContent,
|
|
16728
17817
|
isPackage: "package" === kind
|
|
@@ -16907,7 +17996,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16907
17996
|
}
|
|
16908
17997
|
};
|
|
16909
17998
|
const validationResult = validateAnvilYaml(yamlContent);
|
|
16910
|
-
if (!validationResult.ok) throw new Error("anvil.yaml
|
|
17999
|
+
if (!validationResult.ok) throw new Error(formatValidationFailure("anvil.yaml", validationResult));
|
|
16911
18000
|
const changes = [];
|
|
16912
18001
|
for (const [key, value] of Object.entries(parsedYaml))if (!previousYaml || !deepEqual(previousYaml[key], value)) changes.push({
|
|
16913
18002
|
key,
|
|
@@ -17014,6 +18103,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17014
18103
|
}
|
|
17015
18104
|
}
|
|
17016
18105
|
function authDiagnostic(error) {
|
|
18106
|
+
if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
|
|
18107
|
+
code: error.type,
|
|
18108
|
+
recoverability: "requires_user_action",
|
|
18109
|
+
requiresUserAction: true,
|
|
18110
|
+
suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
|
|
18111
|
+
};
|
|
17017
18112
|
return {
|
|
17018
18113
|
code: error.type,
|
|
17019
18114
|
recoverability: "requires_user_action",
|
|
@@ -17076,6 +18171,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17076
18171
|
class SaveProcessor extends Emitter {
|
|
17077
18172
|
hasPendingChanges = false;
|
|
17078
18173
|
debounceTimer = null;
|
|
18174
|
+
saveLoopPromise = null;
|
|
17079
18175
|
saveSequence = 0;
|
|
17080
18176
|
SAVE_DEBOUNCE_MS = 1000;
|
|
17081
18177
|
MAX_RETRIES = 3;
|
|
@@ -17099,6 +18195,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17099
18195
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
17100
18196
|
if (isFirstChange) logger_logger.progress("sync", "Change detected");
|
|
17101
18197
|
this.debounceTimer = setTimeout(()=>{
|
|
18198
|
+
this.debounceTimer = null;
|
|
17102
18199
|
this.processSaveBatchWithEvents().catch((error)=>{
|
|
17103
18200
|
logger_logger.error(`Save failed: ${errors_getErrorMessage(error)}`);
|
|
17104
18201
|
});
|
|
@@ -17123,6 +18220,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17123
18220
|
}
|
|
17124
18221
|
}
|
|
17125
18222
|
async processSaveBatchWithEvents() {
|
|
18223
|
+
if (this.saveLoopPromise) return this.saveLoopPromise;
|
|
18224
|
+
this.saveLoopPromise = this.drainSaveQueue();
|
|
18225
|
+
try {
|
|
18226
|
+
await this.saveLoopPromise;
|
|
18227
|
+
} finally{
|
|
18228
|
+
this.saveLoopPromise = null;
|
|
18229
|
+
}
|
|
18230
|
+
}
|
|
18231
|
+
async drainSaveQueue() {
|
|
18232
|
+
while(this.hasPendingChanges && !this.config.isCleanedUp() && !this.config.isPaused())await this.processOneSaveBatchWithEvents();
|
|
18233
|
+
}
|
|
18234
|
+
async processOneSaveBatchWithEvents() {
|
|
17126
18235
|
if (this.config.isCleanedUp()) return;
|
|
17127
18236
|
logger_logger.progressUpdate("sync", "Processing");
|
|
17128
18237
|
if (!this.hasPendingChanges) return void logger_logger.progressEnd("sync");
|
|
@@ -17140,6 +18249,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17140
18249
|
newCommitId: this.config.getCommitId(),
|
|
17141
18250
|
fileCount
|
|
17142
18251
|
});
|
|
18252
|
+
if (result.anvilYamlDependenciesChanged) this.emit("anvil-yaml-dependencies-changed", void 0);
|
|
17143
18253
|
this.emit("save-complete", {
|
|
17144
18254
|
newCommitId: this.config.getCommitId(),
|
|
17145
18255
|
fileCount
|
|
@@ -17230,13 +18340,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17230
18340
|
}
|
|
17231
18341
|
async getStagedChanges() {
|
|
17232
18342
|
try {
|
|
17233
|
-
return await getStagedFileChanges(this.config.gitService.getGit());
|
|
18343
|
+
return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
|
|
17234
18344
|
} catch (e) {
|
|
17235
18345
|
logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
|
|
17236
18346
|
return [];
|
|
17237
18347
|
}
|
|
17238
18348
|
}
|
|
17239
18349
|
async getUnstagedChanges(status) {
|
|
18350
|
+
const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
|
|
17240
18351
|
const renames = status.renamed;
|
|
17241
18352
|
const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
|
|
17242
18353
|
const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
|
|
@@ -17256,7 +18367,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17256
18367
|
const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
|
|
17257
18368
|
const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
|
|
17258
18369
|
return [
|
|
17259
|
-
...status.modified.map((f)=>({
|
|
18370
|
+
...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
|
|
17260
18371
|
path: f,
|
|
17261
18372
|
type: "change"
|
|
17262
18373
|
})),
|
|
@@ -17327,11 +18438,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17327
18438
|
async processBatchedChanges(changes) {
|
|
17328
18439
|
const saveArray = [];
|
|
17329
18440
|
const originalFilePaths = [];
|
|
18441
|
+
let anvilYamlDependenciesChanged = false;
|
|
17330
18442
|
const deduplicatedChanges = deduplicateFormChanges(changes);
|
|
17331
18443
|
for (const change of deduplicatedChanges){
|
|
17332
18444
|
if ("anvil.yaml" === change.path) {
|
|
17333
18445
|
const previousAnvilYaml = await this.getHeadAnvilYaml();
|
|
17334
18446
|
const anvilChanges = await getAnvilYamlChanges(this.config.repoPath, previousAnvilYaml);
|
|
18447
|
+
const previousDependencies = normalizeAnvilYamlDependenciesValue(previousAnvilYaml?.dependencies);
|
|
18448
|
+
const currentDependencies = await readAnvilYamlDependencies(this.config.repoPath);
|
|
18449
|
+
if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) anvilYamlDependenciesChanged = true;
|
|
17335
18450
|
if (0 === anvilChanges.length) {
|
|
17336
18451
|
logger_logger.verbose(external_chalk_default().gray(" Ignoring anvil.yaml: No changes detected"));
|
|
17337
18452
|
continue;
|
|
@@ -17383,7 +18498,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17383
18498
|
}
|
|
17384
18499
|
await this.sendSavesToAnvil(saveArray, originalFilePaths);
|
|
17385
18500
|
return {
|
|
17386
|
-
skipped: false
|
|
18501
|
+
skipped: false,
|
|
18502
|
+
anvilYamlDependenciesChanged
|
|
17387
18503
|
};
|
|
17388
18504
|
}
|
|
17389
18505
|
logSaveResult(change, savePath, content) {
|
|
@@ -17459,7 +18575,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17459
18575
|
await this.handleConflict(json_resp, originalFilePaths, retryCount);
|
|
17460
18576
|
return;
|
|
17461
18577
|
}
|
|
17462
|
-
if (200 === resp.status) return void await this.handleSuccess(json_resp);
|
|
18578
|
+
if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
|
|
17463
18579
|
logger_logger.error(`Unexpected response status: ${resp.status}`);
|
|
17464
18580
|
throw {
|
|
17465
18581
|
type: "server_error",
|
|
@@ -17562,13 +18678,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17562
18678
|
reason: "Unknown conflict"
|
|
17563
18679
|
};
|
|
17564
18680
|
}
|
|
17565
|
-
async handleSuccess(json_resp) {
|
|
18681
|
+
async handleSuccess(json_resp, originalFilePaths) {
|
|
17566
18682
|
const version = json_resp.version;
|
|
17567
|
-
|
|
18683
|
+
const oldCommitId = json_resp.previous_commit ?? null;
|
|
18684
|
+
const commitIdBeforePostSaveSync = this.config.getCommitId();
|
|
18685
|
+
this.emit("saved", {
|
|
18686
|
+
oldCommitId,
|
|
18687
|
+
newCommitId: version,
|
|
18688
|
+
branch: this.config.getCurrentBranch()
|
|
18689
|
+
});
|
|
17568
18690
|
if (this.postSaveCallback) {
|
|
17569
18691
|
logger_logger.progressUpdate("sync", "Syncing back");
|
|
17570
|
-
await this.postSaveCallback();
|
|
18692
|
+
await this.postSaveCallback(new Set(originalFilePaths));
|
|
17571
18693
|
}
|
|
18694
|
+
if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
|
|
17572
18695
|
}
|
|
17573
18696
|
async rerouteChanges(changes) {
|
|
17574
18697
|
logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
|
|
@@ -17622,76 +18745,114 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17622
18745
|
}
|
|
17623
18746
|
class SyncManager extends Emitter {
|
|
17624
18747
|
config;
|
|
17625
|
-
|
|
18748
|
+
syncQueue = Promise.resolve();
|
|
17626
18749
|
constructor(config){
|
|
17627
18750
|
super();
|
|
17628
18751
|
this.config = config;
|
|
17629
18752
|
}
|
|
17630
18753
|
async syncRemoteChanges(knownLocalChanges) {
|
|
17631
|
-
|
|
17632
|
-
|
|
18754
|
+
const runSync = ()=>this.performSyncRemoteChanges(knownLocalChanges);
|
|
18755
|
+
const queuedSync = this.syncQueue.then(runSync, runSync);
|
|
18756
|
+
this.syncQueue = queuedSync.catch(()=>{});
|
|
18757
|
+
return queuedSync;
|
|
18758
|
+
}
|
|
18759
|
+
async performSyncRemoteChanges(knownLocalChanges) {
|
|
18760
|
+
const oldCommitId = this.config.getCommitId();
|
|
18761
|
+
let locallyChangedFiles;
|
|
18762
|
+
let initialStatus;
|
|
18763
|
+
if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
|
|
18764
|
+
else {
|
|
18765
|
+
initialStatus = await this.config.gitService.getStatus();
|
|
18766
|
+
locallyChangedFiles = buildLocalChangesSet(initialStatus);
|
|
18767
|
+
}
|
|
18768
|
+
let syncMode;
|
|
18769
|
+
if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
|
|
18770
|
+
else {
|
|
18771
|
+
await this.fetchAndResetFromAnvil(locallyChangedFiles);
|
|
18772
|
+
syncMode = "preservation";
|
|
18773
|
+
}
|
|
18774
|
+
if ("clean-move" === syncMode) return {
|
|
18775
|
+
localOnlyChanges: [],
|
|
18776
|
+
conflicts: []
|
|
18777
|
+
};
|
|
18778
|
+
const newCommitId = await this.config.gitService.getCommitId();
|
|
18779
|
+
this.config.setCommitId(newCommitId);
|
|
18780
|
+
const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
|
|
18781
|
+
const statusAfter = await this.config.gitService.getStatus();
|
|
18782
|
+
const newChanges = extractChangesFromStatus(statusAfter);
|
|
18783
|
+
if (0 === newChanges.length) {
|
|
18784
|
+
logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
|
|
17633
18785
|
return {
|
|
17634
18786
|
localOnlyChanges: [],
|
|
17635
18787
|
conflicts: []
|
|
17636
18788
|
};
|
|
17637
18789
|
}
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
18790
|
+
const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
|
|
18791
|
+
await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
|
|
18792
|
+
logConflictResolution(result);
|
|
18793
|
+
return {
|
|
18794
|
+
localOnlyChanges: result.localOnlyChanges,
|
|
18795
|
+
conflicts: result.conflicts
|
|
18796
|
+
};
|
|
18797
|
+
}
|
|
18798
|
+
async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
|
|
18799
|
+
const oldCommitId = this.config.getCommitId();
|
|
18800
|
+
await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
|
|
18801
|
+
}
|
|
18802
|
+
async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
|
|
18803
|
+
return this.withFetchedAnvilRef(async (tempRef)=>{
|
|
18804
|
+
const localHead = await this.config.gitService.getCommitId();
|
|
18805
|
+
if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
|
|
17643
18806
|
else {
|
|
17644
|
-
const
|
|
17645
|
-
|
|
18807
|
+
const statusBeforeHardReset = await this.config.gitService.getStatus();
|
|
18808
|
+
if (!this.isCleanForFastForward(statusBeforeHardReset)) {
|
|
18809
|
+
for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
|
|
18810
|
+
await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
|
|
18811
|
+
return "preservation";
|
|
18812
|
+
}
|
|
18813
|
+
await this.config.gitService.reset(tempRef, "hard");
|
|
17646
18814
|
}
|
|
17647
|
-
await this.fetchAndResetFromAnvil();
|
|
17648
18815
|
const newCommitId = await this.config.gitService.getCommitId();
|
|
17649
18816
|
this.config.setCommitId(newCommitId);
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
if (0 === newChanges.length) {
|
|
17654
|
-
logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
|
|
17655
|
-
return {
|
|
17656
|
-
localOnlyChanges: [],
|
|
17657
|
-
conflicts: []
|
|
17658
|
-
};
|
|
17659
|
-
}
|
|
17660
|
-
const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
|
|
17661
|
-
await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
|
|
17662
|
-
logConflictResolution(result);
|
|
17663
|
-
return {
|
|
17664
|
-
localOnlyChanges: result.localOnlyChanges,
|
|
17665
|
-
conflicts: result.conflicts
|
|
17666
|
-
};
|
|
17667
|
-
} finally{
|
|
17668
|
-
this.syncInProgress = false;
|
|
17669
|
-
}
|
|
18817
|
+
await this.config.editorYaml.reload();
|
|
18818
|
+
return "clean-move";
|
|
18819
|
+
});
|
|
17670
18820
|
}
|
|
17671
|
-
async
|
|
18821
|
+
async withFetchedAnvilRef(callback) {
|
|
17672
18822
|
const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
|
|
17673
18823
|
this.config.setAuthToken(validToken);
|
|
17674
18824
|
const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
|
|
17675
18825
|
const currentBranch = this.config.getCurrentBranch();
|
|
17676
18826
|
const tempRef = `anvil-sync-temp-${Date.now()}`;
|
|
17677
|
-
|
|
17678
|
-
|
|
18827
|
+
try {
|
|
18828
|
+
await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
|
|
18829
|
+
return await callback(tempRef);
|
|
18830
|
+
} finally{
|
|
18831
|
+
await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
|
|
18832
|
+
}
|
|
18833
|
+
}
|
|
18834
|
+
async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
|
|
17679
18835
|
await this.config.gitService.reset(tempRef, "mixed");
|
|
17680
|
-
await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
|
|
17681
18836
|
const newCommitId = await this.config.gitService.getCommitId();
|
|
17682
18837
|
await this.config.gitService.checkout([
|
|
17683
18838
|
".anvil_editor.yaml",
|
|
17684
18839
|
"anvil.yaml"
|
|
17685
18840
|
]);
|
|
17686
18841
|
await this.config.editorYaml.reload();
|
|
17687
|
-
await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
|
|
18842
|
+
await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
|
|
17688
18843
|
await this.discardFormattingOnlyYamlChanges();
|
|
17689
18844
|
}
|
|
17690
|
-
|
|
18845
|
+
isCleanForFastForward(status) {
|
|
18846
|
+
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;
|
|
18847
|
+
}
|
|
18848
|
+
async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
|
|
17691
18849
|
try {
|
|
17692
18850
|
const status = await this.config.gitService.getStatus();
|
|
17693
18851
|
const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
|
|
17694
|
-
await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles
|
|
18852
|
+
await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles, {
|
|
18853
|
+
oldCommitId,
|
|
18854
|
+
localChangesToPreserve
|
|
18855
|
+
});
|
|
17695
18856
|
} catch (e) {
|
|
17696
18857
|
logger_logger.verbose(external_chalk_default().gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
|
|
17697
18858
|
}
|
|
@@ -17804,6 +18965,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17804
18965
|
environment;
|
|
17805
18966
|
envBranchChangeMode;
|
|
17806
18967
|
isSwitchingForEnvironment = false;
|
|
18968
|
+
environmentSwitchTargetBranch = null;
|
|
18969
|
+
dependencyWatchEnabled;
|
|
18970
|
+
isRefreshingDependencies = false;
|
|
18971
|
+
hasPendingDependencyRefresh = false;
|
|
17807
18972
|
pendingEnvironmentBranchSwitch = null;
|
|
17808
18973
|
BRANCH_CHANGE_SETTLE_MS = 2000;
|
|
17809
18974
|
LOCAL_CHANGE_POLL_MS = 2000;
|
|
@@ -17821,6 +18986,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17821
18986
|
this.stagedOnly = options.stagedOnly ?? false;
|
|
17822
18987
|
this.environment = options.environment;
|
|
17823
18988
|
this.envBranchChangeMode = options.envBranchChangeMode ?? "manual";
|
|
18989
|
+
this.dependencyWatchEnabled = options.dependencyWatch ?? true;
|
|
17824
18990
|
this.editSession = "anvil-sync-" + Math.random().toString(36).substring(2, 15);
|
|
17825
18991
|
this.gitService = new GitService(this.repoPath);
|
|
17826
18992
|
this.editorYaml = new EditorYamlManager(this.repoPath);
|
|
@@ -17840,6 +19006,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17840
19006
|
async initialize() {
|
|
17841
19007
|
await this.editorYaml.load();
|
|
17842
19008
|
this.initializeProcessors();
|
|
19009
|
+
if (this.dependencyWatchEnabled) await this.refreshDependencies("startup");
|
|
17843
19010
|
try {
|
|
17844
19011
|
await this.connectWebSocket();
|
|
17845
19012
|
} catch (error) {
|
|
@@ -17859,7 +19026,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17859
19026
|
getAuthToken: ()=>this.authToken,
|
|
17860
19027
|
setAuthToken: (token)=>{
|
|
17861
19028
|
this.authToken = token;
|
|
17862
|
-
this.wsClient?.updateAuthToken(token);
|
|
19029
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
17863
19030
|
},
|
|
17864
19031
|
getCurrentBranch: ()=>this.currentBranch,
|
|
17865
19032
|
getCommitId: ()=>this.commitId,
|
|
@@ -17879,7 +19046,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17879
19046
|
getAuthToken: ()=>this.authToken,
|
|
17880
19047
|
setAuthToken: (token)=>{
|
|
17881
19048
|
this.authToken = token;
|
|
17882
|
-
this.wsClient?.updateAuthToken(token);
|
|
19049
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
17883
19050
|
},
|
|
17884
19051
|
getCurrentBranch: ()=>this.currentBranch,
|
|
17885
19052
|
getCommitId: ()=>this.commitId,
|
|
@@ -17892,15 +19059,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17892
19059
|
isPaused: ()=>this.isPausedForUserInput
|
|
17893
19060
|
});
|
|
17894
19061
|
this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
|
|
17895
|
-
this.saveProcessor.setPostSaveCallback(()=>this.
|
|
19062
|
+
this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
|
|
17896
19063
|
this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
|
|
17897
19064
|
this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
|
|
17898
19065
|
this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
|
|
17899
19066
|
this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
|
|
19067
|
+
this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
|
|
19068
|
+
this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
|
|
19069
|
+
this.queueDependencyRefresh("anvil.yaml dependencies changed");
|
|
19070
|
+
});
|
|
17900
19071
|
this.saveProcessor.on("validation-failed", (data)=>this.emit("validation-failed", data));
|
|
17901
19072
|
this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
|
|
17902
19073
|
this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
|
|
17903
19074
|
}
|
|
19075
|
+
async syncAfterSave(savedFilePaths) {
|
|
19076
|
+
const status = await this.gitService.getStatus();
|
|
19077
|
+
const locallyChangedFiles = buildLocalChangesSet(status);
|
|
19078
|
+
if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
|
|
19079
|
+
return this.syncManager.syncRemoteChanges(locallyChangedFiles);
|
|
19080
|
+
}
|
|
17904
19081
|
async connectWebSocket() {
|
|
17905
19082
|
this.wsClient = new WebSocketClient({
|
|
17906
19083
|
appId: this.appId,
|
|
@@ -17909,7 +19086,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17909
19086
|
currentBranch: this.currentBranch,
|
|
17910
19087
|
editSession: this.editSession,
|
|
17911
19088
|
username: this.username,
|
|
17912
|
-
environment: this.environment
|
|
19089
|
+
environment: this.environment,
|
|
19090
|
+
dependencyWatch: this.dependencyWatchEnabled,
|
|
19091
|
+
dependencySubscriptions: this.dependencyWatchEnabled ? await this.getDependencyWatchSubscriptions() : []
|
|
17913
19092
|
});
|
|
17914
19093
|
this.wsClient.on("env-state", ({ envs })=>{
|
|
17915
19094
|
for (const env of envs)this.handleEnvironmentBinding(env, "snapshot");
|
|
@@ -17917,7 +19096,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17917
19096
|
this.wsClient.on("env-update", (env)=>{
|
|
17918
19097
|
this.handleEnvironmentBinding(env, "update");
|
|
17919
19098
|
});
|
|
17920
|
-
this.wsClient.on("repo-update", async ({ ref, commitHash, saveSeq })=>{
|
|
19099
|
+
this.wsClient.on("repo-update", async ({ appId, ref, commitHash, saveSeq })=>{
|
|
19100
|
+
if (appId !== this.appId) return void await this.handleDependencyRepoUpdate(appId, ref, commitHash);
|
|
17921
19101
|
if (saveSeq?.session === this.editSession) return void logger_logger.debug("Ignoring update from our own session");
|
|
17922
19102
|
const branchName = ref.replace(/^refs\/heads\//, "");
|
|
17923
19103
|
if (branchName !== this.currentBranch) return void logger_logger.debug(`Ignoring update to different branch: ${branchName} (current: ${this.currentBranch})`);
|
|
@@ -17939,6 +19119,55 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17939
19119
|
});
|
|
17940
19120
|
await this.wsClient.connect();
|
|
17941
19121
|
}
|
|
19122
|
+
async handleDependencyRepoUpdate(appId, ref, commitHash) {
|
|
19123
|
+
if (!this.dependencyWatchEnabled) return;
|
|
19124
|
+
const branchName = ref.replace(/^refs\/heads\//, "");
|
|
19125
|
+
const subscription = this.wsClient?.getDependencySubscriptions().find((candidate)=>candidate.appId === appId && candidate.branch === branchName);
|
|
19126
|
+
if (!subscription) return void logger_logger.debug(`Ignoring update for unwatched dependency app ${appId} on ${branchName}`);
|
|
19127
|
+
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)})`));
|
|
19128
|
+
await this.queueDependencyRefresh(`dependency ${appId} updated`);
|
|
19129
|
+
}
|
|
19130
|
+
async queueDependencyRefresh(reason) {
|
|
19131
|
+
if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
|
|
19132
|
+
if (this.isRefreshingDependencies) {
|
|
19133
|
+
this.hasPendingDependencyRefresh = true;
|
|
19134
|
+
return;
|
|
19135
|
+
}
|
|
19136
|
+
this.isRefreshingDependencies = true;
|
|
19137
|
+
try {
|
|
19138
|
+
do {
|
|
19139
|
+
this.hasPendingDependencyRefresh = false;
|
|
19140
|
+
await this.refreshDependencies(reason);
|
|
19141
|
+
}while (this.hasPendingDependencyRefresh && !this.isCleanedUp);
|
|
19142
|
+
} finally{
|
|
19143
|
+
this.isRefreshingDependencies = false;
|
|
19144
|
+
}
|
|
19145
|
+
}
|
|
19146
|
+
async refreshDependencies(reason) {
|
|
19147
|
+
if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
|
|
19148
|
+
logger_logger.progress("dependency-refresh", "startup" === reason ? "Fetching dependencies" : "Refreshing dependencies");
|
|
19149
|
+
try {
|
|
19150
|
+
const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
|
|
19151
|
+
const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
|
|
19152
|
+
includeDocs: true
|
|
19153
|
+
});
|
|
19154
|
+
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
19155
|
+
const nextSubscriptions = await this.getDependencyWatchSubscriptions();
|
|
19156
|
+
this.reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions);
|
|
19157
|
+
logger_logger.progressEnd("dependency-refresh", response.dependencies.length ? `Fetched ${response.dependencies.length} dependencies` : "Dependencies already up to date");
|
|
19158
|
+
} catch (error) {
|
|
19159
|
+
logger_logger.progressEnd("dependency-refresh", "Failed");
|
|
19160
|
+
logger_logger.warn(external_chalk_default().yellow(`Failed to refresh dependencies: ${errors_getErrorMessage(error)}`));
|
|
19161
|
+
}
|
|
19162
|
+
}
|
|
19163
|
+
async getDependencyWatchSubscriptions() {
|
|
19164
|
+
return getDependencyWatchSubscriptionsFromManifest(await readDepsCacheManifest(this.repoPath));
|
|
19165
|
+
}
|
|
19166
|
+
reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions) {
|
|
19167
|
+
if (dependencySubscriptionsEqual(previousSubscriptions, nextSubscriptions)) return;
|
|
19168
|
+
this.wsClient?.updateDependencySubscriptions(nextSubscriptions);
|
|
19169
|
+
if (this.wsClient?.isConnected() && previousSubscriptions.length > 0) this.wsClient.reconnect();
|
|
19170
|
+
}
|
|
17942
19171
|
async handleEnvironmentBinding(env, source) {
|
|
17943
19172
|
if (!this.environment?.envPid || env.envPid !== this.environment.envPid) return;
|
|
17944
19173
|
this.environment = {
|
|
@@ -17961,6 +19190,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17961
19190
|
async switchToEnvironmentBranch(targetBranch, source) {
|
|
17962
19191
|
if (this.isCleanedUp) return;
|
|
17963
19192
|
this.isSwitchingForEnvironment = true;
|
|
19193
|
+
this.environmentSwitchTargetBranch = targetBranch;
|
|
17964
19194
|
const oldPausedState = this.isPausedForUserInput;
|
|
17965
19195
|
try {
|
|
17966
19196
|
const status = await this.gitService.getStatus();
|
|
@@ -17985,6 +19215,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17985
19215
|
]);
|
|
17986
19216
|
}
|
|
17987
19217
|
logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
|
|
19218
|
+
this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
|
|
19219
|
+
this.wsClient?.updateAuthToken?.(this.authToken);
|
|
17988
19220
|
const fetchUrl = getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
|
|
17989
19221
|
const envRemoteRef = environmentRemoteRefForBranch(targetBranch);
|
|
17990
19222
|
await this.gitService.fetch(fetchUrl, environmentFetchRefSpecForBranch(targetBranch));
|
|
@@ -18000,6 +19232,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18000
19232
|
this.lastBranchChangeTime = Date.now();
|
|
18001
19233
|
this.fileWatcher?.setCurrentBranch(targetBranch);
|
|
18002
19234
|
this.wsClient?.updateBranch(targetBranch);
|
|
19235
|
+
this.queueDependencyRefresh("branch changed");
|
|
18003
19236
|
await this.initializeLocalChangeFingerprint();
|
|
18004
19237
|
this.emit("branch-changed", {
|
|
18005
19238
|
oldBranch,
|
|
@@ -18019,6 +19252,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18019
19252
|
} finally{
|
|
18020
19253
|
this.isPausedForUserInput = oldPausedState;
|
|
18021
19254
|
this.isSwitchingForEnvironment = false;
|
|
19255
|
+
this.environmentSwitchTargetBranch = null;
|
|
18022
19256
|
}
|
|
18023
19257
|
const pendingSwitch = this.pendingEnvironmentBranchSwitch;
|
|
18024
19258
|
this.pendingEnvironmentBranchSwitch = null;
|
|
@@ -18070,7 +19304,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18070
19304
|
});
|
|
18071
19305
|
}
|
|
18072
19306
|
async syncRemoteChanges(knownLocalChanges) {
|
|
18073
|
-
|
|
19307
|
+
const previousDependencies = this.dependencyWatchEnabled ? await readAnvilYamlDependencies(this.repoPath) : void 0;
|
|
19308
|
+
const result = await this.syncManager.syncRemoteChanges(knownLocalChanges);
|
|
19309
|
+
if (this.dependencyWatchEnabled && void 0 !== previousDependencies) {
|
|
19310
|
+
const currentDependencies = await readAnvilYamlDependencies(this.repoPath);
|
|
19311
|
+
if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) await this.queueDependencyRefresh("anvil.yaml dependencies changed");
|
|
19312
|
+
}
|
|
19313
|
+
return result;
|
|
18074
19314
|
}
|
|
18075
19315
|
cleanup() {
|
|
18076
19316
|
this.isCleanedUp = true;
|
|
@@ -18100,15 +19340,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18100
19340
|
getCurrentBranch: async ()=>this.gitService.getCurrentBranch()
|
|
18101
19341
|
});
|
|
18102
19342
|
this.fileWatcher.on("branch-change", ({ newBranch })=>{
|
|
18103
|
-
|
|
18104
|
-
logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
|
|
18105
|
-
this.currentBranch = newBranch;
|
|
18106
|
-
this.lastBranchChangeTime = Date.now();
|
|
18107
|
-
this.wsClient?.updateBranch(newBranch);
|
|
18108
|
-
this.emit("branch-changed", {
|
|
18109
|
-
oldBranch,
|
|
18110
|
-
newBranch
|
|
18111
|
-
});
|
|
19343
|
+
this.handleFileWatcherBranchChange(newBranch);
|
|
18112
19344
|
});
|
|
18113
19345
|
this.fileWatcher.on("index-change", ()=>{
|
|
18114
19346
|
this.saveProcessor?.queueSave();
|
|
@@ -18125,6 +19357,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18125
19357
|
this.startLocalChangeFallbackPolling();
|
|
18126
19358
|
await new Promise(()=>{});
|
|
18127
19359
|
}
|
|
19360
|
+
handleFileWatcherBranchChange(newBranch) {
|
|
19361
|
+
if (this.isSwitchingForEnvironment && newBranch === this.environmentSwitchTargetBranch) return void logger_logger.debug(`Ignoring FileWatcher branch-change during environment switch: ${newBranch}`);
|
|
19362
|
+
const oldBranch = this.currentBranch;
|
|
19363
|
+
logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
|
|
19364
|
+
this.currentBranch = newBranch;
|
|
19365
|
+
this.lastBranchChangeTime = Date.now();
|
|
19366
|
+
this.wsClient?.updateBranch(newBranch);
|
|
19367
|
+
this.queueDependencyRefresh("branch changed");
|
|
19368
|
+
this.emit("external-branch-changed", {
|
|
19369
|
+
oldBranch,
|
|
19370
|
+
newBranch
|
|
19371
|
+
});
|
|
19372
|
+
this.emit("branch-changed", {
|
|
19373
|
+
oldBranch,
|
|
19374
|
+
newBranch
|
|
19375
|
+
});
|
|
19376
|
+
}
|
|
18128
19377
|
startWatchingInBackground(onError) {
|
|
18129
19378
|
this.startWatching().catch((error)=>{
|
|
18130
19379
|
this.cleanup();
|
|
@@ -18250,6 +19499,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18250
19499
|
return false;
|
|
18251
19500
|
}
|
|
18252
19501
|
}
|
|
19502
|
+
function dependencySubscriptionsEqual(left, right) {
|
|
19503
|
+
return subscriptionKeys(left).join("\n") === subscriptionKeys(right).join("\n");
|
|
19504
|
+
}
|
|
19505
|
+
function subscriptionKeys(subscriptions) {
|
|
19506
|
+
return subscriptions.map((subscription)=>`${subscription.appId}\0${subscription.branch}`).sort();
|
|
19507
|
+
}
|
|
18253
19508
|
async function validateAnvilApp(repoPath) {
|
|
18254
19509
|
const anvilYamlPath = external_path_default().join(repoPath, "anvil.yaml");
|
|
18255
19510
|
if (!external_fs_.existsSync(anvilYamlPath)) throw createValidationError.missingYaml(repoPath);
|
|
@@ -19628,38 +20883,419 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
19628
20883
|
await checkout_openPathInEditorOrDefault(result.destinationPath, preferredEditorCommand);
|
|
19629
20884
|
logger_logger.info(external_chalk_default().gray(`Opened ${result.destinationDisplay}`));
|
|
19630
20885
|
}
|
|
19631
|
-
if (preferredEditorCommand && !options.open) if (isCommandAvailable(preferredEditorCommand)) logger_logger.info(external_chalk_default().cyan(`Next: ${preferredEditorCommand} ${result.destinationDisplay}`));
|
|
19632
|
-
else logger_logger.warn(`Preferred editor command '${preferredEditorCommand}' was not found in PATH. Run 'anvil configure' to choose an installed editor.`);
|
|
20886
|
+
if (preferredEditorCommand && !options.open) if (isCommandAvailable(preferredEditorCommand)) logger_logger.info(external_chalk_default().cyan(`Next: ${preferredEditorCommand} ${result.destinationDisplay}`));
|
|
20887
|
+
else logger_logger.warn(`Preferred editor command '${preferredEditorCommand}' was not found in PATH. Run 'anvil configure' to choose an installed editor.`);
|
|
20888
|
+
}
|
|
20889
|
+
async function checkout_openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps) {
|
|
20890
|
+
await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
|
|
20891
|
+
}
|
|
20892
|
+
function registerCheckoutCommand(program) {
|
|
20893
|
+
const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
|
|
20894
|
+
try {
|
|
20895
|
+
const globalOptions = command?.optsWithGlobals() || options || {};
|
|
20896
|
+
if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
|
|
20897
|
+
await executeCheckoutInteractive({
|
|
20898
|
+
input,
|
|
20899
|
+
directory,
|
|
20900
|
+
open: options?.open,
|
|
20901
|
+
branch: options?.branch,
|
|
20902
|
+
depth: options?.depth,
|
|
20903
|
+
singleBranch: options?.singleBranch,
|
|
20904
|
+
origin: options?.origin,
|
|
20905
|
+
quiet: options?.quiet,
|
|
20906
|
+
verbose: globalOptions.verbose,
|
|
20907
|
+
url: options?.url,
|
|
20908
|
+
user: options?.user,
|
|
20909
|
+
force: options?.force,
|
|
20910
|
+
query: options?.query
|
|
20911
|
+
}, defaultCheckoutDeps);
|
|
20912
|
+
} catch (e) {
|
|
20913
|
+
logger_logger.error("Error: " + errors_getErrorMessage(e));
|
|
20914
|
+
process.exit(1);
|
|
20915
|
+
}
|
|
20916
|
+
});
|
|
20917
|
+
checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
|
|
20918
|
+
}
|
|
20919
|
+
const defaultGitAuthDoctorDeps = {
|
|
20920
|
+
getAppAuthBinding: getAppAuthBinding,
|
|
20921
|
+
getAccountsForUrl: auth_getAccountsForUrl,
|
|
20922
|
+
hasTokensForUrl: hasTokensForUrl,
|
|
20923
|
+
runCredentialFill: runGitCredentialFill
|
|
20924
|
+
};
|
|
20925
|
+
function getHighestStatus(statuses) {
|
|
20926
|
+
if (statuses.includes("error")) return "error";
|
|
20927
|
+
if (statuses.includes("warning")) return "warning";
|
|
20928
|
+
return "ok";
|
|
20929
|
+
}
|
|
20930
|
+
function finding(status, code, message) {
|
|
20931
|
+
return {
|
|
20932
|
+
status,
|
|
20933
|
+
code,
|
|
20934
|
+
message
|
|
20935
|
+
};
|
|
20936
|
+
}
|
|
20937
|
+
function redactSecrets(value) {
|
|
20938
|
+
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>");
|
|
20939
|
+
}
|
|
20940
|
+
function splitShellCommand(command) {
|
|
20941
|
+
const tokens = [];
|
|
20942
|
+
let current = "";
|
|
20943
|
+
let state = "normal";
|
|
20944
|
+
for(let i = 0; i < command.length; i++){
|
|
20945
|
+
const ch = command[i];
|
|
20946
|
+
if ("single" === state) {
|
|
20947
|
+
if ("'" === ch) state = "normal";
|
|
20948
|
+
else current += ch;
|
|
20949
|
+
continue;
|
|
20950
|
+
}
|
|
20951
|
+
if ("double" === state) {
|
|
20952
|
+
if ("\"" === ch) state = "normal";
|
|
20953
|
+
else if ("\\" === ch && i + 1 < command.length && [
|
|
20954
|
+
"$",
|
|
20955
|
+
"`",
|
|
20956
|
+
"\"",
|
|
20957
|
+
"\\",
|
|
20958
|
+
"\n"
|
|
20959
|
+
].includes(command[i + 1])) current += command[++i];
|
|
20960
|
+
else current += ch;
|
|
20961
|
+
continue;
|
|
20962
|
+
}
|
|
20963
|
+
if (/\s/.test(ch)) {
|
|
20964
|
+
if (current) {
|
|
20965
|
+
tokens.push(current);
|
|
20966
|
+
current = "";
|
|
20967
|
+
}
|
|
20968
|
+
} else if ("'" === ch) state = "single";
|
|
20969
|
+
else if ("\"" === ch) state = "double";
|
|
20970
|
+
else if ("\\" === ch && i + 1 < command.length) current += command[++i];
|
|
20971
|
+
else current += ch;
|
|
20972
|
+
}
|
|
20973
|
+
if ("normal" !== state) throw new Error("Unterminated quote in helper command");
|
|
20974
|
+
if (current) tokens.push(current);
|
|
20975
|
+
return tokens;
|
|
20976
|
+
}
|
|
20977
|
+
function parseCredentialHelperCommand(command) {
|
|
20978
|
+
const trimmed = command.trim();
|
|
20979
|
+
if (!trimmed || !trimmed.startsWith("!")) return {};
|
|
20980
|
+
const tokens = splitShellCommand(trimmed.slice(1).trim());
|
|
20981
|
+
return {
|
|
20982
|
+
execPath: tokens[0],
|
|
20983
|
+
helperPath: tokens[1]
|
|
20984
|
+
};
|
|
20985
|
+
}
|
|
20986
|
+
function readHelperModulePath(helperPath) {
|
|
20987
|
+
try {
|
|
20988
|
+
const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
|
|
20989
|
+
const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
|
|
20990
|
+
return match?.[2];
|
|
20991
|
+
} catch {
|
|
20992
|
+
return;
|
|
20993
|
+
}
|
|
20994
|
+
}
|
|
20995
|
+
function inspectCredentialHelperCommand(raw) {
|
|
20996
|
+
const details = {
|
|
20997
|
+
raw
|
|
20998
|
+
};
|
|
20999
|
+
try {
|
|
21000
|
+
const parsed = parseCredentialHelperCommand(raw);
|
|
21001
|
+
details.execPath = parsed.execPath;
|
|
21002
|
+
details.helperPath = parsed.helperPath;
|
|
21003
|
+
} catch (e) {
|
|
21004
|
+
details.parseError = e.message;
|
|
21005
|
+
return details;
|
|
21006
|
+
}
|
|
21007
|
+
if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
|
|
21008
|
+
if (details.helperPath) {
|
|
21009
|
+
details.helperExists = external_fs_default().existsSync(details.helperPath);
|
|
21010
|
+
if (details.helperExists) {
|
|
21011
|
+
try {
|
|
21012
|
+
const stat = external_fs_default().statSync(details.helperPath);
|
|
21013
|
+
details.helperExecutable = (73 & stat.mode) !== 0;
|
|
21014
|
+
} catch {
|
|
21015
|
+
details.helperExecutable = false;
|
|
21016
|
+
}
|
|
21017
|
+
details.modulePath = readHelperModulePath(details.helperPath);
|
|
21018
|
+
if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
|
|
21019
|
+
}
|
|
21020
|
+
}
|
|
21021
|
+
return details;
|
|
21022
|
+
}
|
|
21023
|
+
function detectAnvilRemote(remote) {
|
|
21024
|
+
const fetchUrl = remote.refs.fetch;
|
|
21025
|
+
const out = {
|
|
21026
|
+
name: remote.name,
|
|
21027
|
+
fetchUrl
|
|
21028
|
+
};
|
|
21029
|
+
if (!fetchUrl) return out;
|
|
21030
|
+
const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
|
|
21031
|
+
if (httpMatch) {
|
|
21032
|
+
const [, protocol, host, appId] = httpMatch;
|
|
21033
|
+
return {
|
|
21034
|
+
...out,
|
|
21035
|
+
appId,
|
|
21036
|
+
anvilUrl: normalizeAnvilUrl(`${protocol}://${host}`),
|
|
21037
|
+
transport: "https"
|
|
21038
|
+
};
|
|
21039
|
+
}
|
|
21040
|
+
const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
|
|
21041
|
+
if (sshMatch) {
|
|
21042
|
+
const [, , host, appId] = sshMatch;
|
|
21043
|
+
return {
|
|
21044
|
+
...out,
|
|
21045
|
+
appId,
|
|
21046
|
+
anvilUrl: normalizeAnvilUrl(host),
|
|
21047
|
+
transport: "ssh"
|
|
21048
|
+
};
|
|
21049
|
+
}
|
|
21050
|
+
return out;
|
|
21051
|
+
}
|
|
21052
|
+
async function getLocalConfigValues(repoPath, key) {
|
|
21053
|
+
try {
|
|
21054
|
+
const output = await esm_default(repoPath).raw([
|
|
21055
|
+
"config",
|
|
21056
|
+
"--local",
|
|
21057
|
+
"--get-all",
|
|
21058
|
+
key
|
|
21059
|
+
]);
|
|
21060
|
+
const values = output.split(/\r?\n/);
|
|
21061
|
+
if ("" === values[values.length - 1]) values.pop();
|
|
21062
|
+
return values;
|
|
21063
|
+
} catch {
|
|
21064
|
+
return [];
|
|
21065
|
+
}
|
|
19633
21066
|
}
|
|
19634
|
-
|
|
19635
|
-
|
|
21067
|
+
function buildCredentialInput(anvilUrl, appId) {
|
|
21068
|
+
const url = new URL(anvilUrl);
|
|
21069
|
+
return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
|
|
21070
|
+
}
|
|
21071
|
+
async function runGitCredentialFill(repoPath, input) {
|
|
21072
|
+
return new Promise((resolve)=>{
|
|
21073
|
+
const child = (0, external_child_process_namespaceObject.spawn)("git", [
|
|
21074
|
+
"credential",
|
|
21075
|
+
"fill"
|
|
21076
|
+
], {
|
|
21077
|
+
cwd: repoPath,
|
|
21078
|
+
env: {
|
|
21079
|
+
...process.env,
|
|
21080
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
21081
|
+
},
|
|
21082
|
+
stdio: [
|
|
21083
|
+
"pipe",
|
|
21084
|
+
"pipe",
|
|
21085
|
+
"pipe"
|
|
21086
|
+
]
|
|
21087
|
+
});
|
|
21088
|
+
let stdout = "";
|
|
21089
|
+
let stderr = "";
|
|
21090
|
+
child.stdout.setEncoding("utf8");
|
|
21091
|
+
child.stderr.setEncoding("utf8");
|
|
21092
|
+
child.stdout.on("data", (chunk)=>{
|
|
21093
|
+
stdout += chunk;
|
|
21094
|
+
});
|
|
21095
|
+
child.stderr.on("data", (chunk)=>{
|
|
21096
|
+
stderr += chunk;
|
|
21097
|
+
});
|
|
21098
|
+
child.on("error", (error)=>{
|
|
21099
|
+
resolve({
|
|
21100
|
+
exitCode: null,
|
|
21101
|
+
stdout: redactSecrets(stdout),
|
|
21102
|
+
stderr: redactSecrets(stderr),
|
|
21103
|
+
error: redactSecrets(error.message)
|
|
21104
|
+
});
|
|
21105
|
+
});
|
|
21106
|
+
child.on("close", (code)=>{
|
|
21107
|
+
resolve({
|
|
21108
|
+
exitCode: code,
|
|
21109
|
+
stdout: redactSecrets(stdout),
|
|
21110
|
+
stderr: redactSecrets(stderr)
|
|
21111
|
+
});
|
|
21112
|
+
});
|
|
21113
|
+
child.stdin.end(input);
|
|
21114
|
+
});
|
|
19636
21115
|
}
|
|
19637
|
-
function
|
|
19638
|
-
const
|
|
21116
|
+
async function buildAppReport(repoPath, remote, deps) {
|
|
21117
|
+
const appId = remote.appId;
|
|
21118
|
+
const anvilUrl = remote.anvilUrl;
|
|
21119
|
+
const url = new URL(anvilUrl);
|
|
21120
|
+
const credentialScope = `${url.protocol}//${url.host}`;
|
|
21121
|
+
const helperConfigKey = `credential.${credentialScope}.helper`;
|
|
21122
|
+
const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
|
|
21123
|
+
const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
|
|
21124
|
+
const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
|
|
21125
|
+
const findings = [];
|
|
21126
|
+
if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
|
|
21127
|
+
else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
|
|
21128
|
+
for (const helper of helperDetails){
|
|
21129
|
+
if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
|
|
21130
|
+
if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
|
|
21131
|
+
if (helper.helperPath) {
|
|
21132
|
+
if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
|
|
21133
|
+
else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
|
|
21134
|
+
} else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
|
|
21135
|
+
if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
|
|
21136
|
+
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}`));
|
|
21137
|
+
}
|
|
21138
|
+
const binding = await deps.getAppAuthBinding(repoPath, appId);
|
|
21139
|
+
if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
|
|
21140
|
+
if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
|
|
21141
|
+
const authUrl = binding.url ? normalizeAnvilUrl(binding.url) : anvilUrl;
|
|
21142
|
+
let accounts = [];
|
|
21143
|
+
let authUsername = binding.username;
|
|
21144
|
+
let hasUsableAuth = false;
|
|
21145
|
+
try {
|
|
21146
|
+
accounts = await deps.getAccountsForUrl(authUrl);
|
|
21147
|
+
if (authUsername || 1 !== accounts.length) {
|
|
21148
|
+
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.`));
|
|
21149
|
+
} else authUsername = accounts[0];
|
|
21150
|
+
hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
|
|
21151
|
+
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}.`));
|
|
21152
|
+
} catch (e) {
|
|
21153
|
+
findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
|
|
21154
|
+
}
|
|
21155
|
+
const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
|
|
21156
|
+
const credentialSimulation = {
|
|
21157
|
+
status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
|
|
21158
|
+
exitCode: fillResult.exitCode,
|
|
21159
|
+
stdout: redactSecrets(fillResult.stdout),
|
|
21160
|
+
stderr: redactSecrets(fillResult.stderr),
|
|
21161
|
+
error: fillResult.error ? redactSecrets(fillResult.error) : void 0
|
|
21162
|
+
};
|
|
21163
|
+
if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
|
|
21164
|
+
const report = {
|
|
21165
|
+
appId,
|
|
21166
|
+
anvilUrl,
|
|
21167
|
+
remoteName: remote.name,
|
|
21168
|
+
remoteUrl: remote.fetchUrl,
|
|
21169
|
+
credentialScope,
|
|
21170
|
+
helperConfigKey,
|
|
21171
|
+
helperValues,
|
|
21172
|
+
helperDetails,
|
|
21173
|
+
binding: {
|
|
21174
|
+
url: binding.url,
|
|
21175
|
+
username: binding.username,
|
|
21176
|
+
hasUrl: !!binding.url,
|
|
21177
|
+
hasUsername: !!binding.username
|
|
21178
|
+
},
|
|
21179
|
+
auth: {
|
|
21180
|
+
url: authUrl,
|
|
21181
|
+
username: authUsername,
|
|
21182
|
+
accounts,
|
|
21183
|
+
hasUsableAuth
|
|
21184
|
+
},
|
|
21185
|
+
credentialSimulation,
|
|
21186
|
+
findings
|
|
21187
|
+
};
|
|
21188
|
+
return report;
|
|
21189
|
+
}
|
|
21190
|
+
async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
|
|
21191
|
+
const deps = options.deps ?? defaultGitAuthDoctorDeps;
|
|
21192
|
+
const inputPath = external_path_default().resolve(repoPathInput);
|
|
21193
|
+
const git = esm_default(inputPath);
|
|
21194
|
+
const repoPath = (await git.raw([
|
|
21195
|
+
"rev-parse",
|
|
21196
|
+
"--show-toplevel"
|
|
21197
|
+
])).trim();
|
|
21198
|
+
const rawGitDir = (await git.raw([
|
|
21199
|
+
"rev-parse",
|
|
21200
|
+
"--git-dir"
|
|
21201
|
+
])).trim();
|
|
21202
|
+
const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
|
|
21203
|
+
setRepoContext(repoPath);
|
|
21204
|
+
const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
|
|
21205
|
+
const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
|
|
21206
|
+
const findings = [];
|
|
21207
|
+
if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
|
|
21208
|
+
const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
|
|
21209
|
+
const status = getHighestStatus([
|
|
21210
|
+
...findings.map((item)=>item.status),
|
|
21211
|
+
...apps.flatMap((app)=>app.findings.map((item)=>item.status))
|
|
21212
|
+
]);
|
|
21213
|
+
return {
|
|
21214
|
+
repoPath,
|
|
21215
|
+
gitDir,
|
|
21216
|
+
remotes,
|
|
21217
|
+
apps,
|
|
21218
|
+
findings,
|
|
21219
|
+
status
|
|
21220
|
+
};
|
|
21221
|
+
}
|
|
21222
|
+
function formatStatus(status) {
|
|
21223
|
+
if ("ok" === status) return external_chalk_default().green("[ok]");
|
|
21224
|
+
if ("warning" === status) return external_chalk_default().yellow("[warn]");
|
|
21225
|
+
return external_chalk_default().red("[error]");
|
|
21226
|
+
}
|
|
21227
|
+
function formatGitAuthDoctorReport(report) {
|
|
21228
|
+
const lines = [];
|
|
21229
|
+
lines.push(external_chalk_default().bold("Git auth doctor"));
|
|
21230
|
+
lines.push(`Repository: ${report.repoPath}`);
|
|
21231
|
+
lines.push(`Git dir: ${report.gitDir}`);
|
|
21232
|
+
lines.push(`Overall: ${formatStatus(report.status)}`);
|
|
21233
|
+
lines.push("");
|
|
21234
|
+
lines.push(external_chalk_default().bold("Detected remotes:"));
|
|
21235
|
+
if (0 === report.remotes.length) lines.push(" (none)");
|
|
21236
|
+
else for (const remote of report.remotes){
|
|
21237
|
+
const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
|
|
21238
|
+
lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
|
|
21239
|
+
}
|
|
21240
|
+
for (const app of report.apps){
|
|
21241
|
+
lines.push("");
|
|
21242
|
+
lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
|
|
21243
|
+
lines.push(` URL: ${app.anvilUrl}`);
|
|
21244
|
+
lines.push(` Credential scope: ${app.credentialScope}`);
|
|
21245
|
+
lines.push(` Helper config: ${app.helperConfigKey}`);
|
|
21246
|
+
if (0 === app.helperValues.length) lines.push(" (none)");
|
|
21247
|
+
else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
|
|
21248
|
+
for (const helper of app.helperDetails){
|
|
21249
|
+
lines.push(` Helper command: ${helper.raw}`);
|
|
21250
|
+
lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
|
|
21251
|
+
lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
|
|
21252
|
+
if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
|
|
21253
|
+
}
|
|
21254
|
+
lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
|
|
21255
|
+
lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
|
|
21256
|
+
lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
|
|
21257
|
+
if (app.credentialSimulation) {
|
|
21258
|
+
lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
|
|
21259
|
+
if (app.credentialSimulation.stdout.trim()) {
|
|
21260
|
+
lines.push(" stdout:");
|
|
21261
|
+
for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
|
|
21262
|
+
}
|
|
21263
|
+
if (app.credentialSimulation.stderr.trim()) {
|
|
21264
|
+
lines.push(" stderr:");
|
|
21265
|
+
for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
|
|
21266
|
+
}
|
|
21267
|
+
}
|
|
21268
|
+
if (app.findings.length > 0) {
|
|
21269
|
+
lines.push(" Findings:");
|
|
21270
|
+
for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
|
|
21271
|
+
}
|
|
21272
|
+
}
|
|
21273
|
+
if (report.findings.length > 0) {
|
|
21274
|
+
lines.push("");
|
|
21275
|
+
lines.push(external_chalk_default().bold("Repository findings:"));
|
|
21276
|
+
for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
|
|
21277
|
+
}
|
|
21278
|
+
return lines.join("\n");
|
|
21279
|
+
}
|
|
21280
|
+
function registerGitAuthCommand(program) {
|
|
21281
|
+
const gitAuthCommand = program.command("git-auth").description("Diagnose Anvil Git authentication");
|
|
21282
|
+
gitAuthCommand.command("doctor").description("Diagnose why plain Git commands cannot authenticate to Anvil").option("--repo <PATH>", "Repository path to diagnose", process.cwd()).action(async (options)=>{
|
|
19639
21283
|
try {
|
|
19640
|
-
const
|
|
19641
|
-
if (
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
branch: options?.branch,
|
|
19647
|
-
depth: options?.depth,
|
|
19648
|
-
singleBranch: options?.singleBranch,
|
|
19649
|
-
origin: options?.origin,
|
|
19650
|
-
quiet: options?.quiet,
|
|
19651
|
-
verbose: globalOptions.verbose,
|
|
19652
|
-
url: options?.url,
|
|
19653
|
-
user: options?.user,
|
|
19654
|
-
force: options?.force,
|
|
19655
|
-
query: options?.query
|
|
19656
|
-
}, defaultCheckoutDeps);
|
|
21284
|
+
const report = await buildGitAuthDoctorReport(options.repo || process.cwd());
|
|
21285
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult("error" !== report.status, {
|
|
21286
|
+
data: report
|
|
21287
|
+
});
|
|
21288
|
+
else logger_logger.info(formatGitAuthDoctorReport(report));
|
|
21289
|
+
if ("error" === report.status) process.exitCode = 1;
|
|
19657
21290
|
} catch (e) {
|
|
19658
|
-
|
|
19659
|
-
|
|
21291
|
+
const message = redactSecrets(e.message);
|
|
21292
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
21293
|
+
error: message
|
|
21294
|
+
});
|
|
21295
|
+
else logger_logger.error(`Git auth doctor failed: ${message}`);
|
|
21296
|
+
process.exitCode = 1;
|
|
19660
21297
|
}
|
|
19661
21298
|
});
|
|
19662
|
-
checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
|
|
19663
21299
|
}
|
|
19664
21300
|
const gitCredential_defaultDeps = {
|
|
19665
21301
|
getValidAuthToken: auth_getValidAuthToken,
|
|
@@ -19667,6 +21303,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
19667
21303
|
getAppAuthBinding: getAppAuthBinding,
|
|
19668
21304
|
cwd: ()=>process.cwd()
|
|
19669
21305
|
};
|
|
21306
|
+
function sanitizeCredentialError(message) {
|
|
21307
|
+
return message.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>");
|
|
21308
|
+
}
|
|
19670
21309
|
function parseGitCredentialRequest(rawInput) {
|
|
19671
21310
|
const out = {};
|
|
19672
21311
|
const lines = rawInput.split(/\r?\n/);
|
|
@@ -19727,7 +21366,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
19727
21366
|
}).description("Internal helper command for Git HTTPS authentication").action(async (operation)=>{
|
|
19728
21367
|
try {
|
|
19729
21368
|
await executeGitCredentialOperation(operation);
|
|
19730
|
-
} catch
|
|
21369
|
+
} catch (e) {
|
|
21370
|
+
if ("1" === process.env["ANVIL_GIT_CREDENTIAL_DEBUG"]) {
|
|
21371
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
21372
|
+
process.stderr.write(`anvil git-credential failed: ${sanitizeCredentialError(message)}\n`);
|
|
21373
|
+
}
|
|
19731
21374
|
process.exit(1);
|
|
19732
21375
|
}
|
|
19733
21376
|
});
|
|
@@ -19745,7 +21388,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
19745
21388
|
authToken,
|
|
19746
21389
|
currentBranch,
|
|
19747
21390
|
commitId,
|
|
19748
|
-
username
|
|
21391
|
+
username,
|
|
21392
|
+
dependencyWatch: false
|
|
19749
21393
|
});
|
|
19750
21394
|
await session.initialize();
|
|
19751
21395
|
await session.syncRemoteChanges();
|
|
@@ -19842,7 +21486,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
19842
21486
|
stagedOnly,
|
|
19843
21487
|
username,
|
|
19844
21488
|
environment,
|
|
19845
|
-
envBranchChangeMode: options.envBranchChangeMode ?? "manual"
|
|
21489
|
+
envBranchChangeMode: options.envBranchChangeMode ?? "manual",
|
|
21490
|
+
dependencyWatch: options.dependencyWatch ?? true
|
|
19846
21491
|
});
|
|
19847
21492
|
session.on("branch-changed", (data)=>{
|
|
19848
21493
|
logger_logger.debug("Event: branch-changed", data);
|
|
@@ -20181,7 +21826,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20181
21826
|
anvilUrl: options.anvilUrl,
|
|
20182
21827
|
stagedOnly: options.stagedOnly,
|
|
20183
21828
|
username: options.username,
|
|
20184
|
-
envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual"
|
|
21829
|
+
envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual",
|
|
21830
|
+
dependencyWatch: options.dependencyWatch ?? true
|
|
20185
21831
|
});
|
|
20186
21832
|
logger_logger.progressEnd("verify");
|
|
20187
21833
|
const currentCategory = getSyncStateCategory(freshSession.syncStatus);
|
|
@@ -20205,7 +21851,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20205
21851
|
anvilUrl: options.anvilUrl,
|
|
20206
21852
|
stagedOnly: options.stagedOnly,
|
|
20207
21853
|
username: options.username,
|
|
20208
|
-
envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual"
|
|
21854
|
+
envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual",
|
|
21855
|
+
dependencyWatch: options.dependencyWatch ?? true
|
|
20209
21856
|
});
|
|
20210
21857
|
logger_logger.progressEnd("init");
|
|
20211
21858
|
return await checkSyncStatusAndStart(newSession, options);
|
|
@@ -20455,7 +22102,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20455
22102
|
anvilUrl,
|
|
20456
22103
|
stagedOnly,
|
|
20457
22104
|
username: options.username,
|
|
20458
|
-
envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual"
|
|
22105
|
+
envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual",
|
|
22106
|
+
dependencyWatch: options.dependencyWatch ?? true
|
|
20459
22107
|
});
|
|
20460
22108
|
logger_logger.progressEnd("init");
|
|
20461
22109
|
const started = await checkSyncStatusAndStart(newSession, options);
|
|
@@ -20542,7 +22190,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20542
22190
|
logger_logger.error("'sync' has been renamed to 'watch'. Please use 'anvil watch' instead.");
|
|
20543
22191
|
process.exit(1);
|
|
20544
22192
|
}
|
|
20545
|
-
const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
|
|
22193
|
+
const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, dependencyWatch = true, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
|
|
20546
22194
|
setRepoContext(repoPath);
|
|
20547
22195
|
try {
|
|
20548
22196
|
const validationResult = await validateAnvilApp(repoPath);
|
|
@@ -20698,7 +22346,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20698
22346
|
anvilUrl,
|
|
20699
22347
|
stagedOnly,
|
|
20700
22348
|
username,
|
|
20701
|
-
envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual"
|
|
22349
|
+
envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual",
|
|
22350
|
+
dependencyWatch
|
|
20702
22351
|
});
|
|
20703
22352
|
logger_logger.progressEnd("init");
|
|
20704
22353
|
if (openAfterValidation) await openWatchPath(resolveWatchOpenPath(repoPath));
|
|
@@ -20709,6 +22358,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20709
22358
|
appId: finalAppId,
|
|
20710
22359
|
anvilUrl,
|
|
20711
22360
|
stagedOnly,
|
|
22361
|
+
dependencyWatch,
|
|
20712
22362
|
username
|
|
20713
22363
|
});
|
|
20714
22364
|
if (!started) process.exit(1);
|
|
@@ -20718,7 +22368,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20718
22368
|
}
|
|
20719
22369
|
}
|
|
20720
22370
|
function registerWatchCommand(program) {
|
|
20721
|
-
const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
|
|
22371
|
+
const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("--no-dependency-watch", "Do not watch and refresh .anvil/deps dependencies").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
|
|
20722
22372
|
const globalOptions = command.optsWithGlobals();
|
|
20723
22373
|
if (void 0 !== globalOptions.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(globalOptions.verbose);
|
|
20724
22374
|
await handleWatchCommand({
|
|
@@ -20726,6 +22376,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20726
22376
|
appid: options.appid,
|
|
20727
22377
|
useFirst: options.first,
|
|
20728
22378
|
stagedOnly: options.stagedOnly,
|
|
22379
|
+
dependencyWatch: options.dependencyWatch,
|
|
20729
22380
|
autoMode: options.auto,
|
|
20730
22381
|
verbose: globalOptions.verbose,
|
|
20731
22382
|
open: options.open,
|
|
@@ -20977,6 +22628,178 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
20977
22628
|
}
|
|
20978
22629
|
});
|
|
20979
22630
|
}
|
|
22631
|
+
function formSpecContext_isPlainObject(value) {
|
|
22632
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
22633
|
+
}
|
|
22634
|
+
function collectFormSpecReferences(value, references) {
|
|
22635
|
+
if ("string" == typeof value) {
|
|
22636
|
+
if (value.startsWith("form:") || /^[^:]+:[^:]+$/.test(value)) references.add(value);
|
|
22637
|
+
return;
|
|
22638
|
+
}
|
|
22639
|
+
if (Array.isArray(value)) {
|
|
22640
|
+
for (const item of value)collectFormSpecReferences(item, references);
|
|
22641
|
+
return;
|
|
22642
|
+
}
|
|
22643
|
+
if (!formSpecContext_isPlainObject(value)) return;
|
|
22644
|
+
for (const [key, nested] of Object.entries(value)){
|
|
22645
|
+
if ("type" === key && "string" == typeof nested && nested.startsWith("form:")) references.add(nested);
|
|
22646
|
+
if ("item_template" === key && "string" == typeof nested) references.add(nested);
|
|
22647
|
+
collectFormSpecReferences(nested, references);
|
|
22648
|
+
}
|
|
22649
|
+
}
|
|
22650
|
+
function formYamlHasFormSpecReferences(form) {
|
|
22651
|
+
const references = new Set();
|
|
22652
|
+
collectFormSpecReferences(form, references);
|
|
22653
|
+
return references.size > 0;
|
|
22654
|
+
}
|
|
22655
|
+
async function readAnvilYamlConfig(appRoot) {
|
|
22656
|
+
try {
|
|
22657
|
+
const content = await external_fs_.promises.readFile(external_path_default().join(appRoot, "anvil.yaml"), "utf8");
|
|
22658
|
+
const parsed = external_js_yaml_default().load(content);
|
|
22659
|
+
return formSpecContext_isPlainObject(parsed) ? parsed : void 0;
|
|
22660
|
+
} catch {
|
|
22661
|
+
return;
|
|
22662
|
+
}
|
|
22663
|
+
}
|
|
22664
|
+
function resolveDependencyPackageName(dependency, dependencyIds, cachedPackageNamesByAppId) {
|
|
22665
|
+
const packageName = dependency.resolution_hints?.package_name;
|
|
22666
|
+
if (packageName) return packageName;
|
|
22667
|
+
const depId = dependency.dep_id;
|
|
22668
|
+
if (!depId) return;
|
|
22669
|
+
const appId = dependency.resolution_hints?.app_id ?? dependencyIds?.[depId];
|
|
22670
|
+
if (!appId) return;
|
|
22671
|
+
return cachedPackageNamesByAppId.get(appId);
|
|
22672
|
+
}
|
|
22673
|
+
async function buildFormSpecPackageContext(sourcePath) {
|
|
22674
|
+
const appRoot = findAnvilAppRoot(sourcePath);
|
|
22675
|
+
if (!appRoot) return;
|
|
22676
|
+
const config = await readAnvilYamlConfig(appRoot);
|
|
22677
|
+
if (!config) return;
|
|
22678
|
+
const manifest = await readDepsCacheManifest(appRoot);
|
|
22679
|
+
const cachedPackageNamesByAppId = new Map((manifest?.cached_dependencies ?? []).map((entry)=>[
|
|
22680
|
+
entry.app_id,
|
|
22681
|
+
entry.package_name
|
|
22682
|
+
]));
|
|
22683
|
+
const dependencyPackageNamesByLogicalDepId = {};
|
|
22684
|
+
const knownPackageNames = new Set();
|
|
22685
|
+
for (const dependency of config.dependencies ?? []){
|
|
22686
|
+
const depId = dependency.dep_id;
|
|
22687
|
+
if (!depId) continue;
|
|
22688
|
+
const packageName = resolveDependencyPackageName(dependency, config.dependency_ids, cachedPackageNamesByAppId);
|
|
22689
|
+
dependencyPackageNamesByLogicalDepId[depId] = packageName;
|
|
22690
|
+
if (packageName) knownPackageNames.add(packageName);
|
|
22691
|
+
}
|
|
22692
|
+
return {
|
|
22693
|
+
appPackageName: config.package_name,
|
|
22694
|
+
knownPackageNames: [
|
|
22695
|
+
...knownPackageNames
|
|
22696
|
+
],
|
|
22697
|
+
dependencyPackageNamesByLogicalDepId
|
|
22698
|
+
};
|
|
22699
|
+
}
|
|
22700
|
+
class ConvertTemplateValidationError extends Error {
|
|
22701
|
+
issues;
|
|
22702
|
+
constructor(message, issues){
|
|
22703
|
+
super(message), this.issues = issues;
|
|
22704
|
+
this.name = "ConvertTemplateValidationError";
|
|
22705
|
+
}
|
|
22706
|
+
}
|
|
22707
|
+
function hasYamlExtension(filePath) {
|
|
22708
|
+
return /\.ya?ml$/i.test(filePath);
|
|
22709
|
+
}
|
|
22710
|
+
function getDefaultConvertedTemplatePath(inputPath) {
|
|
22711
|
+
return inputPath.replace(/\.ya?ml$/i, ".html");
|
|
22712
|
+
}
|
|
22713
|
+
function resolveConvertTemplatePaths(inputPath, options = {}) {
|
|
22714
|
+
const cwd = options.cwd ?? process.cwd();
|
|
22715
|
+
if (!hasYamlExtension(inputPath)) throw new Error("Input template must be a .yaml or .yml file");
|
|
22716
|
+
const sourcePath = external_path_default().resolve(cwd, inputPath);
|
|
22717
|
+
const outputPath = external_path_default().resolve(cwd, options.output ?? getDefaultConvertedTemplatePath(inputPath));
|
|
22718
|
+
if (sourcePath === outputPath) throw new Error("Output path must be different from the input path");
|
|
22719
|
+
return {
|
|
22720
|
+
sourcePath,
|
|
22721
|
+
outputPath
|
|
22722
|
+
};
|
|
22723
|
+
}
|
|
22724
|
+
async function pathExists(filePath) {
|
|
22725
|
+
try {
|
|
22726
|
+
await external_fs_.promises.access(filePath);
|
|
22727
|
+
return true;
|
|
22728
|
+
} catch {
|
|
22729
|
+
return false;
|
|
22730
|
+
}
|
|
22731
|
+
}
|
|
22732
|
+
async function writeFileAtomically(filePath, content) {
|
|
22733
|
+
const directory = external_path_default().dirname(filePath);
|
|
22734
|
+
await external_fs_.promises.mkdir(directory, {
|
|
22735
|
+
recursive: true
|
|
22736
|
+
});
|
|
22737
|
+
const tempPath = external_path_default().join(directory, `.${external_path_default().basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
22738
|
+
await external_fs_.promises.writeFile(tempPath, content, "utf8");
|
|
22739
|
+
await external_fs_.promises.rename(tempPath, filePath);
|
|
22740
|
+
}
|
|
22741
|
+
async function loadParser() {
|
|
22742
|
+
return import("@anvil-works/form-template-parser");
|
|
22743
|
+
}
|
|
22744
|
+
async function convertTemplateFile(inputPath, options = {}) {
|
|
22745
|
+
const { sourcePath, outputPath } = resolveConvertTemplatePaths(inputPath, options);
|
|
22746
|
+
if (!options.force && await pathExists(outputPath)) throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite it.`);
|
|
22747
|
+
const yamlContent = await external_fs_.promises.readFile(sourcePath, "utf8");
|
|
22748
|
+
const validationResult = validateFormTemplate(yamlContent);
|
|
22749
|
+
if (!validationResult.ok) throw new ConvertTemplateValidationError(validationResult.message, validationResult.issues);
|
|
22750
|
+
let template = external_js_yaml_default().load(yamlContent);
|
|
22751
|
+
const parser = await loadParser();
|
|
22752
|
+
const context = await buildFormSpecPackageContext(sourcePath);
|
|
22753
|
+
if (context) template = parser.copyFormYamlWithCanonicalizedFormSpecs(template, context).form;
|
|
22754
|
+
else if (formYamlHasFormSpecReferences(template)) logger_logger.warn("Skipping form-spec canonicalization: no anvil.yaml found for this template");
|
|
22755
|
+
const html = parser.serializeFormTemplateHtml(template, {
|
|
22756
|
+
allowReparse: true
|
|
22757
|
+
});
|
|
22758
|
+
await writeFileAtomically(outputPath, html.endsWith("\n") ? html : `${html}\n`);
|
|
22759
|
+
let removedSource = false;
|
|
22760
|
+
if (!options.keepSource) try {
|
|
22761
|
+
await external_fs_.promises.unlink(sourcePath);
|
|
22762
|
+
removedSource = true;
|
|
22763
|
+
} catch (error) {
|
|
22764
|
+
throw new Error(`Converted template written to ${outputPath}, but failed to remove source ${sourcePath}: ${error.message}`);
|
|
22765
|
+
}
|
|
22766
|
+
return {
|
|
22767
|
+
source: sourcePath,
|
|
22768
|
+
output: outputPath,
|
|
22769
|
+
removedSource
|
|
22770
|
+
};
|
|
22771
|
+
}
|
|
22772
|
+
function registerConvertTemplateCommand(program) {
|
|
22773
|
+
program.command("convert-template <file>").description("Convert an Anvil form template YAML file to HTML template format").option("-o, --output <path>", "Output HTML path").option("-f, --force", "Overwrite the output file if it already exists").option("--keep-source", "Keep the source YAML file after conversion").action(async (file, options)=>{
|
|
22774
|
+
try {
|
|
22775
|
+
const result = await convertTemplateFile(file, options);
|
|
22776
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
22777
|
+
data: result
|
|
22778
|
+
});
|
|
22779
|
+
else {
|
|
22780
|
+
logger_logger.success(`Converted ${result.source} to ${result.output}`);
|
|
22781
|
+
if (result.removedSource) logger_logger.info(`Removed source ${result.source}`);
|
|
22782
|
+
}
|
|
22783
|
+
} catch (error) {
|
|
22784
|
+
if (error instanceof ConvertTemplateValidationError) if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
22785
|
+
error: error.message,
|
|
22786
|
+
data: {
|
|
22787
|
+
file,
|
|
22788
|
+
issues: error.issues
|
|
22789
|
+
}
|
|
22790
|
+
});
|
|
22791
|
+
else {
|
|
22792
|
+
logger_logger.error(`${file} failed validation: ${error.message}`);
|
|
22793
|
+
for (const issue of error.issues)logger_logger.error(` - ${formatValidationPath(issue.path)}: ${issue.message}`);
|
|
22794
|
+
}
|
|
22795
|
+
else if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
22796
|
+
error: error.message
|
|
22797
|
+
});
|
|
22798
|
+
else logger_logger.error(error.message);
|
|
22799
|
+
process.exitCode = 1;
|
|
22800
|
+
}
|
|
22801
|
+
});
|
|
22802
|
+
}
|
|
20980
22803
|
function getVersionInfo(version) {
|
|
20981
22804
|
return {
|
|
20982
22805
|
version,
|
|
@@ -21157,6 +22980,160 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
21157
22980
|
}
|
|
21158
22981
|
});
|
|
21159
22982
|
}
|
|
22983
|
+
async function resolvePrimaryAppId(projectRoot, anvilUrl) {
|
|
22984
|
+
const remoteCandidates = await detectAppIdsFromAllRemotes(projectRoot);
|
|
22985
|
+
if (remoteCandidates.length > 0) return remoteCandidates[0].appId;
|
|
22986
|
+
const lookupCandidates = await detectAppIdsByCommitLookup(projectRoot, {
|
|
22987
|
+
anvilUrl,
|
|
22988
|
+
includeRemotes: false
|
|
22989
|
+
});
|
|
22990
|
+
return lookupCandidates[0]?.appId ?? null;
|
|
22991
|
+
}
|
|
22992
|
+
function registerDepsCommand(program) {
|
|
22993
|
+
const deps = program.command("deps").description("Manage app dependencies");
|
|
22994
|
+
deps.command("fetch").description("Fetch dependencies into .anvil/deps/").option("--commit <commit>", "Fetch deps at specific commit").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
|
|
22995
|
+
try {
|
|
22996
|
+
const projectRoot = process.cwd();
|
|
22997
|
+
const anvilUrl = resolveAnvilUrl();
|
|
22998
|
+
const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
22999
|
+
if (!mainAppId) {
|
|
23000
|
+
logger_logger.error("No Anvil app found in current directory");
|
|
23001
|
+
logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
|
|
23002
|
+
process.exit(1);
|
|
23003
|
+
}
|
|
23004
|
+
logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
|
|
23005
|
+
const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
|
|
23006
|
+
commit: options.commit,
|
|
23007
|
+
includeDocs: !options.excludeDocs
|
|
23008
|
+
});
|
|
23009
|
+
if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
|
|
23010
|
+
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
23011
|
+
if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
|
|
23012
|
+
logger_logger.success(`Fetched ${response.dependencies.length} dependencies:`);
|
|
23013
|
+
for (const dep of response.dependencies)logger_logger.info(` - ${dep.package_name}`);
|
|
23014
|
+
} catch (e) {
|
|
23015
|
+
logger_logger.error(e.message);
|
|
23016
|
+
process.exit(1);
|
|
23017
|
+
}
|
|
23018
|
+
});
|
|
23019
|
+
deps.command("status").description("Show cached dependencies").action(async ()=>{
|
|
23020
|
+
try {
|
|
23021
|
+
const cached = await getCachedPackages(process.cwd());
|
|
23022
|
+
if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
|
|
23023
|
+
logger_logger.info("Cached dependencies:");
|
|
23024
|
+
for (const pkg of cached)logger_logger.info(` - ${pkg}`);
|
|
23025
|
+
} catch (e) {
|
|
23026
|
+
logger_logger.error(e.message);
|
|
23027
|
+
process.exit(1);
|
|
23028
|
+
}
|
|
23029
|
+
});
|
|
23030
|
+
deps.command("clear").description("Clear cached dependencies").action(async ()=>{
|
|
23031
|
+
try {
|
|
23032
|
+
await clearDepsCache(process.cwd());
|
|
23033
|
+
logger_logger.success("Cleared .anvil/deps/");
|
|
23034
|
+
} catch (e) {
|
|
23035
|
+
logger_logger.error(e.message);
|
|
23036
|
+
process.exit(1);
|
|
23037
|
+
}
|
|
23038
|
+
});
|
|
23039
|
+
}
|
|
23040
|
+
const TABLE_MAPPINGS_PATH = ".anvil/table-mappings.json";
|
|
23041
|
+
function tables_formatFetchError(prefix, status, errorBody) {
|
|
23042
|
+
if (!errorBody) return `${prefix}: ${status}`;
|
|
23043
|
+
try {
|
|
23044
|
+
const parsed = JSON.parse(errorBody);
|
|
23045
|
+
if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
|
|
23046
|
+
} catch {}
|
|
23047
|
+
return `${prefix}: ${status} ${errorBody}`;
|
|
23048
|
+
}
|
|
23049
|
+
async function fetchTableMappings(appId, anvilUrl = resolveAnvilUrl()) {
|
|
23050
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
23051
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db`, {
|
|
23052
|
+
headers: {
|
|
23053
|
+
Authorization: `Bearer ${token}`
|
|
23054
|
+
}
|
|
23055
|
+
});
|
|
23056
|
+
if (!resp.ok) {
|
|
23057
|
+
const errorBody = await resp.text();
|
|
23058
|
+
throw new Error(tables_formatFetchError("Failed to fetch table mappings", resp.status, errorBody));
|
|
23059
|
+
}
|
|
23060
|
+
const databases = await resp.json();
|
|
23061
|
+
const allTables = [];
|
|
23062
|
+
for (const db of databases)for (const table of db.tables || [])allTables.push({
|
|
23063
|
+
table_id: table.id,
|
|
23064
|
+
python_name: table.python_name,
|
|
23065
|
+
name: table.name
|
|
23066
|
+
});
|
|
23067
|
+
return {
|
|
23068
|
+
tables: allTables
|
|
23069
|
+
};
|
|
23070
|
+
}
|
|
23071
|
+
async function writeTableMappings(projectRoot, mappings) {
|
|
23072
|
+
const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
|
|
23073
|
+
await promises_default().mkdir(external_path_default().dirname(filePath), {
|
|
23074
|
+
recursive: true
|
|
23075
|
+
});
|
|
23076
|
+
await promises_default().writeFile(filePath, JSON.stringify(mappings, null, 2));
|
|
23077
|
+
}
|
|
23078
|
+
async function readTableMappings(projectRoot) {
|
|
23079
|
+
const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
|
|
23080
|
+
try {
|
|
23081
|
+
const content = await promises_default().readFile(filePath, "utf-8");
|
|
23082
|
+
return JSON.parse(content);
|
|
23083
|
+
} catch {
|
|
23084
|
+
return null;
|
|
23085
|
+
}
|
|
23086
|
+
}
|
|
23087
|
+
async function clearTableMappings(projectRoot) {
|
|
23088
|
+
const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
|
|
23089
|
+
await promises_default().rm(filePath, {
|
|
23090
|
+
force: true
|
|
23091
|
+
});
|
|
23092
|
+
}
|
|
23093
|
+
function registerTablesCommand(program) {
|
|
23094
|
+
const tables = program.command("tables").description("Manage table mappings");
|
|
23095
|
+
tables.command("fetch").description("Fetch table mappings into .anvil/table-mappings.json").action(async ()=>{
|
|
23096
|
+
try {
|
|
23097
|
+
const projectRoot = process.cwd();
|
|
23098
|
+
const anvilUrl = resolveAnvilUrl();
|
|
23099
|
+
const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
23100
|
+
if (!mainAppId) {
|
|
23101
|
+
logger_logger.error("No Anvil app found in current directory");
|
|
23102
|
+
logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
|
|
23103
|
+
process.exit(1);
|
|
23104
|
+
}
|
|
23105
|
+
logger_logger.info("Fetching table mappings...");
|
|
23106
|
+
const response = await fetchTableMappings(mainAppId, anvilUrl);
|
|
23107
|
+
if (!response.tables.length) return void logger_logger.info("No tables found");
|
|
23108
|
+
if (await ensureProjectGitignoreIgnoresAnvilDir(projectRoot)) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
23109
|
+
await writeTableMappings(projectRoot, response);
|
|
23110
|
+
logger_logger.success(`Fetched ${response.tables.length} table mappings`);
|
|
23111
|
+
} catch (e) {
|
|
23112
|
+
logger_logger.error(e.message);
|
|
23113
|
+
process.exit(1);
|
|
23114
|
+
}
|
|
23115
|
+
});
|
|
23116
|
+
tables.command("status").description("Show cached table mappings").action(async ()=>{
|
|
23117
|
+
try {
|
|
23118
|
+
const mappings = await readTableMappings(process.cwd());
|
|
23119
|
+
if (!mappings || !mappings.tables.length) return void logger_logger.info("No cached table mappings (run 'anvil tables fetch')");
|
|
23120
|
+
logger_logger.info("Cached table mappings:");
|
|
23121
|
+
for (const t of mappings.tables)logger_logger.info(` ${t.python_name} (${t.name}) - ID: ${t.table_id}`);
|
|
23122
|
+
} catch (e) {
|
|
23123
|
+
logger_logger.error(e.message);
|
|
23124
|
+
process.exit(1);
|
|
23125
|
+
}
|
|
23126
|
+
});
|
|
23127
|
+
tables.command("clear").description("Clear cached table mappings").action(async ()=>{
|
|
23128
|
+
try {
|
|
23129
|
+
await clearTableMappings(process.cwd());
|
|
23130
|
+
logger_logger.success("Cleared .anvil/table-mappings.json");
|
|
23131
|
+
} catch (e) {
|
|
23132
|
+
logger_logger.error(e.message);
|
|
23133
|
+
process.exit(1);
|
|
23134
|
+
}
|
|
23135
|
+
});
|
|
23136
|
+
}
|
|
21160
23137
|
const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
|
|
21161
23138
|
const VERSION = program_packageJson.version;
|
|
21162
23139
|
setLogger(new CLILogger({
|
|
@@ -21174,6 +23151,123 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
21174
23151
|
}
|
|
21175
23152
|
}).catch(()=>{});
|
|
21176
23153
|
}
|
|
23154
|
+
function collectValidationFiles(dir) {
|
|
23155
|
+
const files = [];
|
|
23156
|
+
const ignoredDirectories = new Set([
|
|
23157
|
+
".anvil",
|
|
23158
|
+
".git",
|
|
23159
|
+
"node_modules",
|
|
23160
|
+
"__pycache__"
|
|
23161
|
+
]);
|
|
23162
|
+
function walk(currentDir) {
|
|
23163
|
+
for (const entry of (0, external_fs_.readdirSync)(currentDir, {
|
|
23164
|
+
withFileTypes: true
|
|
23165
|
+
})){
|
|
23166
|
+
const entryPath = (0, external_path_namespaceObject.join)(currentDir, entry.name);
|
|
23167
|
+
if (entry.isDirectory()) {
|
|
23168
|
+
if (!ignoredDirectories.has(entry.name)) walk(entryPath);
|
|
23169
|
+
continue;
|
|
23170
|
+
}
|
|
23171
|
+
if (entry.isFile() && inferValidationTarget(entryPath)) files.push(entryPath);
|
|
23172
|
+
}
|
|
23173
|
+
}
|
|
23174
|
+
walk(dir);
|
|
23175
|
+
return files.sort();
|
|
23176
|
+
}
|
|
23177
|
+
function validateSingleFile(file) {
|
|
23178
|
+
let fileContent;
|
|
23179
|
+
try {
|
|
23180
|
+
fileContent = (0, external_fs_.readFileSync)(file, "utf8");
|
|
23181
|
+
} catch (error) {
|
|
23182
|
+
return {
|
|
23183
|
+
file,
|
|
23184
|
+
type: "unknown",
|
|
23185
|
+
valid: false,
|
|
23186
|
+
error: `Failed to read ${file}: ${error.message}`,
|
|
23187
|
+
issues: [
|
|
23188
|
+
{
|
|
23189
|
+
path: "",
|
|
23190
|
+
message: error.message
|
|
23191
|
+
}
|
|
23192
|
+
]
|
|
23193
|
+
};
|
|
23194
|
+
}
|
|
23195
|
+
const result = validatePath(file, fileContent);
|
|
23196
|
+
if (result.ok) return {
|
|
23197
|
+
file,
|
|
23198
|
+
type: result.target,
|
|
23199
|
+
valid: true
|
|
23200
|
+
};
|
|
23201
|
+
return {
|
|
23202
|
+
file,
|
|
23203
|
+
type: result.target,
|
|
23204
|
+
valid: false,
|
|
23205
|
+
error: result.message,
|
|
23206
|
+
issues: result.issues
|
|
23207
|
+
};
|
|
23208
|
+
}
|
|
23209
|
+
function logValidationFailure(fileResult) {
|
|
23210
|
+
logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
|
|
23211
|
+
for (const issue of fileResult.issues ?? []){
|
|
23212
|
+
const issuePath = issue.path.length > 0 ? issue.path : "root";
|
|
23213
|
+
logger_logger.error(` - ${issuePath}: ${issue.message}`);
|
|
23214
|
+
}
|
|
23215
|
+
}
|
|
23216
|
+
function handleValidateFile(file) {
|
|
23217
|
+
const fileResult = validateSingleFile(file);
|
|
23218
|
+
if (fileResult.valid) {
|
|
23219
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23220
|
+
data: {
|
|
23221
|
+
file: fileResult.file,
|
|
23222
|
+
type: fileResult.type,
|
|
23223
|
+
valid: true
|
|
23224
|
+
}
|
|
23225
|
+
});
|
|
23226
|
+
else logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
|
|
23227
|
+
return;
|
|
23228
|
+
}
|
|
23229
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23230
|
+
error: fileResult.error,
|
|
23231
|
+
data: {
|
|
23232
|
+
file: fileResult.file,
|
|
23233
|
+
type: fileResult.type,
|
|
23234
|
+
issues: fileResult.issues ?? []
|
|
23235
|
+
}
|
|
23236
|
+
});
|
|
23237
|
+
else logValidationFailure(fileResult);
|
|
23238
|
+
process.exitCode = 1;
|
|
23239
|
+
}
|
|
23240
|
+
function handleValidateDirectory(dir) {
|
|
23241
|
+
const files = collectValidationFiles(dir);
|
|
23242
|
+
if (0 === files.length) {
|
|
23243
|
+
const error = `No supported files found under ${dir}`;
|
|
23244
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23245
|
+
error,
|
|
23246
|
+
data: {
|
|
23247
|
+
file: dir,
|
|
23248
|
+
valid: false,
|
|
23249
|
+
files: []
|
|
23250
|
+
}
|
|
23251
|
+
});
|
|
23252
|
+
else logger_logger.error(error);
|
|
23253
|
+
process.exitCode = 1;
|
|
23254
|
+
return;
|
|
23255
|
+
}
|
|
23256
|
+
const fileResults = files.map(validateSingleFile);
|
|
23257
|
+
const failedResults = fileResults.filter((result)=>!result.valid);
|
|
23258
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(0 === failedResults.length, {
|
|
23259
|
+
error: failedResults.length > 0 ? "Validation failed" : void 0,
|
|
23260
|
+
data: {
|
|
23261
|
+
file: dir,
|
|
23262
|
+
valid: 0 === failedResults.length,
|
|
23263
|
+
count: fileResults.length,
|
|
23264
|
+
files: fileResults
|
|
23265
|
+
}
|
|
23266
|
+
});
|
|
23267
|
+
else for (const fileResult of fileResults)if (fileResult.valid) logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
|
|
23268
|
+
else logValidationFailure(fileResult);
|
|
23269
|
+
if (failedResults.length > 0) process.exitCode = 1;
|
|
23270
|
+
}
|
|
21177
23271
|
function getUpdateInstructions(platform) {
|
|
21178
23272
|
if ("win32" === platform) return [
|
|
21179
23273
|
"try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }",
|
|
@@ -21274,52 +23368,30 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
21274
23368
|
});
|
|
21275
23369
|
const watchCommand = registerWatchCommand(program);
|
|
21276
23370
|
registerCheckoutCommand(program);
|
|
23371
|
+
registerGitAuthCommand(program);
|
|
21277
23372
|
registerGitCredentialCommand(program);
|
|
21278
23373
|
registerLoginCommand(program);
|
|
21279
23374
|
registerLogoutCommand(program);
|
|
21280
23375
|
registerConfigCommand(program);
|
|
23376
|
+
registerConvertTemplateCommand(program);
|
|
21281
23377
|
registerVersionCommand(program, VERSION);
|
|
21282
23378
|
registerConfigureCommand(program, VERSION);
|
|
23379
|
+
registerDepsCommand(program);
|
|
23380
|
+
registerTablesCommand(program);
|
|
21283
23381
|
program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
|
|
21284
23382
|
await handleUpdateCommand();
|
|
21285
23383
|
});
|
|
21286
|
-
program.command("validate").description("Validate
|
|
21287
|
-
let
|
|
23384
|
+
program.command("validate").description("Validate Anvil app files by path or directory (anvil.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, or server_code/**/*.py)").argument("<path>", "Path to file or directory").action((file)=>{
|
|
23385
|
+
let pathStat;
|
|
21288
23386
|
try {
|
|
21289
|
-
|
|
23387
|
+
pathStat = (0, external_fs_.statSync)(file);
|
|
21290
23388
|
} catch (error) {
|
|
21291
|
-
logger_logger.error(`Failed to
|
|
23389
|
+
logger_logger.error(`Failed to access ${file}: ${error.message}`);
|
|
21292
23390
|
process.exitCode = 1;
|
|
21293
23391
|
return;
|
|
21294
23392
|
}
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
21298
|
-
data: {
|
|
21299
|
-
file,
|
|
21300
|
-
type: result.target,
|
|
21301
|
-
valid: true
|
|
21302
|
-
}
|
|
21303
|
-
});
|
|
21304
|
-
else logger_logger.success(`${file} is valid (${result.target})`);
|
|
21305
|
-
return;
|
|
21306
|
-
}
|
|
21307
|
-
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
21308
|
-
error: result.message,
|
|
21309
|
-
data: {
|
|
21310
|
-
file,
|
|
21311
|
-
type: result.target,
|
|
21312
|
-
issues: result.issues
|
|
21313
|
-
}
|
|
21314
|
-
});
|
|
21315
|
-
else {
|
|
21316
|
-
logger_logger.error(`${file} failed validation: ${result.message}`);
|
|
21317
|
-
for (const issue of result.issues){
|
|
21318
|
-
const issuePath = issue.path.length > 0 ? issue.path : "root";
|
|
21319
|
-
logger_logger.error(` - ${issuePath}: ${issue.message}`);
|
|
21320
|
-
}
|
|
21321
|
-
}
|
|
21322
|
-
process.exitCode = 1;
|
|
23393
|
+
if (pathStat.isDirectory()) return void handleValidateDirectory(file);
|
|
23394
|
+
handleValidateFile(file);
|
|
21323
23395
|
});
|
|
21324
23396
|
if (watchCommand) {
|
|
21325
23397
|
const watchOptions = watchCommand.options.map((opt)=>{
|