@anvil-works/anvil-cli 0.8.0-canary.2 → 0.8.0-canary.21
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 +3 -1
- package/dist/SavePathRouter.d.ts.map +1 -1
- package/dist/WatchSession.d.ts +2 -0
- package/dist/WatchSession.d.ts.map +1 -1
- package/dist/api.d.ts +7 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/cli.js +2255 -1086
- package/dist/commands/checkout-picker.d.ts +1 -2
- package/dist/commands/checkout-picker.d.ts.map +1 -1
- package/dist/commands/configure.d.ts.map +1 -1
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/deps.d.ts.map +1 -1
- package/dist/commands/env.d.ts +8 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/gitAuth.d.ts +5 -1
- package/dist/commands/gitAuth.d.ts.map +1 -1
- package/dist/commands/index.d.ts +4 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/modelToken.d.ts +33 -0
- package/dist/commands/modelToken.d.ts.map +1 -0
- package/dist/commands/repl.d.ts +3 -0
- package/dist/commands/repl.d.ts.map +1 -0
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/componentTypeSpec.d.ts +1 -1
- package/dist/componentTypeSpec.d.ts.map +1 -1
- package/dist/errors.d.ts +11 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/formTemplateValidation.d.ts.map +1 -1
- package/dist/index.js +1555 -1030
- package/dist/program.d.ts +5 -14
- package/dist/program.d.ts.map +1 -1
- package/dist/services/anvil-api.d.ts.map +1 -1
- package/dist/services/auth.d.ts +42 -3
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/db.d.ts +7 -0
- package/dist/services/db.d.ts.map +1 -0
- package/dist/services/deps.d.ts +9 -4
- package/dist/services/deps.d.ts.map +1 -1
- package/dist/services/environment.d.ts +16 -0
- package/dist/services/environment.d.ts.map +1 -0
- package/dist/services/git-auth.d.ts +38 -1
- package/dist/services/git-auth.d.ts.map +1 -1
- package/dist/services/git.d.ts +1 -0
- package/dist/services/git.d.ts.map +1 -1
- package/dist/services/http.d.ts +2 -0
- package/dist/services/http.d.ts.map +1 -0
- package/dist/services/repl.d.ts +18 -0
- package/dist/services/repl.d.ts.map +1 -0
- package/dist/services/secret.d.ts +39 -0
- package/dist/services/secret.d.ts.map +1 -0
- package/dist/validateFormTemplateHtml.d.ts +6 -2
- package/dist/validateFormTemplateHtml.d.ts.map +1 -1
- package/dist/validatePython.d.ts +29 -3
- package/dist/validatePython.d.ts.map +1 -1
- package/dist/validators.d.ts +5 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/watch/SaveProcessor.d.ts +2 -0
- package/dist/watch/SaveProcessor.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/commands/tables.d.ts +0 -7
- package/dist/commands/tables.d.ts.map +0 -1
- package/dist/services/tables.d.ts +0 -27
- package/dist/services/tables.d.ts.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -13384,9 +13384,10 @@ var __webpack_exports__ = {};
|
|
|
13384
13384
|
type: "auth_invalid",
|
|
13385
13385
|
message
|
|
13386
13386
|
}),
|
|
13387
|
-
refreshFailed: (message)=>({
|
|
13387
|
+
refreshFailed: (message, status)=>({
|
|
13388
13388
|
type: "token_refresh_failed",
|
|
13389
|
-
message
|
|
13389
|
+
message,
|
|
13390
|
+
status
|
|
13390
13391
|
})
|
|
13391
13392
|
};
|
|
13392
13393
|
const createAppError = {
|
|
@@ -13600,8 +13601,23 @@ var __webpack_exports__ = {};
|
|
|
13600
13601
|
return "An unknown error occurred";
|
|
13601
13602
|
}
|
|
13602
13603
|
}
|
|
13604
|
+
function getErrorCauseMessage(error) {
|
|
13605
|
+
if ("object" != typeof error || null === error || !("cause" in error)) return null;
|
|
13606
|
+
const cause = error.cause;
|
|
13607
|
+
if (null == cause) return null;
|
|
13608
|
+
const message = errors_getErrorMessage(cause);
|
|
13609
|
+
const code = "object" == typeof cause && null !== cause && "code" in cause && "string" == typeof cause.code ? cause.code : null;
|
|
13610
|
+
if (code && !message.includes(code)) return `${message} (${code})`;
|
|
13611
|
+
return message;
|
|
13612
|
+
}
|
|
13613
|
+
function logErrorCause(error, ui) {
|
|
13614
|
+
const causeMessage = getErrorCauseMessage(error);
|
|
13615
|
+
if (causeMessage) ui.verbose(`Cause: ${causeMessage}`);
|
|
13616
|
+
}
|
|
13603
13617
|
const external_module_namespaceObject = require("module");
|
|
13604
13618
|
const requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
|
|
13619
|
+
const GIT_CREDENTIAL_HELPER_FILENAME = "anvil-credential-helper.cjs";
|
|
13620
|
+
const GIT_CREDENTIAL_HELPER_LOCATOR = `$(git rev-parse --git-common-dir)/${GIT_CREDENTIAL_HELPER_FILENAME}`;
|
|
13605
13621
|
function getUrlConfigKey(appId) {
|
|
13606
13622
|
return `anvil.auth.${appId}.url`;
|
|
13607
13623
|
}
|
|
@@ -13618,12 +13634,14 @@ var __webpack_exports__ = {};
|
|
|
13618
13634
|
const platform = options?.platform ?? process.platform;
|
|
13619
13635
|
const execPath = options?.execPath ?? process.execPath;
|
|
13620
13636
|
const helperPath = options.helperPath;
|
|
13637
|
+
const dynamicHelperPath = `"${GIT_CREDENTIAL_HELPER_LOCATOR}"`;
|
|
13621
13638
|
if ("win32" === platform) {
|
|
13622
13639
|
const resolvedExecPath = external_path_default().win32.resolve(execPath);
|
|
13623
|
-
const
|
|
13624
|
-
return `!${shellQuoteDouble(resolvedExecPath)} ${
|
|
13640
|
+
const helperArgument = helperPath ? shellQuoteDouble(external_path_default().win32.resolve(helperPath)) : dynamicHelperPath;
|
|
13641
|
+
return `!${shellQuoteDouble(resolvedExecPath)} ${helperArgument}`;
|
|
13625
13642
|
}
|
|
13626
|
-
|
|
13643
|
+
const helperArgument = helperPath ? shellQuote(external_path_default().resolve(helperPath)) : dynamicHelperPath;
|
|
13644
|
+
return `!${shellQuote(external_path_default().resolve(execPath))} ${helperArgument}`;
|
|
13627
13645
|
}
|
|
13628
13646
|
async function getLocalConfigValue(repoPath, key) {
|
|
13629
13647
|
try {
|
|
@@ -13672,21 +13690,14 @@ var __webpack_exports__ = {};
|
|
|
13672
13690
|
return `#!/usr/bin/env node
|
|
13673
13691
|
"use strict";
|
|
13674
13692
|
|
|
13675
|
-
const
|
|
13676
|
-
const path = require("path");
|
|
13693
|
+
const { execFileSync } = require("child_process");
|
|
13677
13694
|
const { executeGitCredentialOperation } = require(${JSON.stringify(modulePath)});
|
|
13678
13695
|
|
|
13679
13696
|
function resolveRepoPath() {
|
|
13680
|
-
|
|
13681
|
-
|
|
13682
|
-
|
|
13683
|
-
|
|
13684
|
-
const gitDir = __dirname;
|
|
13685
|
-
const linkedWorktreeGitdir = path.join(gitDir, "gitdir");
|
|
13686
|
-
if (fs.existsSync(linkedWorktreeGitdir)) {
|
|
13687
|
-
return path.dirname(path.resolve(gitDir, fs.readFileSync(linkedWorktreeGitdir, "utf8").trim()));
|
|
13688
|
-
}
|
|
13689
|
-
return path.dirname(gitDir);
|
|
13697
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
13698
|
+
cwd: process.cwd(),
|
|
13699
|
+
encoding: "utf8",
|
|
13700
|
+
}).trim();
|
|
13690
13701
|
}
|
|
13691
13702
|
|
|
13692
13703
|
Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
@@ -13721,16 +13732,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13721
13732
|
if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
|
|
13722
13733
|
return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
|
|
13723
13734
|
}
|
|
13724
|
-
async function
|
|
13725
|
-
const
|
|
13735
|
+
async function getRepositoryGitPath(repoPath, revParseArg, git = esm_default(repoPath)) {
|
|
13736
|
+
const rawGitPath = (await git.raw([
|
|
13726
13737
|
"rev-parse",
|
|
13727
|
-
|
|
13738
|
+
revParseArg
|
|
13728
13739
|
])).trim();
|
|
13729
|
-
return external_path_default().isAbsolute(
|
|
13740
|
+
return external_path_default().isAbsolute(rawGitPath) ? rawGitPath : external_path_default().resolve(repoPath, rawGitPath);
|
|
13741
|
+
}
|
|
13742
|
+
async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
|
|
13743
|
+
return getRepositoryGitPath(repoPath, "--git-dir", git);
|
|
13744
|
+
}
|
|
13745
|
+
async function getRepositoryCommonGitDir(repoPath, git = esm_default(repoPath)) {
|
|
13746
|
+
return getRepositoryGitPath(repoPath, "--git-common-dir", git);
|
|
13730
13747
|
}
|
|
13731
13748
|
async function writeCredentialHelperScript(options) {
|
|
13732
|
-
const
|
|
13733
|
-
const helperPath = external_path_default().join(
|
|
13749
|
+
const gitCommonDir = await getRepositoryCommonGitDir(options.repoPath, options.git);
|
|
13750
|
+
const helperPath = external_path_default().join(gitCommonDir, GIT_CREDENTIAL_HELPER_FILENAME);
|
|
13734
13751
|
await external_fs_.promises.mkdir(external_path_default().dirname(helperPath), {
|
|
13735
13752
|
recursive: true
|
|
13736
13753
|
});
|
|
@@ -13745,7 +13762,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13745
13762
|
const url = new URL(normalized);
|
|
13746
13763
|
const scope = `${url.protocol}//${url.host}`;
|
|
13747
13764
|
const git = esm_default(repoPath);
|
|
13748
|
-
|
|
13765
|
+
await writeCredentialHelperScript({
|
|
13749
13766
|
repoPath,
|
|
13750
13767
|
git,
|
|
13751
13768
|
modulePath: options?.modulePath
|
|
@@ -13772,7 +13789,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13772
13789
|
`credential.${scope}.helper`,
|
|
13773
13790
|
getGitCredentialHelperCommand({
|
|
13774
13791
|
execPath: options?.execPath,
|
|
13775
|
-
helperPath,
|
|
13776
13792
|
platform: options?.platform
|
|
13777
13793
|
})
|
|
13778
13794
|
]);
|
|
@@ -13818,6 +13834,76 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13818
13834
|
function getAuthFilePath(gitDir) {
|
|
13819
13835
|
return external_path_default().join(gitDir, "anvil-auth.json");
|
|
13820
13836
|
}
|
|
13837
|
+
const AUTH_FILE_LOCK_DEFAULTS = {
|
|
13838
|
+
staleMs: 30000,
|
|
13839
|
+
timeoutMs: 60000,
|
|
13840
|
+
retryDelayMs: 100,
|
|
13841
|
+
heartbeatMs: 5000
|
|
13842
|
+
};
|
|
13843
|
+
function git_auth_delay(ms) {
|
|
13844
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
13845
|
+
}
|
|
13846
|
+
async function acquireAuthFileLock(lockPath, options) {
|
|
13847
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
13848
|
+
while(true){
|
|
13849
|
+
try {
|
|
13850
|
+
const handle = await external_fs_.promises.open(lockPath, "wx", 384);
|
|
13851
|
+
try {
|
|
13852
|
+
await handle.writeFile(JSON.stringify({
|
|
13853
|
+
pid: process.pid,
|
|
13854
|
+
acquiredAt: new Date().toISOString()
|
|
13855
|
+
}));
|
|
13856
|
+
} finally{
|
|
13857
|
+
await handle.close();
|
|
13858
|
+
}
|
|
13859
|
+
return;
|
|
13860
|
+
} catch (e) {
|
|
13861
|
+
if ("EEXIST" !== e.code) throw e;
|
|
13862
|
+
}
|
|
13863
|
+
try {
|
|
13864
|
+
const lockStat = await external_fs_.promises.stat(lockPath);
|
|
13865
|
+
if (Date.now() - lockStat.mtimeMs > options.staleMs) {
|
|
13866
|
+
await external_fs_.promises.rm(lockPath, {
|
|
13867
|
+
force: true
|
|
13868
|
+
});
|
|
13869
|
+
continue;
|
|
13870
|
+
}
|
|
13871
|
+
} catch {
|
|
13872
|
+
continue;
|
|
13873
|
+
}
|
|
13874
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the Anvil auth file lock at ${lockPath}`);
|
|
13875
|
+
await git_auth_delay(options.retryDelayMs);
|
|
13876
|
+
}
|
|
13877
|
+
}
|
|
13878
|
+
async function withRepoAuthFileLock(repoPath, fn, options) {
|
|
13879
|
+
const gitDir = await getRepositoryGitDir(repoPath);
|
|
13880
|
+
const lockPath = `${getAuthFilePath(gitDir)}.lock`;
|
|
13881
|
+
const lockOptions = {
|
|
13882
|
+
...AUTH_FILE_LOCK_DEFAULTS,
|
|
13883
|
+
...options
|
|
13884
|
+
};
|
|
13885
|
+
await external_fs_.promises.mkdir(external_path_default().dirname(lockPath), {
|
|
13886
|
+
recursive: true
|
|
13887
|
+
});
|
|
13888
|
+
await acquireAuthFileLock(lockPath, lockOptions);
|
|
13889
|
+
const heartbeat = startAuthFileLockHeartbeat(lockPath, lockOptions.heartbeatMs);
|
|
13890
|
+
try {
|
|
13891
|
+
return await fn();
|
|
13892
|
+
} finally{
|
|
13893
|
+
clearInterval(heartbeat);
|
|
13894
|
+
await external_fs_.promises.rm(lockPath, {
|
|
13895
|
+
force: true
|
|
13896
|
+
});
|
|
13897
|
+
}
|
|
13898
|
+
}
|
|
13899
|
+
function startAuthFileLockHeartbeat(lockPath, heartbeatMs) {
|
|
13900
|
+
const heartbeat = setInterval(()=>{
|
|
13901
|
+
const now = new Date();
|
|
13902
|
+
external_fs_.promises.utimes(lockPath, now, now).catch(()=>{});
|
|
13903
|
+
}, heartbeatMs);
|
|
13904
|
+
heartbeat.unref();
|
|
13905
|
+
return heartbeat;
|
|
13906
|
+
}
|
|
13821
13907
|
function decodeKey(b64key) {
|
|
13822
13908
|
const key = Buffer.from(b64key, "base64");
|
|
13823
13909
|
if (32 !== key.length) throw new Error("Encryption key must be a base64-encoded 32-byte key");
|
|
@@ -13862,10 +13948,19 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13862
13948
|
recursive: true
|
|
13863
13949
|
});
|
|
13864
13950
|
const authJson = JSON.stringify(auth, null, 2);
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13951
|
+
const tempFilePath = `${authFilePath}.tmp-${external_node_crypto_default().randomBytes(6).toString("hex")}`;
|
|
13952
|
+
try {
|
|
13953
|
+
await external_fs_.promises.writeFile(tempFilePath, encrypt(authJson, encryptionKey), {
|
|
13954
|
+
mode: 384
|
|
13955
|
+
});
|
|
13956
|
+
await external_fs_.promises.chmod(tempFilePath, 448);
|
|
13957
|
+
await external_fs_.promises.rename(tempFilePath, authFilePath);
|
|
13958
|
+
} catch (e) {
|
|
13959
|
+
await external_fs_.promises.rm(tempFilePath, {
|
|
13960
|
+
force: true
|
|
13961
|
+
});
|
|
13962
|
+
throw e;
|
|
13963
|
+
}
|
|
13869
13964
|
await git.raw([
|
|
13870
13965
|
"config",
|
|
13871
13966
|
"--local",
|
|
@@ -13874,8 +13969,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13874
13969
|
]);
|
|
13875
13970
|
return authFilePath;
|
|
13876
13971
|
}
|
|
13877
|
-
const
|
|
13972
|
+
const git_auth_readAuthFromRepo = async (repoPath)=>{
|
|
13878
13973
|
const git = esm_default(repoPath);
|
|
13974
|
+
if (!await git.checkIsRepo()) return;
|
|
13879
13975
|
const gitDir = await getRepositoryGitDir(repoPath, git);
|
|
13880
13976
|
const authFilePath = getAuthFilePath(gitDir);
|
|
13881
13977
|
if (!external_fs_default().existsSync(authFilePath)) return;
|
|
@@ -13887,14 +13983,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13887
13983
|
return resolveAnvilUrl();
|
|
13888
13984
|
}
|
|
13889
13985
|
const ANVIL_SYNC_CLIENT_ID = "anvil-sync";
|
|
13986
|
+
const REFRESH_REQUEST_TIMEOUT_MS = 12000;
|
|
13890
13987
|
let inMemoryAuth = null;
|
|
13891
13988
|
function authMatches(auth, anvilUrl, username) {
|
|
13892
13989
|
return auth.anvilUrl === normalizeAnvilUrl(anvilUrl) && (!username || auth.username === username);
|
|
13893
13990
|
}
|
|
13894
13991
|
let repoContext = null;
|
|
13895
|
-
function
|
|
13992
|
+
function auth_setRepoContext(repoPath) {
|
|
13896
13993
|
repoContext = repoPath;
|
|
13897
13994
|
}
|
|
13995
|
+
async function resolveAuthAnvilUrl() {
|
|
13996
|
+
if (inMemoryAuth) return inMemoryAuth.anvilUrl;
|
|
13997
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
13998
|
+
return repoAuth?.anvilUrl ?? resolveAnvilUrl();
|
|
13999
|
+
}
|
|
13898
14000
|
const inFlightRefreshes = new Map();
|
|
13899
14001
|
async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
|
|
13900
14002
|
try {
|
|
@@ -13915,7 +14017,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13915
14017
|
throw errors_createAuthError.invalid(`Network error: ${e.message}`);
|
|
13916
14018
|
}
|
|
13917
14019
|
}
|
|
13918
|
-
async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID) {
|
|
14020
|
+
async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID, options) {
|
|
13919
14021
|
try {
|
|
13920
14022
|
const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
|
|
13921
14023
|
method: "POST",
|
|
@@ -13926,11 +14028,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13926
14028
|
grant_type: "refresh_token",
|
|
13927
14029
|
refresh_token: refreshToken,
|
|
13928
14030
|
client_id: clientId
|
|
13929
|
-
})
|
|
14031
|
+
}),
|
|
14032
|
+
signal: AbortSignal.timeout(options?.timeoutMs ?? REFRESH_REQUEST_TIMEOUT_MS)
|
|
13930
14033
|
});
|
|
13931
14034
|
if (!tokenResponse.ok) {
|
|
13932
14035
|
const errorText = await tokenResponse.text();
|
|
13933
|
-
throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}
|
|
14036
|
+
throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`, tokenResponse.status);
|
|
13934
14037
|
}
|
|
13935
14038
|
const tokenData = await tokenResponse.json();
|
|
13936
14039
|
return tokenData;
|
|
@@ -13942,22 +14045,21 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13942
14045
|
async function hasTokensForUrl(anvilUrl, username) {
|
|
13943
14046
|
if (inMemoryAuth) return authMatches(inMemoryAuth, anvilUrl, username);
|
|
13944
14047
|
{
|
|
13945
|
-
const repoAuth = repoContext ? await
|
|
14048
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
13946
14049
|
if (repoAuth && authMatches(repoAuth, anvilUrl, username)) return true;
|
|
13947
14050
|
return hasTokens(anvilUrl, username);
|
|
13948
14051
|
}
|
|
13949
14052
|
}
|
|
13950
|
-
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
|
|
14053
|
+
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username, options) {
|
|
13951
14054
|
const normalized = normalizeAnvilUrl(anvilUrl);
|
|
13952
14055
|
const source = await resolveTokenSource(normalized, username);
|
|
13953
14056
|
const tokens = getSourceTokens(source);
|
|
13954
14057
|
if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
|
|
13955
|
-
|
|
13956
|
-
if (tokens.refreshToken && isExpired) {
|
|
14058
|
+
if (tokens.refreshToken && isAccessTokenExpired(tokens)) {
|
|
13957
14059
|
const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
|
|
13958
14060
|
const existingRefresh = inFlightRefreshes.get(refreshKey);
|
|
13959
14061
|
if (existingRefresh) return existingRefresh;
|
|
13960
|
-
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
|
|
14062
|
+
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken, options).finally(()=>{
|
|
13961
14063
|
inFlightRefreshes.delete(refreshKey);
|
|
13962
14064
|
});
|
|
13963
14065
|
inFlightRefreshes.set(refreshKey, refreshPromise);
|
|
@@ -13971,7 +14073,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13971
14073
|
type: "memory",
|
|
13972
14074
|
auth: inMemoryAuth
|
|
13973
14075
|
};
|
|
13974
|
-
const repoAuth = repoContext ? await
|
|
14076
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
13975
14077
|
if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
|
|
13976
14078
|
type: "repo",
|
|
13977
14079
|
auth: repoAuth,
|
|
@@ -13987,6 +14089,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13987
14089
|
function getSourceTokens(source) {
|
|
13988
14090
|
return "store" === source.type ? source.tokens : source.auth.tokens;
|
|
13989
14091
|
}
|
|
14092
|
+
function isAccessTokenExpired(tokens) {
|
|
14093
|
+
return null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
|
|
14094
|
+
}
|
|
13990
14095
|
function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
|
|
13991
14096
|
if (username) return username;
|
|
13992
14097
|
const urlTokens = getUrlTokens(normalizedUrl);
|
|
@@ -14005,11 +14110,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14005
14110
|
return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
|
|
14006
14111
|
}
|
|
14007
14112
|
}
|
|
14008
|
-
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
|
|
14113
|
+
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken, options) {
|
|
14114
|
+
if ("repo" === source.type) return refreshRepoAuthTokens(source.repoPath, {
|
|
14115
|
+
anvilUrl: normalizedUrl,
|
|
14116
|
+
username,
|
|
14117
|
+
auth: source.auth,
|
|
14118
|
+
refreshToken,
|
|
14119
|
+
lock: options?.lock,
|
|
14120
|
+
requestTimeoutMs: options?.requestTimeoutMs
|
|
14121
|
+
});
|
|
14122
|
+
return performTokenRefresh(normalizedUrl, username, source, refreshToken, options?.requestTimeoutMs);
|
|
14123
|
+
}
|
|
14124
|
+
async function refreshRepoAuthTokens(repoPath, options) {
|
|
14125
|
+
const normalizedUrl = normalizeAnvilUrl(options.anvilUrl);
|
|
14126
|
+
return withRepoAuthFileLock(repoPath, async ()=>{
|
|
14127
|
+
let auth = options.auth;
|
|
14128
|
+
let refreshToken = options.refreshToken;
|
|
14129
|
+
const current = await git_auth_readAuthFromRepo(repoPath);
|
|
14130
|
+
if (current && authMatches(current, normalizedUrl, options.username)) {
|
|
14131
|
+
if (current.tokens.authToken && !isAccessTokenExpired(current.tokens)) return current.tokens.authToken;
|
|
14132
|
+
auth = current;
|
|
14133
|
+
refreshToken = current.tokens.refreshToken ?? refreshToken;
|
|
14134
|
+
}
|
|
14135
|
+
if (!auth || !refreshToken) throw errors_createAuthError.required("No repo auth tokens available to refresh.");
|
|
14136
|
+
return performTokenRefresh(normalizedUrl, options.username, {
|
|
14137
|
+
type: "repo",
|
|
14138
|
+
auth,
|
|
14139
|
+
repoPath
|
|
14140
|
+
}, refreshToken, options.requestTimeoutMs);
|
|
14141
|
+
}, options.lock);
|
|
14142
|
+
}
|
|
14143
|
+
async function performTokenRefresh(normalizedUrl, username, source, refreshToken, requestTimeoutMs) {
|
|
14009
14144
|
try {
|
|
14010
14145
|
const tokens = getSourceTokens(source);
|
|
14011
14146
|
const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
|
|
14012
|
-
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId
|
|
14147
|
+
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId, {
|
|
14148
|
+
timeoutMs: requestTimeoutMs
|
|
14149
|
+
});
|
|
14013
14150
|
const newTokens = {
|
|
14014
14151
|
authToken: tokenData.access_token,
|
|
14015
14152
|
refreshToken: tokenData.refresh_token,
|
|
@@ -14031,12 +14168,28 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14031
14168
|
}
|
|
14032
14169
|
return tokenData.access_token;
|
|
14033
14170
|
} catch (e) {
|
|
14171
|
+
if ("repo" === source.type && isRefreshTokenRejection(e)) await clearRepoAuthTokens(source.repoPath, source.auth).catch(()=>{});
|
|
14034
14172
|
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
14035
14173
|
throw e;
|
|
14036
14174
|
}
|
|
14037
14175
|
}
|
|
14176
|
+
function isRefreshTokenRejection(e) {
|
|
14177
|
+
const error = e;
|
|
14178
|
+
return error?.type === "token_refresh_failed" && (400 === error.status || 401 === error.status);
|
|
14179
|
+
}
|
|
14180
|
+
async function clearRepoAuthTokens(repoPath, auth) {
|
|
14181
|
+
const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
|
|
14182
|
+
if (!encryptionKey) return;
|
|
14183
|
+
auth.tokens = {
|
|
14184
|
+
authToken: null,
|
|
14185
|
+
refreshToken: null,
|
|
14186
|
+
authTokenExpiresAt: null,
|
|
14187
|
+
clientId: auth.tokens.clientId
|
|
14188
|
+
};
|
|
14189
|
+
await writeAuthToFile(repoPath, auth, encryptionKey);
|
|
14190
|
+
}
|
|
14038
14191
|
async function clearTokensForAccount(url, username, refreshToken) {
|
|
14039
|
-
if (inMemoryAuth || repoContext && await
|
|
14192
|
+
if (inMemoryAuth || repoContext && await git_auth_readAuthFromRepo(repoContext)) return;
|
|
14040
14193
|
{
|
|
14041
14194
|
const normalized = normalizeAnvilUrl(url);
|
|
14042
14195
|
if (username) deleteAccountTokens(normalized, username);
|
|
@@ -14066,7 +14219,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14066
14219
|
loggedOut: false,
|
|
14067
14220
|
message: "Cannot log out when using in-memory auth tokens"
|
|
14068
14221
|
};
|
|
14069
|
-
if (repoContext && await
|
|
14222
|
+
if (repoContext && await git_auth_readAuthFromRepo(repoContext)) return {
|
|
14070
14223
|
loggedOut: false,
|
|
14071
14224
|
message: "Cannot log out when using auth tokens stored in the local git config."
|
|
14072
14225
|
};
|
|
@@ -14097,7 +14250,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14097
14250
|
inMemoryAuth.username
|
|
14098
14251
|
] : [];
|
|
14099
14252
|
{
|
|
14100
|
-
const repoAuth = repoContext ? await
|
|
14253
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
14101
14254
|
if (repoAuth && authMatches(repoAuth, url)) return [
|
|
14102
14255
|
repoAuth.username
|
|
14103
14256
|
];
|
|
@@ -14109,587 +14262,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14109
14262
|
inMemoryAuth.anvilUrl
|
|
14110
14263
|
];
|
|
14111
14264
|
{
|
|
14112
|
-
const repoAuth = repoContext ? await
|
|
14265
|
+
const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
|
|
14113
14266
|
if (repoAuth) return [
|
|
14114
14267
|
repoAuth.anvilUrl
|
|
14115
14268
|
];
|
|
14116
14269
|
return getAvailableUrls();
|
|
14117
14270
|
}
|
|
14118
14271
|
}
|
|
14119
|
-
|
|
14120
|
-
|
|
14272
|
+
const promises_namespaceObject = require("timers/promises");
|
|
14273
|
+
const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
|
|
14274
|
+
const FETCH_RETRY_DELAY_MS = 200;
|
|
14275
|
+
function isWantNotValidError(error) {
|
|
14276
|
+
return WANT_NOT_VALID_RE.test(error.message || "");
|
|
14121
14277
|
}
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
const data = await resp.json();
|
|
14136
|
-
if (resp.ok) return {
|
|
14137
|
-
valid: data.valid ?? false,
|
|
14138
|
-
app_name: data.app_name
|
|
14139
|
-
};
|
|
14140
|
-
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14141
|
-
if (403 === resp.status) throw createAppError.accessDenied(appId);
|
|
14142
|
-
if (404 === resp.status) throw createAppError.notFound(appId);
|
|
14143
|
-
logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
|
|
14144
|
-
return {
|
|
14145
|
-
valid: false,
|
|
14146
|
-
error: data.error || `Server error: ${resp.status}`
|
|
14147
|
-
};
|
|
14148
|
-
} catch (error) {
|
|
14149
|
-
if (error.type) throw error;
|
|
14150
|
-
throw errors_createNetworkError.network(error.message);
|
|
14278
|
+
class GitService extends Emitter {
|
|
14279
|
+
git;
|
|
14280
|
+
repoPath;
|
|
14281
|
+
static INDEX_LOCK_RETRY_DELAYS_MS = [
|
|
14282
|
+
25,
|
|
14283
|
+
50,
|
|
14284
|
+
100,
|
|
14285
|
+
200
|
|
14286
|
+
];
|
|
14287
|
+
constructor(repoPath){
|
|
14288
|
+
super();
|
|
14289
|
+
this.repoPath = external_path_default().resolve(repoPath);
|
|
14290
|
+
this.git = esm_default(this.repoPath);
|
|
14151
14291
|
}
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
|
|
14155
|
-
const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
14156
|
-
const params = new URLSearchParams();
|
|
14157
|
-
if ("number" == typeof options.limit) params.set("limit", String(options.limit));
|
|
14158
|
-
if (options.cursor) params.set("cursor", options.cursor);
|
|
14159
|
-
if (options.q && options.q.trim()) params.set("q", options.q.trim());
|
|
14160
|
-
const query = params.toString();
|
|
14161
|
-
const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
|
|
14162
|
-
try {
|
|
14163
|
-
const resp = await fetch(url, {
|
|
14164
|
-
method: "GET",
|
|
14165
|
-
headers: {
|
|
14166
|
-
Authorization: `Bearer ${authToken}`
|
|
14167
|
-
},
|
|
14168
|
-
signal: options.signal
|
|
14169
|
-
});
|
|
14170
|
-
if (!resp.ok) {
|
|
14171
|
-
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14172
|
-
throw errors_createNetworkError.server(resp.status, resp.statusText);
|
|
14173
|
-
}
|
|
14174
|
-
const data = await resp.json();
|
|
14175
|
-
return {
|
|
14176
|
-
apps: Array.isArray(data.apps) ? data.apps : [],
|
|
14177
|
-
next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
|
|
14178
|
-
};
|
|
14179
|
-
} catch (error) {
|
|
14180
|
-
if (error.type) throw error;
|
|
14181
|
-
throw errors_createNetworkError.network(error.message);
|
|
14292
|
+
getGit() {
|
|
14293
|
+
return this.git;
|
|
14182
14294
|
}
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
const url = new URL(gitUrl);
|
|
14186
|
-
const encodedToken = encodeURIComponent(authToken);
|
|
14187
|
-
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
|
|
14188
|
-
}
|
|
14189
|
-
function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14190
|
-
const url = new URL(anvilUrl);
|
|
14191
|
-
const encodedToken = encodeURIComponent(authToken);
|
|
14192
|
-
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
|
|
14193
|
-
}
|
|
14194
|
-
function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14195
|
-
return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
|
|
14196
|
-
}
|
|
14197
|
-
async function getLatestVersion() {
|
|
14198
|
-
try {
|
|
14199
|
-
const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
|
|
14200
|
-
if (!response.ok) return null;
|
|
14201
|
-
const data = await response.json();
|
|
14202
|
-
return data.version;
|
|
14203
|
-
} catch (e) {
|
|
14204
|
-
return null;
|
|
14295
|
+
getRepoPath() {
|
|
14296
|
+
return this.repoPath;
|
|
14205
14297
|
}
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14298
|
+
async getGitDir() {
|
|
14299
|
+
try {
|
|
14300
|
+
const gitDir = (await this.git.revparse([
|
|
14301
|
+
"--git-dir"
|
|
14302
|
+
])).trim();
|
|
14303
|
+
if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
|
|
14304
|
+
return gitDir;
|
|
14305
|
+
} catch (e) {
|
|
14306
|
+
throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
|
|
14307
|
+
}
|
|
14212
14308
|
}
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
}
|
|
14225
|
-
function lookupRemoteInfoForAppId(appId, detectedRemotes) {
|
|
14226
|
-
const matches = detectedRemotes.filter((c)=>c.appId === appId);
|
|
14227
|
-
if (0 === matches.length) return {};
|
|
14228
|
-
const withUsername = matches.find((c)=>c.detectedUsername);
|
|
14229
|
-
if (withUsername) return {
|
|
14230
|
-
detectedUrl: withUsername.detectedUrl,
|
|
14231
|
-
detectedUsername: withUsername.detectedUsername
|
|
14232
|
-
};
|
|
14233
|
-
return {
|
|
14234
|
-
detectedUrl: matches[0].detectedUrl,
|
|
14235
|
-
detectedUsername: matches[0].detectedUsername
|
|
14236
|
-
};
|
|
14237
|
-
}
|
|
14238
|
-
async function detectAppIdsFromAllRemotes(repoPath) {
|
|
14239
|
-
const git = esm_default(repoPath);
|
|
14240
|
-
const out = [];
|
|
14241
|
-
try {
|
|
14242
|
-
const remotes = await git.getRemotes(true);
|
|
14243
|
-
for (const remote of remotes){
|
|
14244
|
-
const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
|
|
14245
|
-
if (httpMatch) {
|
|
14246
|
-
const [, host, detectedAppId] = httpMatch;
|
|
14247
|
-
out.push({
|
|
14248
|
-
appId: detectedAppId,
|
|
14249
|
-
source: "remote",
|
|
14250
|
-
description: `Git remote '${remote.name}'`,
|
|
14251
|
-
detectedUrl: normalizeAnvilUrl(host)
|
|
14252
|
-
});
|
|
14253
|
-
continue;
|
|
14254
|
-
}
|
|
14255
|
-
const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
|
|
14256
|
-
if (sshMatch) {
|
|
14257
|
-
const [, usernamePart, host, , detectedAppId] = sshMatch;
|
|
14258
|
-
const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
|
|
14259
|
-
const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
|
|
14260
|
-
out.push({
|
|
14261
|
-
appId: detectedAppId,
|
|
14262
|
-
source: "remote",
|
|
14263
|
-
description: `Git remote '${remote.name}' (SSH)`,
|
|
14264
|
-
detectedUrl: normalizeAnvilUrl(host),
|
|
14265
|
-
detectedUsername: finalUsername
|
|
14266
|
-
});
|
|
14267
|
-
}
|
|
14268
|
-
}
|
|
14269
|
-
} catch (_e) {}
|
|
14270
|
-
return out;
|
|
14271
|
-
}
|
|
14272
|
-
async function detectAppIdsByCommitLookup(repoPath, options) {
|
|
14273
|
-
const anvilUrl = options.anvilUrl || resolveAnvilUrl();
|
|
14274
|
-
const username = options.username;
|
|
14275
|
-
const git = esm_default(repoPath);
|
|
14276
|
-
const out = [];
|
|
14277
|
-
try {
|
|
14278
|
-
const authToken = await auth_getValidAuthToken(anvilUrl, username);
|
|
14279
|
-
const branchRef = await git.revparse([
|
|
14280
|
-
"--abbrev-ref",
|
|
14281
|
-
"HEAD"
|
|
14282
|
-
]);
|
|
14283
|
-
const currentBranch = "HEAD" === branchRef ? "master" : branchRef;
|
|
14284
|
-
const commitId = (await git.revparse([
|
|
14285
|
-
"HEAD"
|
|
14286
|
-
]))?.trim();
|
|
14287
|
-
if (!commitId) return out;
|
|
14288
|
-
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
|
|
14289
|
-
method: "POST",
|
|
14290
|
-
headers: {
|
|
14291
|
-
Authorization: `Bearer ${authToken}`,
|
|
14292
|
-
"Content-Type": "application/json"
|
|
14293
|
-
},
|
|
14294
|
-
body: JSON.stringify({
|
|
14295
|
-
commit_id: commitId,
|
|
14296
|
-
branch_name: currentBranch
|
|
14297
|
-
})
|
|
14298
|
-
});
|
|
14299
|
-
logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${currentBranch} returned ${resp.status}`);
|
|
14300
|
-
if (!resp.ok) {
|
|
14301
|
-
const errorBody = await resp.text();
|
|
14302
|
-
logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
|
|
14303
|
-
}
|
|
14304
|
-
if (resp.ok) {
|
|
14305
|
-
const data = await resp.json();
|
|
14306
|
-
logger_logger.debug(`data ${JSON.stringify(data)}`);
|
|
14307
|
-
const apps = data?.apps ?? [];
|
|
14308
|
-
for (const app of apps)if (app?.app_id) out.push({
|
|
14309
|
-
appId: app.app_id,
|
|
14310
|
-
source: "remote",
|
|
14311
|
-
description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
|
|
14312
|
-
});
|
|
14313
|
-
}
|
|
14314
|
-
} catch (_e) {}
|
|
14315
|
-
return out;
|
|
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
|
-
}
|
|
14645
|
-
const external_timers_promises_namespaceObject = require("timers/promises");
|
|
14646
|
-
const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
|
|
14647
|
-
const FETCH_RETRY_DELAY_MS = 200;
|
|
14648
|
-
function isWantNotValidError(error) {
|
|
14649
|
-
return WANT_NOT_VALID_RE.test(error.message || "");
|
|
14650
|
-
}
|
|
14651
|
-
class GitService extends Emitter {
|
|
14652
|
-
git;
|
|
14653
|
-
repoPath;
|
|
14654
|
-
static INDEX_LOCK_RETRY_DELAYS_MS = [
|
|
14655
|
-
25,
|
|
14656
|
-
50,
|
|
14657
|
-
100,
|
|
14658
|
-
200
|
|
14659
|
-
];
|
|
14660
|
-
constructor(repoPath){
|
|
14661
|
-
super();
|
|
14662
|
-
this.repoPath = external_path_default().resolve(repoPath);
|
|
14663
|
-
this.git = esm_default(this.repoPath);
|
|
14664
|
-
}
|
|
14665
|
-
getGit() {
|
|
14666
|
-
return this.git;
|
|
14667
|
-
}
|
|
14668
|
-
getRepoPath() {
|
|
14669
|
-
return this.repoPath;
|
|
14670
|
-
}
|
|
14671
|
-
async getGitDir() {
|
|
14672
|
-
try {
|
|
14673
|
-
const gitDir = (await this.git.revparse([
|
|
14674
|
-
"--git-dir"
|
|
14675
|
-
])).trim();
|
|
14676
|
-
if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
|
|
14677
|
-
return gitDir;
|
|
14678
|
-
} catch (e) {
|
|
14679
|
-
throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
|
|
14680
|
-
}
|
|
14681
|
-
}
|
|
14682
|
-
async getCurrentBranch() {
|
|
14683
|
-
try {
|
|
14684
|
-
const branchRef = await this.git.revparse([
|
|
14685
|
-
"--abbrev-ref",
|
|
14686
|
-
"HEAD"
|
|
14687
|
-
]);
|
|
14688
|
-
if ("HEAD" === branchRef) throw createSyncError.detachedHead();
|
|
14689
|
-
return branchRef;
|
|
14690
|
-
} catch (e) {
|
|
14691
|
-
if ("detached_head" === e.type) throw e;
|
|
14692
|
-
throw createSyncError.detachedHead();
|
|
14309
|
+
async getCurrentBranch() {
|
|
14310
|
+
try {
|
|
14311
|
+
const branchRef = await this.git.revparse([
|
|
14312
|
+
"--abbrev-ref",
|
|
14313
|
+
"HEAD"
|
|
14314
|
+
]);
|
|
14315
|
+
if ("HEAD" === branchRef) throw createSyncError.detachedHead();
|
|
14316
|
+
return branchRef;
|
|
14317
|
+
} catch (e) {
|
|
14318
|
+
if ("detached_head" === e.type) throw e;
|
|
14319
|
+
throw createSyncError.detachedHead();
|
|
14693
14320
|
}
|
|
14694
14321
|
}
|
|
14695
14322
|
async getCommitId() {
|
|
@@ -14755,341 +14382,951 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
14755
14382
|
}
|
|
14756
14383
|
return discarded;
|
|
14757
14384
|
}
|
|
14758
|
-
async discardStagedModeOnlyChanges(paths) {
|
|
14759
|
-
const discarded = [];
|
|
14760
|
-
for (const relativePath of paths)try {
|
|
14761
|
-
const headMode = await this.getHeadFileMode(relativePath);
|
|
14762
|
-
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14763
|
-
const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
|
|
14764
|
-
if (!indexContentUnchanged) continue;
|
|
14385
|
+
async discardStagedModeOnlyChanges(paths) {
|
|
14386
|
+
const discarded = [];
|
|
14387
|
+
for (const relativePath of paths)try {
|
|
14388
|
+
const headMode = await this.getHeadFileMode(relativePath);
|
|
14389
|
+
if ("100644" !== headMode && "100755" !== headMode) continue;
|
|
14390
|
+
const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
|
|
14391
|
+
if (!indexContentUnchanged) continue;
|
|
14392
|
+
await this.git.raw([
|
|
14393
|
+
"reset",
|
|
14394
|
+
"-q",
|
|
14395
|
+
"HEAD",
|
|
14396
|
+
"--",
|
|
14397
|
+
relativePath
|
|
14398
|
+
]);
|
|
14399
|
+
await this.restoreExecutableBit(relativePath, "100755" === headMode);
|
|
14400
|
+
discarded.push(relativePath);
|
|
14401
|
+
} catch (e) {
|
|
14402
|
+
throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
|
|
14403
|
+
}
|
|
14404
|
+
return discarded;
|
|
14405
|
+
}
|
|
14406
|
+
async getHeadFileMode(relativePath) {
|
|
14407
|
+
const output = await this.git.raw([
|
|
14408
|
+
"ls-tree",
|
|
14409
|
+
"HEAD",
|
|
14410
|
+
"--",
|
|
14411
|
+
relativePath
|
|
14412
|
+
]);
|
|
14413
|
+
const match = output.match(/^(\d{6})\s/);
|
|
14414
|
+
return match?.[1] ?? null;
|
|
14415
|
+
}
|
|
14416
|
+
async hasSameContentAsHead(relativePath) {
|
|
14417
|
+
const headHash = (await this.git.revparse([
|
|
14418
|
+
`HEAD:${relativePath}`
|
|
14419
|
+
])).trim();
|
|
14420
|
+
const worktreeHash = (await this.git.raw([
|
|
14421
|
+
"hash-object",
|
|
14422
|
+
"--",
|
|
14423
|
+
relativePath
|
|
14424
|
+
])).trim();
|
|
14425
|
+
return headHash === worktreeHash;
|
|
14426
|
+
}
|
|
14427
|
+
async hasSameIndexContentAsHead(relativePath) {
|
|
14428
|
+
const headHash = (await this.git.revparse([
|
|
14429
|
+
`HEAD:${relativePath}`
|
|
14430
|
+
])).trim();
|
|
14431
|
+
const indexHash = (await this.git.revparse([
|
|
14432
|
+
`:${relativePath}`
|
|
14433
|
+
])).trim();
|
|
14434
|
+
return headHash === indexHash;
|
|
14435
|
+
}
|
|
14436
|
+
async restoreExecutableBit(relativePath, executable) {
|
|
14437
|
+
const filePath = external_path_default().join(this.repoPath, relativePath);
|
|
14438
|
+
const stat = await external_fs_.promises.stat(filePath);
|
|
14439
|
+
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14440
|
+
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14441
|
+
}
|
|
14442
|
+
async hasUncommittedChanges() {
|
|
14443
|
+
const status = await this.getStatus();
|
|
14444
|
+
return !status.isClean;
|
|
14445
|
+
}
|
|
14446
|
+
async fetch(url, refSpec) {
|
|
14447
|
+
let attempt = 0;
|
|
14448
|
+
while(true)try {
|
|
14449
|
+
await this.git.fetch([
|
|
14450
|
+
url,
|
|
14451
|
+
refSpec
|
|
14452
|
+
]);
|
|
14453
|
+
if (attempt > 0) this.emit("fetch-retry-resolved", {
|
|
14454
|
+
attempts: attempt,
|
|
14455
|
+
outcome: "succeeded"
|
|
14456
|
+
});
|
|
14457
|
+
return;
|
|
14458
|
+
} catch (e) {
|
|
14459
|
+
const err = e;
|
|
14460
|
+
if (!isWantNotValidError(err)) {
|
|
14461
|
+
if (attempt > 0) this.emit("fetch-retry-resolved", {
|
|
14462
|
+
attempts: attempt,
|
|
14463
|
+
outcome: "failed"
|
|
14464
|
+
});
|
|
14465
|
+
throw errors_createGitError.fetchFailed(err.message);
|
|
14466
|
+
}
|
|
14467
|
+
attempt++;
|
|
14468
|
+
this.emit("fetch-retry", {
|
|
14469
|
+
attempt,
|
|
14470
|
+
delayMs: FETCH_RETRY_DELAY_MS,
|
|
14471
|
+
error: err.message
|
|
14472
|
+
});
|
|
14473
|
+
logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
|
|
14474
|
+
await (0, promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
|
|
14475
|
+
}
|
|
14476
|
+
}
|
|
14477
|
+
async reset(ref, mode = "mixed") {
|
|
14478
|
+
await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
|
|
14479
|
+
`--${mode}`,
|
|
14480
|
+
ref
|
|
14481
|
+
]));
|
|
14482
|
+
}
|
|
14483
|
+
async checkout(paths) {
|
|
14484
|
+
await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
|
|
14485
|
+
}
|
|
14486
|
+
async runGitCommandWithIndexLockRetry(command, operation) {
|
|
14487
|
+
let lastError;
|
|
14488
|
+
for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
|
|
14489
|
+
await operation();
|
|
14490
|
+
return;
|
|
14491
|
+
} catch (e) {
|
|
14492
|
+
lastError = e;
|
|
14493
|
+
if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
|
|
14494
|
+
await (0, promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
|
|
14495
|
+
}
|
|
14496
|
+
throw errors_createGitError.commandFailed(command, lastError.message);
|
|
14497
|
+
}
|
|
14498
|
+
isIndexLockError(error) {
|
|
14499
|
+
const message = error.message ?? "";
|
|
14500
|
+
return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
|
|
14501
|
+
}
|
|
14502
|
+
async stash(message) {
|
|
14503
|
+
try {
|
|
14504
|
+
const args = [
|
|
14505
|
+
"stash",
|
|
14506
|
+
"push",
|
|
14507
|
+
"--include-untracked"
|
|
14508
|
+
];
|
|
14509
|
+
if (message) args.push("-m", message);
|
|
14510
|
+
const result = await this.git.raw(args);
|
|
14511
|
+
return !result.includes("No local changes to save");
|
|
14512
|
+
} catch (e) {
|
|
14513
|
+
throw errors_createGitError.commandFailed("stash", e.message);
|
|
14514
|
+
}
|
|
14515
|
+
}
|
|
14516
|
+
async stashPop() {
|
|
14517
|
+
try {
|
|
14518
|
+
await this.git.raw([
|
|
14519
|
+
"stash",
|
|
14520
|
+
"pop"
|
|
14521
|
+
]);
|
|
14522
|
+
} catch (e) {
|
|
14523
|
+
throw errors_createGitError.commandFailed("stash pop", e.message);
|
|
14524
|
+
}
|
|
14525
|
+
}
|
|
14526
|
+
async clean(files) {
|
|
14527
|
+
try {
|
|
14528
|
+
await this.git.clean(CleanOptions.FORCE, files);
|
|
14529
|
+
} catch (e) {
|
|
14530
|
+
throw errors_createGitError.commandFailed("clean", e.message);
|
|
14531
|
+
}
|
|
14532
|
+
}
|
|
14533
|
+
async diffNames(fromCommit, toCommit) {
|
|
14534
|
+
try {
|
|
14535
|
+
const diffResult = await this.git.diff([
|
|
14536
|
+
fromCommit,
|
|
14537
|
+
toCommit,
|
|
14538
|
+
"--name-only"
|
|
14539
|
+
]);
|
|
14540
|
+
const files = diffResult.split("\n").filter((f)=>f.trim());
|
|
14541
|
+
return files;
|
|
14542
|
+
} catch (e) {
|
|
14543
|
+
throw errors_createGitError.commandFailed("diff", e.message);
|
|
14544
|
+
}
|
|
14545
|
+
}
|
|
14546
|
+
async getAheadBehind(localRef, remoteRef) {
|
|
14547
|
+
try {
|
|
14548
|
+
const result = await this.git.raw([
|
|
14549
|
+
"rev-list",
|
|
14550
|
+
"--left-right",
|
|
14551
|
+
"--count",
|
|
14552
|
+
`${remoteRef}...${localRef}`
|
|
14553
|
+
]);
|
|
14554
|
+
const [behind, ahead] = result.trim().split("\t").map(Number);
|
|
14555
|
+
return {
|
|
14556
|
+
ahead,
|
|
14557
|
+
behind
|
|
14558
|
+
};
|
|
14559
|
+
} catch (e) {
|
|
14560
|
+
throw errors_createGitError.commandFailed("rev-list", e.message);
|
|
14561
|
+
}
|
|
14562
|
+
}
|
|
14563
|
+
async show(refPath) {
|
|
14564
|
+
try {
|
|
14565
|
+
const content = await this.git.show([
|
|
14566
|
+
refPath
|
|
14567
|
+
]);
|
|
14568
|
+
return content;
|
|
14569
|
+
} catch (e) {
|
|
14570
|
+
throw errors_createGitError.commandFailed("show", e.message);
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14573
|
+
async push(url, refSpec, force = false) {
|
|
14574
|
+
try {
|
|
14575
|
+
const args = force ? [
|
|
14576
|
+
"push",
|
|
14577
|
+
"--force",
|
|
14578
|
+
url,
|
|
14579
|
+
refSpec
|
|
14580
|
+
] : [
|
|
14581
|
+
"push",
|
|
14582
|
+
url,
|
|
14583
|
+
refSpec
|
|
14584
|
+
];
|
|
14585
|
+
await this.git.raw(args);
|
|
14586
|
+
} catch (e) {
|
|
14587
|
+
throw errors_createGitError.commandFailed("push", e.message);
|
|
14588
|
+
}
|
|
14589
|
+
}
|
|
14590
|
+
async rebase(onto) {
|
|
14591
|
+
try {
|
|
14592
|
+
await this.git.raw([
|
|
14593
|
+
"rebase",
|
|
14594
|
+
onto
|
|
14595
|
+
]);
|
|
14596
|
+
} catch (e) {
|
|
14597
|
+
throw errors_createGitError.commandFailed("rebase", e.message);
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
async rebaseAbort() {
|
|
14601
|
+
try {
|
|
14602
|
+
await this.git.raw([
|
|
14603
|
+
"rebase",
|
|
14604
|
+
"--abort"
|
|
14605
|
+
]);
|
|
14606
|
+
} catch (e) {
|
|
14607
|
+
throw errors_createGitError.commandFailed("rebase --abort", e.message);
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
async isAncestor(ancestorRef, descendantRef) {
|
|
14611
|
+
try {
|
|
14612
|
+
const mergeBase = (await this.git.raw([
|
|
14613
|
+
"merge-base",
|
|
14614
|
+
ancestorRef,
|
|
14615
|
+
descendantRef
|
|
14616
|
+
])).trim();
|
|
14617
|
+
const ancestorCommit = (await this.git.revparse([
|
|
14618
|
+
ancestorRef
|
|
14619
|
+
])).trim();
|
|
14620
|
+
return mergeBase === ancestorCommit;
|
|
14621
|
+
} catch {
|
|
14622
|
+
return false;
|
|
14623
|
+
}
|
|
14624
|
+
}
|
|
14625
|
+
async mergeFastForward(ref) {
|
|
14626
|
+
await this.runGitCommandWithIndexLockRetry("merge --ff-only", ()=>this.git.raw([
|
|
14627
|
+
"merge",
|
|
14628
|
+
"--ff-only",
|
|
14629
|
+
ref
|
|
14630
|
+
]));
|
|
14631
|
+
}
|
|
14632
|
+
async deleteRef(ref) {
|
|
14633
|
+
try {
|
|
14765
14634
|
await this.git.raw([
|
|
14766
|
-
"
|
|
14767
|
-
"-
|
|
14768
|
-
|
|
14769
|
-
"--",
|
|
14770
|
-
relativePath
|
|
14635
|
+
"update-ref",
|
|
14636
|
+
"-d",
|
|
14637
|
+
ref
|
|
14771
14638
|
]);
|
|
14772
|
-
|
|
14773
|
-
|
|
14639
|
+
} catch (e) {}
|
|
14640
|
+
}
|
|
14641
|
+
async getRemotes() {
|
|
14642
|
+
try {
|
|
14643
|
+
const remotes = await this.git.getRemotes(true);
|
|
14644
|
+
return remotes.map((r)=>({
|
|
14645
|
+
name: r.name,
|
|
14646
|
+
fetchUrl: r.refs.fetch
|
|
14647
|
+
}));
|
|
14774
14648
|
} catch (e) {
|
|
14775
|
-
throw errors_createGitError.commandFailed("
|
|
14649
|
+
throw errors_createGitError.commandFailed("remote", e.message);
|
|
14776
14650
|
}
|
|
14777
|
-
return discarded;
|
|
14778
|
-
}
|
|
14779
|
-
async getHeadFileMode(relativePath) {
|
|
14780
|
-
const output = await this.git.raw([
|
|
14781
|
-
"ls-tree",
|
|
14782
|
-
"HEAD",
|
|
14783
|
-
"--",
|
|
14784
|
-
relativePath
|
|
14785
|
-
]);
|
|
14786
|
-
const match = output.match(/^(\d{6})\s/);
|
|
14787
|
-
return match?.[1] ?? null;
|
|
14788
|
-
}
|
|
14789
|
-
async hasSameContentAsHead(relativePath) {
|
|
14790
|
-
const headHash = (await this.git.revparse([
|
|
14791
|
-
`HEAD:${relativePath}`
|
|
14792
|
-
])).trim();
|
|
14793
|
-
const worktreeHash = (await this.git.raw([
|
|
14794
|
-
"hash-object",
|
|
14795
|
-
"--",
|
|
14796
|
-
relativePath
|
|
14797
|
-
])).trim();
|
|
14798
|
-
return headHash === worktreeHash;
|
|
14799
|
-
}
|
|
14800
|
-
async hasSameIndexContentAsHead(relativePath) {
|
|
14801
|
-
const headHash = (await this.git.revparse([
|
|
14802
|
-
`HEAD:${relativePath}`
|
|
14803
|
-
])).trim();
|
|
14804
|
-
const indexHash = (await this.git.revparse([
|
|
14805
|
-
`:${relativePath}`
|
|
14806
|
-
])).trim();
|
|
14807
|
-
return headHash === indexHash;
|
|
14808
14651
|
}
|
|
14809
|
-
async
|
|
14810
|
-
const
|
|
14811
|
-
|
|
14812
|
-
const mode = executable ? 73 | stat.mode : -74 & stat.mode;
|
|
14813
|
-
if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
|
|
14652
|
+
async isGitInitialized() {
|
|
14653
|
+
const gitPath = external_path_default().join(this.repoPath, ".git");
|
|
14654
|
+
return external_fs_.existsSync(gitPath);
|
|
14814
14655
|
}
|
|
14815
|
-
async
|
|
14816
|
-
const
|
|
14817
|
-
|
|
14656
|
+
async removeEmptyDirectories(removedFiles) {
|
|
14657
|
+
const directoriesToCheck = new Set();
|
|
14658
|
+
for (const file of removedFiles){
|
|
14659
|
+
let dir = external_path_default().dirname(file);
|
|
14660
|
+
while(dir && "." !== dir){
|
|
14661
|
+
directoriesToCheck.add(dir);
|
|
14662
|
+
const parentDir = external_path_default().dirname(dir);
|
|
14663
|
+
if (parentDir === dir) break;
|
|
14664
|
+
dir = parentDir;
|
|
14665
|
+
}
|
|
14666
|
+
}
|
|
14667
|
+
const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
|
|
14668
|
+
for (const dir of sortedDirs)try {
|
|
14669
|
+
const dirPath = external_path_default().join(this.repoPath, dir);
|
|
14670
|
+
if (!external_fs_.existsSync(dirPath)) continue;
|
|
14671
|
+
const entries = await external_fs_.promises.readdir(dirPath);
|
|
14672
|
+
if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
|
|
14673
|
+
} catch (error) {}
|
|
14818
14674
|
}
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14675
|
+
}
|
|
14676
|
+
async function getStagedFileChanges(git, gitService) {
|
|
14677
|
+
try {
|
|
14678
|
+
const status = await git.status();
|
|
14679
|
+
const renames = status.renamed || [];
|
|
14680
|
+
const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
|
|
14681
|
+
const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
|
|
14682
|
+
const stagedRenames = [];
|
|
14683
|
+
for (const f of stagedFiles)if ("R" === f.index) {
|
|
14684
|
+
const rename = renames.find((r)=>r.to === f.path);
|
|
14685
|
+
if (rename) stagedRenames.push({
|
|
14686
|
+
path: rename.to,
|
|
14687
|
+
type: "rename",
|
|
14688
|
+
from: rename.from
|
|
14829
14689
|
});
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14690
|
+
}
|
|
14691
|
+
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
14692
|
+
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
14693
|
+
const changes = [];
|
|
14694
|
+
for (const f of stagedFiles){
|
|
14695
|
+
if ("R" !== f.index) {
|
|
14696
|
+
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
14697
|
+
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
14698
|
+
path: f.path,
|
|
14699
|
+
type: "unlink"
|
|
14700
|
+
});
|
|
14701
|
+
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
14702
|
+
path: f.path,
|
|
14703
|
+
type: "add"
|
|
14704
|
+
});
|
|
14705
|
+
else changes.push({
|
|
14706
|
+
path: f.path,
|
|
14707
|
+
type: "change"
|
|
14708
|
+
});
|
|
14709
|
+
}
|
|
14839
14710
|
}
|
|
14840
|
-
attempt++;
|
|
14841
|
-
this.emit("fetch-retry", {
|
|
14842
|
-
attempt,
|
|
14843
|
-
delayMs: FETCH_RETRY_DELAY_MS,
|
|
14844
|
-
error: err.message
|
|
14845
|
-
});
|
|
14846
|
-
logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
|
|
14847
|
-
await (0, external_timers_promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
|
|
14848
14711
|
}
|
|
14712
|
+
changes.push(...stagedRenames);
|
|
14713
|
+
return changes;
|
|
14714
|
+
} catch (e) {
|
|
14715
|
+
throw errors_createGitError.commandFailed("status", e.message);
|
|
14849
14716
|
}
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14717
|
+
}
|
|
14718
|
+
async function getCurrentOrFallbackBranchName(git) {
|
|
14719
|
+
const branchRef = (await git.revparse([
|
|
14720
|
+
"--abbrev-ref",
|
|
14721
|
+
"HEAD"
|
|
14722
|
+
])).trim();
|
|
14723
|
+
if ("HEAD" !== branchRef) return branchRef;
|
|
14724
|
+
const [localBranches, remoteBranches] = await Promise.all([
|
|
14725
|
+
git.raw([
|
|
14726
|
+
"branch",
|
|
14727
|
+
"--format=%(refname:short)",
|
|
14728
|
+
"--contains",
|
|
14729
|
+
"HEAD"
|
|
14730
|
+
]),
|
|
14731
|
+
git.raw([
|
|
14732
|
+
"branch",
|
|
14733
|
+
"-r",
|
|
14734
|
+
"--format=%(refname:short)",
|
|
14735
|
+
"--contains",
|
|
14736
|
+
"HEAD"
|
|
14737
|
+
])
|
|
14738
|
+
]);
|
|
14739
|
+
const branchNames = [
|
|
14740
|
+
...parseBranchList(localBranches),
|
|
14741
|
+
...parseBranchList(remoteBranches).filter((branchName)=>!branchName.endsWith("/HEAD")).map((branchName)=>branchName.replace(/^[^/]+\//, ""))
|
|
14742
|
+
];
|
|
14743
|
+
const branchSet = new Set(branchNames);
|
|
14744
|
+
if (branchSet.has("master")) return "master";
|
|
14745
|
+
if (branchSet.has("main")) return "main";
|
|
14746
|
+
return "master";
|
|
14747
|
+
}
|
|
14748
|
+
function parseBranchList(output) {
|
|
14749
|
+
return output.split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
|
|
14750
|
+
}
|
|
14751
|
+
function anvil_api_getDefaultAnvilUrl() {
|
|
14752
|
+
return resolveAnvilUrl();
|
|
14753
|
+
}
|
|
14754
|
+
async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
|
|
14755
|
+
const authToken = await auth_getValidAuthToken(anvilUrl, username);
|
|
14756
|
+
try {
|
|
14757
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
|
|
14758
|
+
method: "POST",
|
|
14759
|
+
headers: {
|
|
14760
|
+
Authorization: `Bearer ${authToken}`,
|
|
14761
|
+
"Content-Type": "application/json"
|
|
14762
|
+
},
|
|
14763
|
+
body: JSON.stringify({
|
|
14764
|
+
app_id: appId
|
|
14765
|
+
})
|
|
14766
|
+
});
|
|
14767
|
+
const data = await resp.json();
|
|
14768
|
+
if (resp.ok) return {
|
|
14769
|
+
valid: data.valid ?? false,
|
|
14770
|
+
app_name: data.app_name
|
|
14771
|
+
};
|
|
14772
|
+
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14773
|
+
if (403 === resp.status) throw createAppError.accessDenied(appId);
|
|
14774
|
+
if (404 === resp.status) throw createAppError.notFound(appId);
|
|
14775
|
+
logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
|
|
14776
|
+
return {
|
|
14777
|
+
valid: false,
|
|
14778
|
+
error: data.error || `Server error: ${resp.status}`
|
|
14779
|
+
};
|
|
14780
|
+
} catch (error) {
|
|
14781
|
+
if (error.type) throw error;
|
|
14782
|
+
throw errors_createNetworkError.network(error.message);
|
|
14858
14783
|
}
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14784
|
+
}
|
|
14785
|
+
async function listAppsForCheckout(options = {}) {
|
|
14786
|
+
const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
|
|
14787
|
+
const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
14788
|
+
const params = new URLSearchParams();
|
|
14789
|
+
if ("number" == typeof options.limit) params.set("limit", String(options.limit));
|
|
14790
|
+
if (options.cursor) params.set("cursor", options.cursor);
|
|
14791
|
+
if (options.q && options.q.trim()) params.set("q", options.q.trim());
|
|
14792
|
+
const query = params.toString();
|
|
14793
|
+
const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
|
|
14794
|
+
try {
|
|
14795
|
+
const resp = await fetch(url, {
|
|
14796
|
+
method: "GET",
|
|
14797
|
+
headers: {
|
|
14798
|
+
Authorization: `Bearer ${authToken}`
|
|
14799
|
+
},
|
|
14800
|
+
signal: options.signal
|
|
14801
|
+
});
|
|
14802
|
+
if (!resp.ok) {
|
|
14803
|
+
if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
|
|
14804
|
+
throw errors_createNetworkError.server(resp.status, resp.statusText);
|
|
14868
14805
|
}
|
|
14869
|
-
|
|
14806
|
+
const data = await resp.json();
|
|
14807
|
+
return {
|
|
14808
|
+
apps: Array.isArray(data.apps) ? data.apps : [],
|
|
14809
|
+
next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
|
|
14810
|
+
};
|
|
14811
|
+
} catch (error) {
|
|
14812
|
+
if (error.type) throw error;
|
|
14813
|
+
throw errors_createNetworkError.network(error.message);
|
|
14814
|
+
}
|
|
14815
|
+
}
|
|
14816
|
+
function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14817
|
+
const url = new URL(gitUrl);
|
|
14818
|
+
const encodedToken = encodeURIComponent(authToken);
|
|
14819
|
+
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
|
|
14820
|
+
}
|
|
14821
|
+
function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14822
|
+
const url = new URL(anvilUrl);
|
|
14823
|
+
const encodedToken = encodeURIComponent(authToken);
|
|
14824
|
+
return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
|
|
14825
|
+
}
|
|
14826
|
+
function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
|
|
14827
|
+
return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
|
|
14828
|
+
}
|
|
14829
|
+
async function getLatestVersion() {
|
|
14830
|
+
try {
|
|
14831
|
+
const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
|
|
14832
|
+
if (!response.ok) return null;
|
|
14833
|
+
const data = await response.json();
|
|
14834
|
+
return data.version;
|
|
14835
|
+
} catch (e) {
|
|
14836
|
+
return null;
|
|
14870
14837
|
}
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14838
|
+
}
|
|
14839
|
+
function filterCandidates(candidates, explicitUrl, explicitUsername) {
|
|
14840
|
+
let filtered = candidates;
|
|
14841
|
+
if (explicitUrl) {
|
|
14842
|
+
const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
|
|
14843
|
+
filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
|
|
14874
14844
|
}
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14845
|
+
if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
|
|
14846
|
+
return filtered;
|
|
14847
|
+
}
|
|
14848
|
+
function formatCandidateLabel(candidate) {
|
|
14849
|
+
const parts = [
|
|
14850
|
+
candidate.appId
|
|
14851
|
+
];
|
|
14852
|
+
if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
|
|
14853
|
+
else parts.push(`(${candidate.detectedUrl})`);
|
|
14854
|
+
parts.push(`- ${candidate.description}`);
|
|
14855
|
+
return parts.join(" ");
|
|
14856
|
+
}
|
|
14857
|
+
function lookupRemoteInfoForAppId(appId, detectedRemotes) {
|
|
14858
|
+
const matches = detectedRemotes.filter((c)=>c.appId === appId);
|
|
14859
|
+
if (0 === matches.length) return {};
|
|
14860
|
+
const withUsername = matches.find((c)=>c.detectedUsername);
|
|
14861
|
+
if (withUsername) return {
|
|
14862
|
+
detectedUrl: withUsername.detectedUrl,
|
|
14863
|
+
detectedUsername: withUsername.detectedUsername
|
|
14864
|
+
};
|
|
14865
|
+
return {
|
|
14866
|
+
detectedUrl: matches[0].detectedUrl,
|
|
14867
|
+
detectedUsername: matches[0].detectedUsername
|
|
14868
|
+
};
|
|
14869
|
+
}
|
|
14870
|
+
async function detectAppIdsFromAllRemotes(repoPath) {
|
|
14871
|
+
const git = esm_default(repoPath);
|
|
14872
|
+
const out = [];
|
|
14873
|
+
try {
|
|
14874
|
+
const remotes = await git.getRemotes(true);
|
|
14875
|
+
for (const remote of remotes){
|
|
14876
|
+
const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
|
|
14877
|
+
if (httpMatch) {
|
|
14878
|
+
const [, host, detectedAppId] = httpMatch;
|
|
14879
|
+
out.push({
|
|
14880
|
+
appId: detectedAppId,
|
|
14881
|
+
source: "remote",
|
|
14882
|
+
description: `Git remote '${remote.name}'`,
|
|
14883
|
+
detectedUrl: normalizeAnvilUrl(host)
|
|
14884
|
+
});
|
|
14885
|
+
continue;
|
|
14886
|
+
}
|
|
14887
|
+
const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
|
|
14888
|
+
if (sshMatch) {
|
|
14889
|
+
const [, usernamePart, host, , detectedAppId] = sshMatch;
|
|
14890
|
+
const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
|
|
14891
|
+
const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
|
|
14892
|
+
out.push({
|
|
14893
|
+
appId: detectedAppId,
|
|
14894
|
+
source: "remote",
|
|
14895
|
+
description: `Git remote '${remote.name}' (SSH)`,
|
|
14896
|
+
detectedUrl: normalizeAnvilUrl(host),
|
|
14897
|
+
detectedUsername: finalUsername
|
|
14898
|
+
});
|
|
14899
|
+
}
|
|
14887
14900
|
}
|
|
14888
|
-
}
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14901
|
+
} catch (_e) {}
|
|
14902
|
+
return out;
|
|
14903
|
+
}
|
|
14904
|
+
async function detectAppIdsByCommitLookup(repoPath, options) {
|
|
14905
|
+
const anvilUrl = options.anvilUrl || resolveAnvilUrl();
|
|
14906
|
+
const username = options.username;
|
|
14907
|
+
const git = esm_default(repoPath);
|
|
14908
|
+
const out = [];
|
|
14909
|
+
try {
|
|
14910
|
+
const authToken = await auth_getValidAuthToken(anvilUrl, username);
|
|
14911
|
+
const commitId = (await git.revparse([
|
|
14912
|
+
"HEAD"
|
|
14913
|
+
]))?.trim();
|
|
14914
|
+
if (!commitId) return out;
|
|
14915
|
+
const branchName = await getCurrentOrFallbackBranchName(git);
|
|
14916
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
|
|
14917
|
+
method: "POST",
|
|
14918
|
+
headers: {
|
|
14919
|
+
Authorization: `Bearer ${authToken}`,
|
|
14920
|
+
"Content-Type": "application/json"
|
|
14921
|
+
},
|
|
14922
|
+
body: JSON.stringify({
|
|
14923
|
+
commit_id: commitId,
|
|
14924
|
+
branch_name: branchName
|
|
14925
|
+
})
|
|
14926
|
+
});
|
|
14927
|
+
logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${branchName} returned ${resp.status}`);
|
|
14928
|
+
if (!resp.ok) {
|
|
14929
|
+
const errorBody = await resp.text();
|
|
14930
|
+
logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
|
|
14897
14931
|
}
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14932
|
+
if (resp.ok) {
|
|
14933
|
+
const data = await resp.json();
|
|
14934
|
+
logger_logger.debug(`data ${JSON.stringify(data)}`);
|
|
14935
|
+
const apps = data?.apps ?? [];
|
|
14936
|
+
for (const app of apps)if (app?.app_id) out.push({
|
|
14937
|
+
appId: app.app_id,
|
|
14938
|
+
source: "remote",
|
|
14939
|
+
description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
|
|
14940
|
+
});
|
|
14904
14941
|
}
|
|
14942
|
+
} catch (_e) {}
|
|
14943
|
+
return out;
|
|
14944
|
+
}
|
|
14945
|
+
const external_fs_promises_namespaceObject = require("fs/promises");
|
|
14946
|
+
var external_fs_promises_default = /*#__PURE__*/ __webpack_require__.n(external_fs_promises_namespaceObject);
|
|
14947
|
+
function normalizeLineEndings(content) {
|
|
14948
|
+
return content.replace(/\r\n/g, "\n");
|
|
14949
|
+
}
|
|
14950
|
+
function pythonifyName(name) {
|
|
14951
|
+
return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
|
|
14952
|
+
}
|
|
14953
|
+
function extractPythonName(relativePath, skipParts = 1) {
|
|
14954
|
+
const parts = relativePath.slice(0, -3).split("/");
|
|
14955
|
+
const isPackage = relativePath.endsWith("__init__.py");
|
|
14956
|
+
const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
|
|
14957
|
+
return relevantParts.map(pythonifyName).join(".");
|
|
14958
|
+
}
|
|
14959
|
+
function deepEqual(a, b) {
|
|
14960
|
+
if (a === b) return true;
|
|
14961
|
+
if (null == a || null == b) return false;
|
|
14962
|
+
if (typeof a !== typeof b) return false;
|
|
14963
|
+
if ("object" != typeof a) return a === b;
|
|
14964
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
14965
|
+
if (Array.isArray(a)) {
|
|
14966
|
+
if (a.length !== b.length) return false;
|
|
14967
|
+
return a.every((item, index)=>deepEqual(item, b[index]));
|
|
14905
14968
|
}
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
}
|
|
14969
|
+
const keysA = Object.keys(a);
|
|
14970
|
+
const keysB = Object.keys(b);
|
|
14971
|
+
if (keysA.length !== keysB.length) return false;
|
|
14972
|
+
return keysA.every((key)=>deepEqual(a[key], b[key]));
|
|
14973
|
+
}
|
|
14974
|
+
function parseHtmlWithFrontmatter(htmlContent) {
|
|
14975
|
+
const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14976
|
+
const match = htmlContent.match(frontmatterPattern);
|
|
14977
|
+
if (match) {
|
|
14978
|
+
const yamlStr = match[1];
|
|
14979
|
+
const html = match[2];
|
|
14980
|
+
const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
|
|
14981
|
+
return {
|
|
14982
|
+
frontmatter,
|
|
14983
|
+
html
|
|
14984
|
+
};
|
|
14918
14985
|
}
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14986
|
+
return {
|
|
14987
|
+
frontmatter: {},
|
|
14988
|
+
html: htmlContent
|
|
14989
|
+
};
|
|
14990
|
+
}
|
|
14991
|
+
function formatHttpError(prefix, status, body) {
|
|
14992
|
+
if (!body) return `${prefix}: ${status}`;
|
|
14993
|
+
try {
|
|
14994
|
+
const parsed = JSON.parse(body);
|
|
14995
|
+
if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
|
|
14996
|
+
} catch {}
|
|
14997
|
+
return `${prefix}: ${status} ${body}`;
|
|
14998
|
+
}
|
|
14999
|
+
const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
|
|
15000
|
+
async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
|
|
15001
|
+
const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
|
|
15002
|
+
try {
|
|
15003
|
+
const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
|
|
15004
|
+
if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
|
|
15005
|
+
const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
|
|
15006
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
15007
|
+
return true;
|
|
15008
|
+
} catch (error) {
|
|
15009
|
+
if ("ENOENT" !== error.code) throw error;
|
|
15010
|
+
await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
|
|
15011
|
+
return true;
|
|
14935
15012
|
}
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
15013
|
+
}
|
|
15014
|
+
const ANVIL_DEPS_DIR = ".anvil/deps";
|
|
15015
|
+
const ANVIL_DEPS_MANIFEST = "manifest.json";
|
|
15016
|
+
const DEPS_MANIFEST_VERSION = 3;
|
|
15017
|
+
const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
|
|
15018
|
+
async function fetchAllDependencies(appId, anvilUrl, options = {}) {
|
|
15019
|
+
const { commit, includeDocs = true, knownDependencies = [] } = options;
|
|
15020
|
+
anvilUrl ??= await resolveAuthAnvilUrl();
|
|
15021
|
+
const token = await auth_getValidAuthToken(anvilUrl, options.username);
|
|
15022
|
+
const body = {
|
|
15023
|
+
include_docs: includeDocs,
|
|
15024
|
+
known_dependencies: knownDependencies
|
|
15025
|
+
};
|
|
15026
|
+
if (commit) body.commit = commit;
|
|
15027
|
+
const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
|
|
15028
|
+
method: "POST",
|
|
15029
|
+
headers: {
|
|
15030
|
+
Authorization: `Bearer ${token}`,
|
|
15031
|
+
"Content-Type": "application/json"
|
|
15032
|
+
},
|
|
15033
|
+
body: JSON.stringify(body)
|
|
15034
|
+
});
|
|
15035
|
+
if (!resp.ok) {
|
|
15036
|
+
const errorBody = await resp.text();
|
|
15037
|
+
throw new Error(formatHttpError("Failed to fetch dependencies", resp.status, errorBody));
|
|
14945
15038
|
}
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
15039
|
+
return await resp.json();
|
|
15040
|
+
}
|
|
15041
|
+
async function refreshDependencyCache(projectRoot, appId, anvilUrl, options = {}) {
|
|
15042
|
+
auth_setRepoContext(projectRoot);
|
|
15043
|
+
anvilUrl ??= await resolveAuthAnvilUrl();
|
|
15044
|
+
const includeDocs = options.includeDocs ?? true;
|
|
15045
|
+
const git = new GitService(projectRoot);
|
|
15046
|
+
const commit = options.commit ?? await git.getCommitId();
|
|
15047
|
+
const sourceYaml = await git.show(`${commit}:anvil.yaml`).catch((error)=>{
|
|
15048
|
+
if (options.commit) return null;
|
|
15049
|
+
throw error;
|
|
15050
|
+
});
|
|
15051
|
+
const sourceDependencies = null === sourceYaml ? null : normalizeAnvilYamlDependenciesFromContent(sourceYaml);
|
|
15052
|
+
const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
|
|
15053
|
+
const response = await fetchAllDependencies(appId, anvilUrl, {
|
|
15054
|
+
username: options.username,
|
|
15055
|
+
commit,
|
|
15056
|
+
includeDocs,
|
|
15057
|
+
knownDependencies
|
|
15058
|
+
});
|
|
15059
|
+
if (!response.resolved_dependencies.length) {
|
|
15060
|
+
await clearDepsCache(projectRoot);
|
|
15061
|
+
return {
|
|
15062
|
+
response,
|
|
15063
|
+
addedGitignoreEntry: false
|
|
15064
|
+
};
|
|
14962
15065
|
}
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
15066
|
+
const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
|
|
15067
|
+
await writeAllDepsToCache(projectRoot, response, {
|
|
15068
|
+
includeDocs,
|
|
15069
|
+
sourceDependencies
|
|
15070
|
+
});
|
|
15071
|
+
return {
|
|
15072
|
+
response,
|
|
15073
|
+
addedGitignoreEntry
|
|
15074
|
+
};
|
|
15075
|
+
}
|
|
15076
|
+
function depsDir(projectRoot) {
|
|
15077
|
+
return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
|
|
15078
|
+
}
|
|
15079
|
+
function depsManifestPath(projectRoot) {
|
|
15080
|
+
return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
|
|
15081
|
+
}
|
|
15082
|
+
function isResolvedDependencyManifestEntry(value) {
|
|
15083
|
+
if (!value || "object" != typeof value) return false;
|
|
15084
|
+
const entry = value;
|
|
15085
|
+
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);
|
|
15086
|
+
}
|
|
15087
|
+
function isCachedDependencyManifestEntry(value) {
|
|
15088
|
+
if (!isResolvedDependencyManifestEntry(value)) return false;
|
|
15089
|
+
const entry = value;
|
|
15090
|
+
return "string" == typeof entry.package_name && entry.package_name.length > 0;
|
|
15091
|
+
}
|
|
15092
|
+
function isDepsCacheManifest(value) {
|
|
15093
|
+
if (!value || "object" != typeof value) return false;
|
|
15094
|
+
const manifest = value;
|
|
15095
|
+
return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && (null === manifest.source_dependencies || Array.isArray(manifest.source_dependencies)) && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
|
|
15096
|
+
}
|
|
15097
|
+
async function readDepsCacheManifest(projectRoot) {
|
|
15098
|
+
try {
|
|
15099
|
+
const raw = await external_fs_promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
|
|
15100
|
+
const parsed = JSON.parse(raw);
|
|
15101
|
+
return isDepsCacheManifest(parsed) ? parsed : void 0;
|
|
15102
|
+
} catch {
|
|
15103
|
+
return;
|
|
14972
15104
|
}
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
15105
|
+
}
|
|
15106
|
+
async function getKnownDependenciesForCache(projectRoot, includeDocs) {
|
|
15107
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
15108
|
+
if (!manifest || manifest.includeDocs !== includeDocs) return [];
|
|
15109
|
+
return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
|
|
15110
|
+
app_id,
|
|
15111
|
+
commit_id
|
|
15112
|
+
}));
|
|
15113
|
+
}
|
|
15114
|
+
function getDependencyWatchSubscriptionsFromManifest(manifest) {
|
|
15115
|
+
if (!manifest) return [];
|
|
15116
|
+
const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
|
|
15117
|
+
dep.app_id,
|
|
15118
|
+
dep
|
|
15119
|
+
]));
|
|
15120
|
+
return manifest.resolved_dependencies.flatMap((dep)=>{
|
|
15121
|
+
if (!dep.branch) return [];
|
|
15122
|
+
return [
|
|
15123
|
+
{
|
|
15124
|
+
appId: dep.app_id,
|
|
15125
|
+
branch: dep.branch,
|
|
15126
|
+
packageName: cachedByAppId.get(dep.app_id)?.package_name
|
|
15127
|
+
}
|
|
15128
|
+
];
|
|
15129
|
+
});
|
|
15130
|
+
}
|
|
15131
|
+
function normalizeAnvilYamlDependenciesFromContent(content) {
|
|
15132
|
+
const parsed = external_js_yaml_default().load(content);
|
|
15133
|
+
if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
|
|
15134
|
+
return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
|
|
15135
|
+
}
|
|
15136
|
+
function normalizeAnvilYamlDependenciesValue(value) {
|
|
15137
|
+
if (!Array.isArray(value)) return [];
|
|
15138
|
+
return value.map(normalizeForComparison);
|
|
15139
|
+
}
|
|
15140
|
+
async function readAnvilYamlDependencies(projectRoot) {
|
|
15141
|
+
try {
|
|
15142
|
+
const content = await external_fs_promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
|
|
15143
|
+
return normalizeAnvilYamlDependenciesFromContent(content);
|
|
15144
|
+
} catch {
|
|
15145
|
+
return [];
|
|
14982
15146
|
}
|
|
14983
|
-
|
|
15147
|
+
}
|
|
15148
|
+
async function getDependencyCacheWarning(projectRoot) {
|
|
15149
|
+
const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
|
|
15150
|
+
if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
|
|
15151
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
15152
|
+
if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
|
|
15153
|
+
if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
|
|
15154
|
+
const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
|
|
15155
|
+
dependency.app_id,
|
|
15156
|
+
dependency
|
|
15157
|
+
]));
|
|
15158
|
+
for (const dependency of manifest.resolved_dependencies){
|
|
15159
|
+
const cached = cachedByAppId.get(dependency.app_id);
|
|
15160
|
+
if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
|
|
14984
15161
|
try {
|
|
14985
|
-
|
|
14986
|
-
"merge-base",
|
|
14987
|
-
ancestorRef,
|
|
14988
|
-
descendantRef
|
|
14989
|
-
])).trim();
|
|
14990
|
-
const ancestorCommit = (await this.git.revparse([
|
|
14991
|
-
ancestorRef
|
|
14992
|
-
])).trim();
|
|
14993
|
-
return mergeBase === ancestorCommit;
|
|
15162
|
+
await external_fs_promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
|
|
14994
15163
|
} catch {
|
|
14995
|
-
return
|
|
15164
|
+
return DEPENDENCY_CACHE_WARNING;
|
|
14996
15165
|
}
|
|
14997
15166
|
}
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15167
|
+
}
|
|
15168
|
+
function anvilYamlDependenciesEqual(left, right) {
|
|
15169
|
+
return deepEqual(left, right);
|
|
15170
|
+
}
|
|
15171
|
+
function normalizeForComparison(value) {
|
|
15172
|
+
if (Array.isArray(value)) return value.map(normalizeForComparison);
|
|
15173
|
+
if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
|
|
15174
|
+
key,
|
|
15175
|
+
normalizeForComparison(entryValue)
|
|
15176
|
+
]));
|
|
15177
|
+
return value;
|
|
15178
|
+
}
|
|
15179
|
+
function cachedManifestEntry(dep, packageName) {
|
|
15180
|
+
return {
|
|
15181
|
+
app_id: dep.app_id,
|
|
15182
|
+
commit_id: dep.commit_id,
|
|
15183
|
+
...dep.branch ? {
|
|
15184
|
+
branch: dep.branch
|
|
15185
|
+
} : {},
|
|
15186
|
+
package_name: packageName
|
|
15187
|
+
};
|
|
15188
|
+
}
|
|
15189
|
+
function safeDependencyPath(filePath, kind) {
|
|
15190
|
+
const normalizedPath = external_path_default().normalize(filePath);
|
|
15191
|
+
if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
|
|
15192
|
+
return normalizedPath;
|
|
15193
|
+
}
|
|
15194
|
+
function dependencyPythonPath(name, kind) {
|
|
15195
|
+
const parts = name.split(".");
|
|
15196
|
+
if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
|
|
15197
|
+
return external_path_default().join(...parts);
|
|
15198
|
+
}
|
|
15199
|
+
async function writeDepToCache(projectRoot, dep) {
|
|
15200
|
+
const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
|
|
15201
|
+
await external_fs_promises_default().rm(depDir, {
|
|
15202
|
+
recursive: true,
|
|
15203
|
+
force: true
|
|
15204
|
+
});
|
|
15205
|
+
await external_fs_promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
|
|
15206
|
+
recursive: true
|
|
15207
|
+
});
|
|
15208
|
+
await external_fs_promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
|
|
15209
|
+
recursive: true
|
|
15210
|
+
});
|
|
15211
|
+
await external_fs_promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
|
|
15212
|
+
package_name: dep.package_name
|
|
15213
|
+
}));
|
|
15214
|
+
for (const mod of dep.modules ?? []){
|
|
15215
|
+
const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
|
|
15216
|
+
if (mod.is_package) {
|
|
15217
|
+
await external_fs_promises_default().mkdir(modulePath, {
|
|
15218
|
+
recursive: true
|
|
15219
|
+
});
|
|
15220
|
+
await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
|
|
15221
|
+
} else {
|
|
15222
|
+
const moduleFile = `${modulePath}.py`;
|
|
15223
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
|
|
15224
|
+
recursive: true
|
|
15225
|
+
});
|
|
15226
|
+
await external_fs_promises_default().writeFile(moduleFile, mod.code);
|
|
15007
15227
|
}
|
|
15008
15228
|
}
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15229
|
+
for (const form of dep.forms ?? []){
|
|
15230
|
+
const { code, class_name, is_package, ...formTemplate } = form;
|
|
15231
|
+
const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
|
|
15232
|
+
if (is_package) {
|
|
15233
|
+
await external_fs_promises_default().mkdir(formPath, {
|
|
15234
|
+
recursive: true
|
|
15235
|
+
});
|
|
15236
|
+
await external_fs_promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
|
|
15237
|
+
await external_fs_promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
|
|
15238
|
+
} else {
|
|
15239
|
+
const formCodeFile = `${formPath}.py`;
|
|
15240
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(formCodeFile), {
|
|
15241
|
+
recursive: true
|
|
15242
|
+
});
|
|
15243
|
+
await external_fs_promises_default().writeFile(formCodeFile, code);
|
|
15244
|
+
await external_fs_promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
|
|
15245
|
+
}
|
|
15017
15246
|
}
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
}
|
|
15026
|
-
|
|
15247
|
+
for (const mod of dep.server_modules ?? []){
|
|
15248
|
+
const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
|
|
15249
|
+
if (mod.is_package) {
|
|
15250
|
+
await external_fs_promises_default().mkdir(modulePath, {
|
|
15251
|
+
recursive: true
|
|
15252
|
+
});
|
|
15253
|
+
await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
|
|
15254
|
+
} else {
|
|
15255
|
+
const moduleFile = `${modulePath}.py`;
|
|
15256
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
|
|
15257
|
+
recursive: true
|
|
15258
|
+
});
|
|
15259
|
+
await external_fs_promises_default().writeFile(moduleFile, mod.code);
|
|
15027
15260
|
}
|
|
15028
15261
|
}
|
|
15029
|
-
|
|
15030
|
-
const
|
|
15031
|
-
|
|
15262
|
+
for (const asset of dep.assets ?? []){
|
|
15263
|
+
const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
|
|
15264
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
15265
|
+
recursive: true
|
|
15266
|
+
});
|
|
15267
|
+
await external_fs_promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
|
|
15032
15268
|
}
|
|
15033
|
-
|
|
15034
|
-
const
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
const parentDir = external_path_default().dirname(dir);
|
|
15040
|
-
if (parentDir === dir) break;
|
|
15041
|
-
dir = parentDir;
|
|
15042
|
-
}
|
|
15043
|
-
}
|
|
15044
|
-
const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
|
|
15045
|
-
for (const dir of sortedDirs)try {
|
|
15046
|
-
const dirPath = external_path_default().join(this.repoPath, dir);
|
|
15047
|
-
if (!external_fs_.existsSync(dirPath)) continue;
|
|
15048
|
-
const entries = await external_fs_.promises.readdir(dirPath);
|
|
15049
|
-
if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
|
|
15050
|
-
} catch (error) {}
|
|
15269
|
+
for (const doc of dep.docs ?? []){
|
|
15270
|
+
const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
|
|
15271
|
+
await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
|
|
15272
|
+
recursive: true
|
|
15273
|
+
});
|
|
15274
|
+
await external_fs_promises_default().writeFile(targetPath, doc.content);
|
|
15051
15275
|
}
|
|
15052
15276
|
}
|
|
15053
|
-
async function
|
|
15277
|
+
async function writeAllDepsToCache(projectRoot, response, options = {}) {
|
|
15278
|
+
const includeDocs = options.includeDocs ?? true;
|
|
15279
|
+
const manifest = await readDepsCacheManifest(projectRoot);
|
|
15280
|
+
const canReuseExistingCache = manifest?.includeDocs === includeDocs;
|
|
15281
|
+
if (!canReuseExistingCache) await clearDepsCache(projectRoot);
|
|
15282
|
+
const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
|
|
15283
|
+
const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
|
|
15284
|
+
for (const dep of previousCached)if (!resolvedAppIds.has(dep.app_id)) await external_fs_promises_default().rm(external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name), {
|
|
15285
|
+
recursive: true,
|
|
15286
|
+
force: true
|
|
15287
|
+
});
|
|
15288
|
+
for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
|
|
15289
|
+
const returnedByAppId = new Map(response.dependencies.map((dep)=>[
|
|
15290
|
+
dep.app_id,
|
|
15291
|
+
dep
|
|
15292
|
+
]));
|
|
15293
|
+
const previousByAppId = new Map(previousCached.map((dep)=>[
|
|
15294
|
+
dep.app_id,
|
|
15295
|
+
dep
|
|
15296
|
+
]));
|
|
15297
|
+
const cachedDependencies = response.resolved_dependencies.map((dep)=>{
|
|
15298
|
+
const returned = returnedByAppId.get(dep.app_id);
|
|
15299
|
+
if (returned) return cachedManifestEntry(returned, returned.package_name);
|
|
15300
|
+
const previous = previousByAppId.get(dep.app_id);
|
|
15301
|
+
if (!previous) return;
|
|
15302
|
+
return cachedManifestEntry(dep, previous.package_name);
|
|
15303
|
+
}).filter((dep)=>void 0 !== dep);
|
|
15304
|
+
const newManifest = {
|
|
15305
|
+
version: DEPS_MANIFEST_VERSION,
|
|
15306
|
+
includeDocs,
|
|
15307
|
+
source_dependencies: void 0 === options.sourceDependencies ? await readAnvilYamlDependencies(projectRoot) : options.sourceDependencies,
|
|
15308
|
+
resolved_dependencies: response.resolved_dependencies,
|
|
15309
|
+
cached_dependencies: cachedDependencies
|
|
15310
|
+
};
|
|
15311
|
+
await external_fs_promises_default().mkdir(depsDir(projectRoot), {
|
|
15312
|
+
recursive: true
|
|
15313
|
+
});
|
|
15314
|
+
await external_fs_promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
|
|
15315
|
+
}
|
|
15316
|
+
async function clearDepsCache(projectRoot) {
|
|
15317
|
+
await external_fs_promises_default().rm(depsDir(projectRoot), {
|
|
15318
|
+
recursive: true,
|
|
15319
|
+
force: true
|
|
15320
|
+
});
|
|
15321
|
+
}
|
|
15322
|
+
async function getCachedPackages(projectRoot) {
|
|
15054
15323
|
try {
|
|
15055
|
-
const
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
const rename = renames.find((r)=>r.to === f.path);
|
|
15062
|
-
if (rename) stagedRenames.push({
|
|
15063
|
-
path: rename.to,
|
|
15064
|
-
type: "rename",
|
|
15065
|
-
from: rename.from
|
|
15066
|
-
});
|
|
15067
|
-
}
|
|
15068
|
-
const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
|
|
15069
|
-
const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
|
|
15070
|
-
const changes = [];
|
|
15071
|
-
for (const f of stagedFiles){
|
|
15072
|
-
if ("R" !== f.index) {
|
|
15073
|
-
if (!stagedModeOnlyPaths.has(f.path)) {
|
|
15074
|
-
if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
|
|
15075
|
-
path: f.path,
|
|
15076
|
-
type: "unlink"
|
|
15077
|
-
});
|
|
15078
|
-
else if ("A" === f.index || "?" === f.index) changes.push({
|
|
15079
|
-
path: f.path,
|
|
15080
|
-
type: "add"
|
|
15081
|
-
});
|
|
15082
|
-
else changes.push({
|
|
15083
|
-
path: f.path,
|
|
15084
|
-
type: "change"
|
|
15085
|
-
});
|
|
15086
|
-
}
|
|
15087
|
-
}
|
|
15088
|
-
}
|
|
15089
|
-
changes.push(...stagedRenames);
|
|
15090
|
-
return changes;
|
|
15091
|
-
} catch (e) {
|
|
15092
|
-
throw errors_createGitError.commandFailed("status", e.message);
|
|
15324
|
+
const entries = await external_fs_promises_default().readdir(depsDir(projectRoot), {
|
|
15325
|
+
withFileTypes: true
|
|
15326
|
+
});
|
|
15327
|
+
return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
|
|
15328
|
+
} catch {
|
|
15329
|
+
return [];
|
|
15093
15330
|
}
|
|
15094
15331
|
}
|
|
15095
15332
|
const external_chokidar_namespaceObject = require("chokidar");
|
|
@@ -15863,7 +16100,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15863
16100
|
if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
15864
16101
|
}
|
|
15865
16102
|
const componentTypeSpec_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15866
|
-
const
|
|
16103
|
+
const LEGACY_CUSTOM_COMPONENT_SPEC_RE = /^form:(?:([^:]+):)?(.+)$/;
|
|
16104
|
+
const PACKAGE_QUALIFIED_FORM_NAME_RE = /^([^.]+)\.(.+)$/;
|
|
15867
16105
|
const componentTypeSpec_PYTHON_KEYWORDS = new Set([
|
|
15868
16106
|
"False",
|
|
15869
16107
|
"None",
|
|
@@ -15903,32 +16141,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
15903
16141
|
"match",
|
|
15904
16142
|
"case"
|
|
15905
16143
|
]);
|
|
15906
|
-
const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)";
|
|
16144
|
+
const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form, form:APP_ID:Package.Form)";
|
|
15907
16145
|
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)";
|
|
15908
16146
|
function isValidIdentifierSegment(value) {
|
|
15909
16147
|
return value.length > 0 && componentTypeSpec_IDENTIFIER_RE.test(value) && !componentTypeSpec_PYTHON_KEYWORDS.has(value);
|
|
15910
16148
|
}
|
|
15911
16149
|
function isValidLegacyFormTypeReference(typeName) {
|
|
15912
|
-
|
|
15913
|
-
const rest = typeName.slice(5);
|
|
15914
|
-
if (0 === rest.length) return false;
|
|
15915
|
-
let classPath = rest;
|
|
15916
|
-
if (rest.startsWith("dep_")) {
|
|
15917
|
-
const splitIndex = rest.indexOf(":");
|
|
15918
|
-
if (splitIndex <= 0) return false;
|
|
15919
|
-
const depId = rest.slice(0, splitIndex);
|
|
15920
|
-
if (!DEP_ID_RE.test(depId)) return false;
|
|
15921
|
-
classPath = rest.slice(splitIndex + 1);
|
|
15922
|
-
}
|
|
15923
|
-
if (0 === classPath.length) return false;
|
|
15924
|
-
return classPath.split(".").every((part)=>isValidIdentifierSegment(part));
|
|
16150
|
+
return LEGACY_CUSTOM_COMPONENT_SPEC_RE.test(typeName);
|
|
15925
16151
|
}
|
|
15926
16152
|
function isValidPackageQualifiedComponentType(typeName) {
|
|
15927
16153
|
if (typeName.startsWith("anvil.")) return false;
|
|
15928
|
-
|
|
15929
|
-
const parts = typeName.split(".");
|
|
15930
|
-
if (parts.length < 2) return false;
|
|
15931
|
-
return parts.every((part)=>isValidIdentifierSegment(part));
|
|
16154
|
+
return PACKAGE_QUALIFIED_FORM_NAME_RE.test(typeName);
|
|
15932
16155
|
}
|
|
15933
16156
|
function componentTypeSpec_pushIssue(issues, path, message) {
|
|
15934
16157
|
issues.push({
|
|
@@ -16011,8 +16234,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16011
16234
|
message
|
|
16012
16235
|
});
|
|
16013
16236
|
}
|
|
16237
|
+
function validateNonEmptyString(value, path, issues) {
|
|
16238
|
+
if ("string" != typeof value || 0 === value.length) {
|
|
16239
|
+
formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
|
|
16240
|
+
return false;
|
|
16241
|
+
}
|
|
16242
|
+
return true;
|
|
16243
|
+
}
|
|
16014
16244
|
function validateIdentifier(value, path, issues) {
|
|
16015
|
-
if (
|
|
16245
|
+
if (!validateNonEmptyString(value, path, issues)) return;
|
|
16016
16246
|
if (!formTemplateValidation_IDENTIFIER_RE.test(value)) return void formTemplateValidation_pushIssue(issues, path, "must be a valid identifier");
|
|
16017
16247
|
if (formTemplateValidation_PYTHON_KEYWORDS.has(value)) formTemplateValidation_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
|
|
16018
16248
|
}
|
|
@@ -16037,7 +16267,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16037
16267
|
const bindingPath = `${path}[${index}]`;
|
|
16038
16268
|
if (!isPlainObject(binding)) return void formTemplateValidation_pushIssue(issues, bindingPath, "must be an object");
|
|
16039
16269
|
if ("string" != typeof binding.property || 0 === binding.property.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
|
|
16040
|
-
if ("string" != typeof binding.code
|
|
16270
|
+
if ("string" != typeof binding.code) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a string");
|
|
16041
16271
|
if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) formTemplateValidation_pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
|
|
16042
16272
|
});
|
|
16043
16273
|
}
|
|
@@ -16095,7 +16325,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16095
16325
|
validatePropertiesObject(value.set_layout_properties, `${path}.set_layout_properties`, issues);
|
|
16096
16326
|
if (isPlainObject(value.target)) {
|
|
16097
16327
|
if ("container" !== value.target.type && "slot" !== value.target.type) formTemplateValidation_pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
|
|
16098
|
-
if ("
|
|
16328
|
+
if ("slot" === value.target.type) validateNonEmptyString(value.target.name, `${path}.target.name`, issues);
|
|
16329
|
+
else if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
|
|
16099
16330
|
} else formTemplateValidation_pushIssue(issues, `${path}.target`, "must be an object");
|
|
16100
16331
|
if (void 0 !== value.template) validateComponentNode(value.template, `${path}.template`, issues, seenNames);
|
|
16101
16332
|
}
|
|
@@ -16152,7 +16383,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16152
16383
|
} else {
|
|
16153
16384
|
const componentNames = options.componentNames ?? new Set();
|
|
16154
16385
|
for (const [slotName, slotDef] of Object.entries(value.slots)){
|
|
16155
|
-
|
|
16386
|
+
validateNonEmptyString(slotName, path(`slots.${slotName}.name`), issues);
|
|
16156
16387
|
validateSlotDefinition(slotDef, path(`slots.${slotName}`), issues, componentNames);
|
|
16157
16388
|
}
|
|
16158
16389
|
}
|
|
@@ -16160,6 +16391,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16160
16391
|
}
|
|
16161
16392
|
if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
|
|
16162
16393
|
if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
|
|
16394
|
+
if (void 0 !== value.help_text && "string" != typeof value.help_text) formTemplateValidation_pushIssue(issues, path("help_text"), "must be a string");
|
|
16163
16395
|
if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
|
|
16164
16396
|
if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
|
|
16165
16397
|
} else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
|
|
@@ -16234,7 +16466,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16234
16466
|
"toolbox_item",
|
|
16235
16467
|
"layout_metadata",
|
|
16236
16468
|
"item_type",
|
|
16237
|
-
"slots"
|
|
16469
|
+
"slots",
|
|
16470
|
+
"help_text"
|
|
16238
16471
|
]);
|
|
16239
16472
|
const FRONTMATTER_DISALLOWED_KEYS = new Set([
|
|
16240
16473
|
"container",
|
|
@@ -16353,7 +16586,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16353
16586
|
function validateParsedHtmlTemplate(parsed) {
|
|
16354
16587
|
return validateFormTemplateData(parsed);
|
|
16355
16588
|
}
|
|
16356
|
-
function validateFormTemplateHtml(htmlContent, parseHtml = form_template_parser_namespaceObject.parseSerializedHtml) {
|
|
16589
|
+
function validateFormTemplateHtml(htmlContent, parseHtml = form_template_parser_namespaceObject.parseSerializedHtml, options = {}) {
|
|
16357
16590
|
const { frontmatter, body, bodyStartOffset } = splitHtmlFormTemplateFrontmatter(htmlContent);
|
|
16358
16591
|
if (null !== frontmatter) {
|
|
16359
16592
|
const frontmatterIssues = validateHtmlFrontmatter(frontmatter);
|
|
@@ -16366,7 +16599,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16366
16599
|
let parsed;
|
|
16367
16600
|
try {
|
|
16368
16601
|
parsed = parseHtml(body, {
|
|
16369
|
-
warnings: true
|
|
16602
|
+
warnings: true,
|
|
16603
|
+
...options.formSpecPackageContext ? {
|
|
16604
|
+
formSpecPackageContext: options.formSpecPackageContext
|
|
16605
|
+
} : {}
|
|
16370
16606
|
});
|
|
16371
16607
|
} catch (error) {
|
|
16372
16608
|
return {
|
|
@@ -16397,6 +16633,58 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16397
16633
|
ok: true
|
|
16398
16634
|
};
|
|
16399
16635
|
}
|
|
16636
|
+
const validatePython_requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
|
|
16637
|
+
const DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB = 768;
|
|
16638
|
+
const DEFAULT_PYRIGHT_TIMEOUT_MS = 300000;
|
|
16639
|
+
function parsePositiveIntegerEnv(env, name, defaultValue) {
|
|
16640
|
+
const rawValue = env[name];
|
|
16641
|
+
if (void 0 === rawValue || "" === rawValue) return {
|
|
16642
|
+
value: defaultValue
|
|
16643
|
+
};
|
|
16644
|
+
if (!/^\d+$/.test(rawValue) || Number(rawValue) <= 0 || !Number.isSafeInteger(Number(rawValue))) return {
|
|
16645
|
+
error: `${name} must be a positive integer`
|
|
16646
|
+
};
|
|
16647
|
+
return {
|
|
16648
|
+
value: Number(rawValue)
|
|
16649
|
+
};
|
|
16650
|
+
}
|
|
16651
|
+
function pyrightChildNodeOptions(nodeOptions, maxOldSpaceMb) {
|
|
16652
|
+
const maxOldSpaceFlag = "--max[-_]old[-_]space[-_]size";
|
|
16653
|
+
const withoutEqualsForm = (nodeOptions ?? "").replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}=\\S+`, "g"), "$1");
|
|
16654
|
+
const withoutSeparateForm = withoutEqualsForm.replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}\\s+\\S+`, "g"), "$1");
|
|
16655
|
+
return [
|
|
16656
|
+
withoutSeparateForm.trim(),
|
|
16657
|
+
`--max-old-space-size=${maxOldSpaceMb}`
|
|
16658
|
+
].filter(Boolean).join(" ");
|
|
16659
|
+
}
|
|
16660
|
+
function semanticProcessFailure(message) {
|
|
16661
|
+
return {
|
|
16662
|
+
ok: false,
|
|
16663
|
+
message: "Python semantic validation could not be run",
|
|
16664
|
+
issues: [
|
|
16665
|
+
{
|
|
16666
|
+
path: "root",
|
|
16667
|
+
message,
|
|
16668
|
+
severity: "error"
|
|
16669
|
+
}
|
|
16670
|
+
]
|
|
16671
|
+
};
|
|
16672
|
+
}
|
|
16673
|
+
function isV8HeapOutOfMemory(stderr) {
|
|
16674
|
+
return /(?:JavaScript heap out of memory|Reached heap limit|Ineffective mark-compacts near heap limit)/i.test(stderr);
|
|
16675
|
+
}
|
|
16676
|
+
function resolveBundledAnvilPyright() {
|
|
16677
|
+
try {
|
|
16678
|
+
return {
|
|
16679
|
+
command: process.execPath,
|
|
16680
|
+
args: [
|
|
16681
|
+
validatePython_requireFromHere.resolve("@anvil-works/anvil-pyright")
|
|
16682
|
+
]
|
|
16683
|
+
};
|
|
16684
|
+
} catch {
|
|
16685
|
+
return null;
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16400
16688
|
const PYTHON_VALIDATOR_SCRIPT = String.raw`
|
|
16401
16689
|
import ast
|
|
16402
16690
|
import json
|
|
@@ -16502,6 +16790,36 @@ print(json.dumps({"issues": issues}))
|
|
|
16502
16790
|
}
|
|
16503
16791
|
return null;
|
|
16504
16792
|
}
|
|
16793
|
+
function findAnvilPyright(options = {}) {
|
|
16794
|
+
const env = options.env ?? process.env;
|
|
16795
|
+
const probe = options.probe ?? external_child_process_namespaceObject.spawnSync;
|
|
16796
|
+
const bundled = resolveBundledAnvilPyright();
|
|
16797
|
+
const candidates = [
|
|
16798
|
+
..."string" == typeof env.ANVIL_PYRIGHT && env.ANVIL_PYRIGHT.length > 0 ? [
|
|
16799
|
+
{
|
|
16800
|
+
command: env.ANVIL_PYRIGHT,
|
|
16801
|
+
args: []
|
|
16802
|
+
}
|
|
16803
|
+
] : [],
|
|
16804
|
+
...bundled ? [
|
|
16805
|
+
bundled
|
|
16806
|
+
] : [],
|
|
16807
|
+
{
|
|
16808
|
+
command: "anvil-pyright",
|
|
16809
|
+
args: []
|
|
16810
|
+
}
|
|
16811
|
+
];
|
|
16812
|
+
for (const candidate of candidates){
|
|
16813
|
+
const result = probe(candidate.command, [
|
|
16814
|
+
...candidate.args,
|
|
16815
|
+
"--version"
|
|
16816
|
+
], {
|
|
16817
|
+
encoding: "utf8"
|
|
16818
|
+
});
|
|
16819
|
+
if (!result.error && 0 === result.status) return candidate;
|
|
16820
|
+
}
|
|
16821
|
+
return null;
|
|
16822
|
+
}
|
|
16505
16823
|
function isClientFormPython(filePath) {
|
|
16506
16824
|
if ("__init__.py" === external_path_default().basename(filePath)) {
|
|
16507
16825
|
const formDir = external_path_default().dirname(filePath);
|
|
@@ -16580,6 +16898,126 @@ print(json.dumps({"issues": issues}))
|
|
|
16580
16898
|
ok: true
|
|
16581
16899
|
};
|
|
16582
16900
|
}
|
|
16901
|
+
function pyrightIssuePath(diagnostic) {
|
|
16902
|
+
const line = diagnostic.range?.start?.line;
|
|
16903
|
+
const character = diagnostic.range?.start?.character;
|
|
16904
|
+
if ("number" != typeof line) return "root";
|
|
16905
|
+
if ("number" != typeof character) return `line ${line + 1}`;
|
|
16906
|
+
return `line ${line + 1}, column ${character + 1}`;
|
|
16907
|
+
}
|
|
16908
|
+
function pyrightIssueMessage(diagnostic) {
|
|
16909
|
+
if ("string" == typeof diagnostic.message && diagnostic.message.length > 0) return diagnostic.message;
|
|
16910
|
+
return "Python semantic validation failed";
|
|
16911
|
+
}
|
|
16912
|
+
function pyrightDiagnosticIssue(diagnostic, severity) {
|
|
16913
|
+
const line = diagnostic.range?.start?.line;
|
|
16914
|
+
const column = diagnostic.range?.start?.character;
|
|
16915
|
+
const endLine = diagnostic.range?.end?.line;
|
|
16916
|
+
const endColumn = diagnostic.range?.end?.character;
|
|
16917
|
+
return {
|
|
16918
|
+
...diagnostic.file ? {
|
|
16919
|
+
file: external_path_default().resolve(diagnostic.file)
|
|
16920
|
+
} : {},
|
|
16921
|
+
path: pyrightIssuePath(diagnostic),
|
|
16922
|
+
message: pyrightIssueMessage(diagnostic),
|
|
16923
|
+
...diagnostic.rule ? {
|
|
16924
|
+
code: diagnostic.rule,
|
|
16925
|
+
rule: diagnostic.rule
|
|
16926
|
+
} : {},
|
|
16927
|
+
severity,
|
|
16928
|
+
..."number" == typeof line ? {
|
|
16929
|
+
line: line + 1
|
|
16930
|
+
} : {},
|
|
16931
|
+
..."number" == typeof column ? {
|
|
16932
|
+
column: column + 1
|
|
16933
|
+
} : {},
|
|
16934
|
+
..."number" == typeof endLine ? {
|
|
16935
|
+
endLine: endLine + 1
|
|
16936
|
+
} : {},
|
|
16937
|
+
..."number" == typeof endColumn ? {
|
|
16938
|
+
endColumn: endColumn + 1
|
|
16939
|
+
} : {}
|
|
16940
|
+
};
|
|
16941
|
+
}
|
|
16942
|
+
function validatePythonWithPyright(appRoot, options = {}) {
|
|
16943
|
+
const env = options.env ?? process.env;
|
|
16944
|
+
const maxOldSpace = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_MAX_OLD_SPACE_MB", DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB);
|
|
16945
|
+
if ("error" in maxOldSpace) return semanticProcessFailure(maxOldSpace.error);
|
|
16946
|
+
const timeout = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_TIMEOUT_MS", DEFAULT_PYRIGHT_TIMEOUT_MS);
|
|
16947
|
+
if ("error" in timeout) return semanticProcessFailure(timeout.error);
|
|
16948
|
+
const pyright = void 0 === options.pyright ? findAnvilPyright({
|
|
16949
|
+
env
|
|
16950
|
+
}) : options.pyright;
|
|
16951
|
+
if (!pyright) return {
|
|
16952
|
+
ok: false,
|
|
16953
|
+
message: "Python semantic validation could not be run",
|
|
16954
|
+
issues: [
|
|
16955
|
+
{
|
|
16956
|
+
path: "root",
|
|
16957
|
+
message: "Python semantic validation requires the bundled @anvil-works/anvil-pyright package, anvil-pyright on PATH, or ANVIL_PYRIGHT to be set.",
|
|
16958
|
+
severity: "error"
|
|
16959
|
+
}
|
|
16960
|
+
]
|
|
16961
|
+
};
|
|
16962
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(pyright.command, [
|
|
16963
|
+
...pyright.args,
|
|
16964
|
+
"--outputjson",
|
|
16965
|
+
"--level",
|
|
16966
|
+
"warning",
|
|
16967
|
+
"--project",
|
|
16968
|
+
appRoot,
|
|
16969
|
+
...options.files ?? []
|
|
16970
|
+
], {
|
|
16971
|
+
encoding: "utf8",
|
|
16972
|
+
env: {
|
|
16973
|
+
...env,
|
|
16974
|
+
NODE_OPTIONS: pyrightChildNodeOptions(env.NODE_OPTIONS, maxOldSpace.value)
|
|
16975
|
+
},
|
|
16976
|
+
maxBuffer: 10485760,
|
|
16977
|
+
timeout: timeout.value
|
|
16978
|
+
});
|
|
16979
|
+
if (result.error) {
|
|
16980
|
+
if ("ETIMEDOUT" === result.error.code) return semanticProcessFailure(`anvil-pyright exceeded the ${timeout.value} ms timeout. Increase ANVIL_PYRIGHT_TIMEOUT_MS or validate a smaller target.`);
|
|
16981
|
+
return semanticProcessFailure(result.error.message);
|
|
16982
|
+
}
|
|
16983
|
+
if (isV8HeapOutOfMemory(result.stderr)) return semanticProcessFailure(`anvil-pyright ran out of memory with a ${maxOldSpace.value} MB heap. Increase ANVIL_PYRIGHT_MAX_OLD_SPACE_MB or validate a smaller target.`);
|
|
16984
|
+
if (0 !== result.status && 1 !== result.status) return semanticProcessFailure(result.stderr.trim() || `anvil-pyright exited with status ${result.status}`);
|
|
16985
|
+
let parsed;
|
|
16986
|
+
try {
|
|
16987
|
+
parsed = JSON.parse(result.stdout.trim());
|
|
16988
|
+
} catch (error) {
|
|
16989
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
16990
|
+
return {
|
|
16991
|
+
ok: false,
|
|
16992
|
+
message: "Python semantic validation could not be read",
|
|
16993
|
+
issues: [
|
|
16994
|
+
{
|
|
16995
|
+
path: "root",
|
|
16996
|
+
message: detail || (error instanceof Error ? error.message : String(error)),
|
|
16997
|
+
severity: "error"
|
|
16998
|
+
}
|
|
16999
|
+
]
|
|
17000
|
+
};
|
|
17001
|
+
}
|
|
17002
|
+
const issues = (parsed.generalDiagnostics ?? []).filter((diagnostic)=>"error" === diagnostic.severity).map((diagnostic)=>pyrightDiagnosticIssue(diagnostic, "error"));
|
|
17003
|
+
const warnings = (parsed.generalDiagnostics ?? []).filter((diagnostic)=>"warning" === diagnostic.severity).map((diagnostic)=>pyrightDiagnosticIssue(diagnostic, "warning"));
|
|
17004
|
+
if (issues.length > 0) return {
|
|
17005
|
+
ok: false,
|
|
17006
|
+
message: "Python semantic validation failed",
|
|
17007
|
+
issues,
|
|
17008
|
+
...warnings.length > 0 ? {
|
|
17009
|
+
warnings
|
|
17010
|
+
} : {}
|
|
17011
|
+
};
|
|
17012
|
+
return warnings.length > 0 ? {
|
|
17013
|
+
ok: true,
|
|
17014
|
+
issues: [],
|
|
17015
|
+
warnings
|
|
17016
|
+
} : {
|
|
17017
|
+
ok: true,
|
|
17018
|
+
issues: []
|
|
17019
|
+
};
|
|
17020
|
+
}
|
|
16583
17021
|
const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16584
17022
|
const DB_ACCESS_LEVELS = new Set([
|
|
16585
17023
|
"none",
|
|
@@ -16860,7 +17298,7 @@ print(json.dumps({"issues": issues}))
|
|
|
16860
17298
|
}
|
|
16861
17299
|
function validateScheduledTasks(value, path) {
|
|
16862
17300
|
const issues = [];
|
|
16863
|
-
if (
|
|
17301
|
+
if (null == value) return issues;
|
|
16864
17302
|
if (!Array.isArray(value)) {
|
|
16865
17303
|
validators_pushIssue(issues, path, "must be an array");
|
|
16866
17304
|
return issues;
|
|
@@ -17008,15 +17446,25 @@ print(json.dumps({"issues": issues}))
|
|
|
17008
17446
|
const existingPath = resolveExistingPath(filePath);
|
|
17009
17447
|
if (!existingPath) return null;
|
|
17010
17448
|
let currentDir = external_fs_.statSync(existingPath).isDirectory() ? existingPath : external_path_default().dirname(existingPath);
|
|
17011
|
-
|
|
17449
|
+
const candidates = [];
|
|
17012
17450
|
while(true){
|
|
17013
|
-
if (external_fs_.existsSync(external_path_default().join(currentDir, "anvil.yaml")))
|
|
17451
|
+
if (external_fs_.existsSync(external_path_default().join(currentDir, "anvil.yaml"))) candidates.push(currentDir);
|
|
17014
17452
|
const parentDir = external_path_default().dirname(currentDir);
|
|
17015
|
-
if (parentDir === currentDir)
|
|
17453
|
+
if (parentDir === currentDir) break;
|
|
17016
17454
|
currentDir = parentDir;
|
|
17017
17455
|
}
|
|
17456
|
+
return candidates.find((candidate)=>!candidates.some((outerCandidate)=>{
|
|
17457
|
+
if (candidate === outerCandidate) return false;
|
|
17458
|
+
return [
|
|
17459
|
+
"client_code",
|
|
17460
|
+
"server_code"
|
|
17461
|
+
].some((codeDir)=>{
|
|
17462
|
+
const relativePath = external_path_default().relative(external_path_default().join(outerCandidate, codeDir), candidate);
|
|
17463
|
+
return !external_path_default().isAbsolute(relativePath) && ".." !== relativePath && !relativePath.startsWith(`..${external_path_default().sep}`);
|
|
17464
|
+
});
|
|
17465
|
+
})) ?? null;
|
|
17018
17466
|
}
|
|
17019
|
-
function validatePath(filePath, fileContent) {
|
|
17467
|
+
function validatePath(filePath, fileContent, options = {}) {
|
|
17020
17468
|
const target = inferValidationTarget(filePath);
|
|
17021
17469
|
const existingPath = resolveExistingPath(filePath);
|
|
17022
17470
|
const appRoot = findAnvilAppRoot(filePath);
|
|
@@ -17089,7 +17537,9 @@ print(json.dumps({"issues": issues}))
|
|
|
17089
17537
|
]
|
|
17090
17538
|
};
|
|
17091
17539
|
}
|
|
17092
|
-
const result = validateFormTemplateHtml(fileContent
|
|
17540
|
+
const result = validateFormTemplateHtml(fileContent, void 0, {
|
|
17541
|
+
formSpecPackageContext: options.formSpecPackageContext
|
|
17542
|
+
});
|
|
17093
17543
|
return result.ok ? validPathResult(target, result.warnings) : {
|
|
17094
17544
|
...result,
|
|
17095
17545
|
target
|
|
@@ -17906,7 +18356,7 @@ print(json.dumps({"issues": issues}))
|
|
|
17906
18356
|
]
|
|
17907
18357
|
});
|
|
17908
18358
|
}
|
|
17909
|
-
async function buildFormTemplateData(relativePath, templateInfo, context) {
|
|
18359
|
+
async function buildFormTemplateData(relativePath, templateInfo, context, missingCodeBehavior = "error") {
|
|
17910
18360
|
const { repoPath, stagedOnly = false } = context;
|
|
17911
18361
|
const { kind } = templateInfo;
|
|
17912
18362
|
const loadResult = await loadFormTemplateData(relativePath, templateInfo, context);
|
|
@@ -17920,15 +18370,28 @@ print(json.dumps({"issues": issues}))
|
|
|
17920
18370
|
};
|
|
17921
18371
|
const codeRelativePath = "package" === kind ? relativePath.replace(/form_template\.(yaml|html)$/i, "__init__.py") : relativePath.replace(/\.(yaml|html)$/i, ".py");
|
|
17922
18372
|
const className = extractPythonName(codeRelativePath);
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
18373
|
+
let codeContent;
|
|
18374
|
+
try {
|
|
18375
|
+
codeContent = await readFileContent(repoPath, codeRelativePath, stagedOnly);
|
|
18376
|
+
} catch (error) {
|
|
18377
|
+
if (stagedOnly || !isFileNotFoundError(error) || "error" === missingCodeBehavior) throw error;
|
|
18378
|
+
const missingCodeReason = `Form code file ${codeRelativePath} is not present yet`;
|
|
18379
|
+
const knownForm = context.editorYaml.hasId("forms", className);
|
|
18380
|
+
if ("defer" === missingCodeBehavior || knownForm) {
|
|
18381
|
+
const action = knownForm ? `not saving existing form ${className}` : `deferring form ${className}`;
|
|
18382
|
+
logger_logger.warn(external_chalk_default().yellow(`Form template ${relativePath} has no code file ${codeRelativePath} yet; ${action}.`));
|
|
18383
|
+
return {
|
|
18384
|
+
formData: {},
|
|
18385
|
+
className: "",
|
|
18386
|
+
ignoreResult: {
|
|
18387
|
+
type: "ignore",
|
|
18388
|
+
reason: missingCodeReason
|
|
18389
|
+
}
|
|
18390
|
+
};
|
|
17930
18391
|
}
|
|
17931
|
-
|
|
18392
|
+
codeContent = "";
|
|
18393
|
+
logger_logger.warn(external_chalk_default().yellow(`Form template ${relativePath} has no code file ${codeRelativePath} yet; creating ${className} with empty code.`));
|
|
18394
|
+
}
|
|
17932
18395
|
const formData = (0, form_template_parser_namespaceObject.buildFormTemplateSavePayload)(loadResult.source, {
|
|
17933
18396
|
className,
|
|
17934
18397
|
code: codeContent,
|
|
@@ -17940,6 +18403,9 @@ print(json.dumps({"issues": issues}))
|
|
|
17940
18403
|
ignoreResult: null
|
|
17941
18404
|
};
|
|
17942
18405
|
}
|
|
18406
|
+
function isFileNotFoundError(error) {
|
|
18407
|
+
return "object" == typeof error && null !== error && "ENOENT" === error.code;
|
|
18408
|
+
}
|
|
17943
18409
|
async function handleFormTemplateChange(change, context) {
|
|
17944
18410
|
const { editorYaml } = context;
|
|
17945
18411
|
const { relativePath, changeType } = change;
|
|
@@ -17950,10 +18416,11 @@ print(json.dumps({"issues": issues}))
|
|
|
17950
18416
|
};
|
|
17951
18417
|
const kind = templateInfo.kind;
|
|
17952
18418
|
const format = templateInfo.format;
|
|
18419
|
+
const missingCodeBehavior = "add" === changeType ? "create-new-form-if-unknown" : "defer";
|
|
17953
18420
|
const result = await buildFormTemplateData(relativePath, {
|
|
17954
18421
|
kind,
|
|
17955
18422
|
format
|
|
17956
|
-
}, context);
|
|
18423
|
+
}, context, missingCodeBehavior);
|
|
17957
18424
|
if (result.ignoreResult) return result.ignoreResult;
|
|
17958
18425
|
const { formData, className } = result;
|
|
17959
18426
|
return handleIdBasedOperation({
|
|
@@ -18239,10 +18706,13 @@ print(json.dumps({"issues": issues}))
|
|
|
18239
18706
|
code: error.type,
|
|
18240
18707
|
recoverability: "requires_user_action",
|
|
18241
18708
|
requiresUserAction: true,
|
|
18242
|
-
suggestedAction: "Inspect the repository state and resolve the Git error before retrying.",
|
|
18709
|
+
suggestedAction: isIndexLockFailure(error) ? "Confirm no Git process is running for this repository, then remove the stale .git/index.lock file and retry." : "Inspect the repository state and resolve the Git error before retrying.",
|
|
18243
18710
|
gitCommand: "git_command_failed" === error.type ? error.command : void 0
|
|
18244
18711
|
};
|
|
18245
18712
|
}
|
|
18713
|
+
function isIndexLockFailure(error) {
|
|
18714
|
+
return "git_command_failed" === error.type && error.message.includes("Unable to create") && error.message.includes("index.lock") && error.message.includes("File exists");
|
|
18715
|
+
}
|
|
18246
18716
|
function networkDiagnostic(error) {
|
|
18247
18717
|
return {
|
|
18248
18718
|
code: error.type,
|
|
@@ -18327,6 +18797,15 @@ print(json.dumps({"issues": issues}))
|
|
|
18327
18797
|
this.hasPendingChanges = true;
|
|
18328
18798
|
await this.processSaveBatchWithEvents();
|
|
18329
18799
|
}
|
|
18800
|
+
async flush() {
|
|
18801
|
+
const requireReady = ()=>{
|
|
18802
|
+
if (this.config.isPaused() || this.config.isCleanedUp() || !this.config.isSettlePeriodOver()) throw new Error("Cannot flush saves while watching is paused, closed, or changing branch");
|
|
18803
|
+
};
|
|
18804
|
+
requireReady();
|
|
18805
|
+
await this.forceSave();
|
|
18806
|
+
requireReady();
|
|
18807
|
+
if (this.hasPendingChanges) throw new Error("Saves remain pending");
|
|
18808
|
+
}
|
|
18330
18809
|
cleanup() {
|
|
18331
18810
|
if (this.hasPendingChanges) {
|
|
18332
18811
|
logger_logger.progressEnd("sync");
|
|
@@ -18423,10 +18902,7 @@ print(json.dumps({"issues": issues}))
|
|
|
18423
18902
|
status = await this.config.gitService.getStatus();
|
|
18424
18903
|
} catch (e) {
|
|
18425
18904
|
logger_logger.error(external_chalk_default().red(`Error reading git status: ${errors_getErrorMessage(e)}`));
|
|
18426
|
-
|
|
18427
|
-
skipped: true,
|
|
18428
|
-
reason: "Git status error"
|
|
18429
|
-
};
|
|
18905
|
+
throw e;
|
|
18430
18906
|
}
|
|
18431
18907
|
let allChanges;
|
|
18432
18908
|
if (this.config.stagedOnly) {
|
|
@@ -19272,6 +19748,7 @@ print(json.dumps({"issues": issues}))
|
|
|
19272
19748
|
try {
|
|
19273
19749
|
const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
|
|
19274
19750
|
const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
|
|
19751
|
+
username: this.username,
|
|
19275
19752
|
includeDocs: true
|
|
19276
19753
|
});
|
|
19277
19754
|
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
@@ -19583,6 +20060,12 @@ print(json.dumps({"issues": issues}))
|
|
|
19583
20060
|
if (!this.saveProcessor) return;
|
|
19584
20061
|
return this.saveProcessor.forceSave();
|
|
19585
20062
|
}
|
|
20063
|
+
async flushSaves() {
|
|
20064
|
+
if (!this.saveProcessor || this.isSwitchingForEnvironment) throw new Error("App synchronization is not ready");
|
|
20065
|
+
const branch = this.currentBranch;
|
|
20066
|
+
await this.saveProcessor.flush();
|
|
20067
|
+
if (this.isSwitchingForEnvironment || this.currentBranch !== branch || await this.gitService.getCurrentBranch() !== branch) throw new Error("App branch changed while flushing saves");
|
|
20068
|
+
}
|
|
19586
20069
|
async getSuccessMessage() {
|
|
19587
20070
|
try {
|
|
19588
20071
|
const info = await this.gitService.getCommitInfo();
|
|
@@ -21117,29 +21600,30 @@ print(json.dumps({"issues": issues}))
|
|
|
21117
21600
|
return;
|
|
21118
21601
|
}
|
|
21119
21602
|
}
|
|
21120
|
-
function inspectCredentialHelperCommand(raw) {
|
|
21603
|
+
function inspectCredentialHelperCommand(raw, options = {}) {
|
|
21121
21604
|
const details = {
|
|
21122
21605
|
raw
|
|
21123
21606
|
};
|
|
21124
21607
|
try {
|
|
21125
21608
|
const parsed = parseCredentialHelperCommand(raw);
|
|
21126
21609
|
details.execPath = parsed.execPath;
|
|
21127
|
-
details.helperPath = parsed.helperPath;
|
|
21610
|
+
details.helperPath = parsed.helperPath === GIT_CREDENTIAL_HELPER_LOCATOR && options.gitCommonDir ? external_path_default().join(options.gitCommonDir, GIT_CREDENTIAL_HELPER_FILENAME) : parsed.helperPath;
|
|
21128
21611
|
} catch (e) {
|
|
21129
21612
|
details.parseError = e.message;
|
|
21130
21613
|
return details;
|
|
21131
21614
|
}
|
|
21132
21615
|
if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
|
|
21133
|
-
|
|
21134
|
-
|
|
21616
|
+
const helperPath = details.helperPath;
|
|
21617
|
+
if (helperPath && helperPath !== GIT_CREDENTIAL_HELPER_LOCATOR) {
|
|
21618
|
+
details.helperExists = external_fs_default().existsSync(helperPath);
|
|
21135
21619
|
if (details.helperExists) {
|
|
21136
21620
|
try {
|
|
21137
|
-
const stat = external_fs_default().statSync(
|
|
21621
|
+
const stat = external_fs_default().statSync(helperPath);
|
|
21138
21622
|
details.helperExecutable = (73 & stat.mode) !== 0;
|
|
21139
21623
|
} catch {
|
|
21140
21624
|
details.helperExecutable = false;
|
|
21141
21625
|
}
|
|
21142
|
-
details.modulePath = readHelperModulePath(
|
|
21626
|
+
details.modulePath = readHelperModulePath(helperPath);
|
|
21143
21627
|
if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
|
|
21144
21628
|
}
|
|
21145
21629
|
}
|
|
@@ -21238,7 +21722,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21238
21722
|
child.stdin.end(input);
|
|
21239
21723
|
});
|
|
21240
21724
|
}
|
|
21241
|
-
async function buildAppReport(repoPath, remote, deps) {
|
|
21725
|
+
async function buildAppReport(repoPath, gitCommonDir, remote, deps) {
|
|
21242
21726
|
const appId = remote.appId;
|
|
21243
21727
|
const anvilUrl = remote.anvilUrl;
|
|
21244
21728
|
const url = new URL(anvilUrl);
|
|
@@ -21246,7 +21730,9 @@ print(json.dumps({"issues": issues}))
|
|
|
21246
21730
|
const helperConfigKey = `credential.${credentialScope}.helper`;
|
|
21247
21731
|
const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
|
|
21248
21732
|
const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
|
|
21249
|
-
const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand
|
|
21733
|
+
const helperDetails = nonEmptyHelperValues.map((value)=>inspectCredentialHelperCommand(value, {
|
|
21734
|
+
gitCommonDir
|
|
21735
|
+
}));
|
|
21250
21736
|
const findings = [];
|
|
21251
21737
|
if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
|
|
21252
21738
|
else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
|
|
@@ -21325,12 +21811,18 @@ print(json.dumps({"issues": issues}))
|
|
|
21325
21811
|
"--git-dir"
|
|
21326
21812
|
])).trim();
|
|
21327
21813
|
const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
|
|
21328
|
-
|
|
21814
|
+
const rawGitCommonDir = (await git.raw([
|
|
21815
|
+
"rev-parse",
|
|
21816
|
+
"--git-common-dir"
|
|
21817
|
+
])).trim();
|
|
21818
|
+
const resolvedGitCommonDir = external_path_default().isAbsolute(rawGitCommonDir) ? rawGitCommonDir : external_path_default().resolve(inputPath, rawGitCommonDir);
|
|
21819
|
+
const gitCommonDir = external_fs_default().realpathSync(resolvedGitCommonDir);
|
|
21820
|
+
auth_setRepoContext(repoPath);
|
|
21329
21821
|
const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
|
|
21330
21822
|
const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
|
|
21331
21823
|
const findings = [];
|
|
21332
21824
|
if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
|
|
21333
|
-
const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
|
|
21825
|
+
const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, gitCommonDir, remote, deps)));
|
|
21334
21826
|
const status = getHighestStatus([
|
|
21335
21827
|
...findings.map((item)=>item.status),
|
|
21336
21828
|
...apps.flatMap((app)=>app.findings.map((item)=>item.status))
|
|
@@ -21338,6 +21830,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21338
21830
|
return {
|
|
21339
21831
|
repoPath,
|
|
21340
21832
|
gitDir,
|
|
21833
|
+
gitCommonDir,
|
|
21341
21834
|
remotes,
|
|
21342
21835
|
apps,
|
|
21343
21836
|
findings,
|
|
@@ -21354,6 +21847,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21354
21847
|
lines.push(external_chalk_default().bold("Git auth doctor"));
|
|
21355
21848
|
lines.push(`Repository: ${report.repoPath}`);
|
|
21356
21849
|
lines.push(`Git dir: ${report.gitDir}`);
|
|
21850
|
+
lines.push(`Git common dir: ${report.gitCommonDir}`);
|
|
21357
21851
|
lines.push(`Overall: ${formatStatus(report.status)}`);
|
|
21358
21852
|
lines.push("");
|
|
21359
21853
|
lines.push(external_chalk_default().bold("Detected remotes:"));
|
|
@@ -21449,7 +21943,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21449
21943
|
const repoPath = options.repoPath ?? deps.cwd();
|
|
21450
21944
|
if ("get" !== operation) return null;
|
|
21451
21945
|
if (!request.protocol || !request.host) return null;
|
|
21452
|
-
|
|
21946
|
+
auth_setRepoContext(repoPath);
|
|
21453
21947
|
const appId = parseAppIdFromGitPath(request.path);
|
|
21454
21948
|
if (!appId) return null;
|
|
21455
21949
|
const requestUrl = normalizeAnvilUrl(`${request.protocol}://${request.host}`);
|
|
@@ -21500,7 +21994,6 @@ print(json.dumps({"issues": issues}))
|
|
|
21500
21994
|
}
|
|
21501
21995
|
});
|
|
21502
21996
|
}
|
|
21503
|
-
const DEFAULT_ANVIL_URL = resolveAnvilUrl();
|
|
21504
21997
|
async function syncToLatest(repoPath, appId, options) {
|
|
21505
21998
|
const { anvilUrl, gitUrl = anvilUrl, authToken, currentBranch, username } = options;
|
|
21506
21999
|
try {
|
|
@@ -21529,9 +22022,10 @@ print(json.dumps({"issues": issues}))
|
|
|
21529
22022
|
}
|
|
21530
22023
|
}
|
|
21531
22024
|
async function watch(repoPath, appId, options = {}) {
|
|
21532
|
-
const { anvilUrl = DEFAULT_ANVIL_URL, gitUrl = anvilUrl, stagedOnly = false, username } = options;
|
|
21533
22025
|
repoPath = external_path_default().resolve(repoPath);
|
|
21534
|
-
|
|
22026
|
+
auth_setRepoContext(repoPath);
|
|
22027
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
22028
|
+
const { gitUrl = anvilUrl, stagedOnly = false, username } = options;
|
|
21535
22029
|
const storedEnvPid = options.environment?.envPid ? void 0 : await readEnvironmentPid(repoPath);
|
|
21536
22030
|
const environment = options.environment ?? (storedEnvPid ? {
|
|
21537
22031
|
envPid: storedEnvPid
|
|
@@ -21959,7 +22453,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21959
22453
|
});
|
|
21960
22454
|
logger_logger.progressEnd("verify");
|
|
21961
22455
|
const currentCategory = getSyncStateCategory(freshSession.syncStatus);
|
|
21962
|
-
const currentBranch = freshSession.getBranchName() ||
|
|
22456
|
+
const currentBranch = freshSession.getBranchName() || await getFallbackBranchName(options.repoPath);
|
|
21963
22457
|
if (currentCategory !== previousCategory || currentBranch !== previousBranch) {
|
|
21964
22458
|
logger_logger.warn("Repository status has changed. Re-evaluating...");
|
|
21965
22459
|
return freshSession;
|
|
@@ -21992,7 +22486,7 @@ print(json.dumps({"issues": issues}))
|
|
|
21992
22486
|
}
|
|
21993
22487
|
async function checkSyncStatusAndStart(session, options, deps = defaultSyncStartDeps) {
|
|
21994
22488
|
const syncStatus = session.syncStatus;
|
|
21995
|
-
const branchName = session.getBranchName() ||
|
|
22489
|
+
const branchName = session.getBranchName() || await getFallbackBranchName(options.repoPath);
|
|
21996
22490
|
const hasUncommitted = session.hasUncommittedChanges;
|
|
21997
22491
|
const stateCategory = getSyncStateCategory(syncStatus);
|
|
21998
22492
|
if (syncStatus?.branchMissing) {
|
|
@@ -22312,6 +22806,9 @@ print(json.dumps({"issues": issues}))
|
|
|
22312
22806
|
const suggestedAction = error.suggestedAction;
|
|
22313
22807
|
if ("string" == typeof suggestedAction && suggestedAction.length > 0) logger_logger.warn(suggestedAction);
|
|
22314
22808
|
}
|
|
22809
|
+
async function getFallbackBranchName(repoPath) {
|
|
22810
|
+
return getCurrentOrFallbackBranchName(esm_default(repoPath));
|
|
22811
|
+
}
|
|
22315
22812
|
async function handleWatchCommand(options) {
|
|
22316
22813
|
const invoked = process.argv[2];
|
|
22317
22814
|
if ("sync" === invoked) {
|
|
@@ -22319,7 +22816,7 @@ print(json.dumps({"issues": issues}))
|
|
|
22319
22816
|
process.exit(1);
|
|
22320
22817
|
}
|
|
22321
22818
|
const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, dependencyWatch = true, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
|
|
22322
|
-
|
|
22819
|
+
auth_setRepoContext(repoPath);
|
|
22323
22820
|
try {
|
|
22324
22821
|
const validationResult = await validateAnvilApp(repoPath);
|
|
22325
22822
|
if (validationResult.appName) logger_logger.info(external_chalk_default().green("Anvil app: ") + external_chalk_default().bold(validationResult.appName));
|
|
@@ -22526,6 +23023,7 @@ print(json.dumps({"issues": issues}))
|
|
|
22526
23023
|
logger_logger.success("Logged in as " + external_chalk_default().bold(result.email));
|
|
22527
23024
|
} catch (e) {
|
|
22528
23025
|
logger_logger.error("Error: " + e.message);
|
|
23026
|
+
logErrorCause(e, logger_logger);
|
|
22529
23027
|
process.exit(1);
|
|
22530
23028
|
}
|
|
22531
23029
|
});
|
|
@@ -23104,6 +23602,7 @@ print(json.dumps({"issues": issues}))
|
|
|
23104
23602
|
await runConfigureFlow(version, configure_defaultDeps);
|
|
23105
23603
|
} catch (e) {
|
|
23106
23604
|
logger_logger.error("Error: " + errors_getErrorMessage(e));
|
|
23605
|
+
logErrorCause(e, logger_logger);
|
|
23107
23606
|
process.exit(1);
|
|
23108
23607
|
}
|
|
23109
23608
|
});
|
|
@@ -23119,10 +23618,11 @@ print(json.dumps({"issues": issues}))
|
|
|
23119
23618
|
}
|
|
23120
23619
|
function registerDepsCommand(program) {
|
|
23121
23620
|
const deps = program.command("deps").description("Manage app dependencies");
|
|
23122
|
-
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)=>{
|
|
23621
|
+
deps.command("fetch").description("Fetch dependencies for checkout HEAD into .anvil/deps/").option("--commit <commit>", "Fetch deps at a specific app commit instead of checkout HEAD").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
|
|
23123
23622
|
try {
|
|
23124
23623
|
const projectRoot = process.cwd();
|
|
23125
|
-
|
|
23624
|
+
auth_setRepoContext(projectRoot);
|
|
23625
|
+
const anvilUrl = await resolveAuthAnvilUrl();
|
|
23126
23626
|
const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
23127
23627
|
if (!mainAppId) {
|
|
23128
23628
|
logger_logger.error("No Anvil app found in current directory");
|
|
@@ -23134,6 +23634,7 @@ print(json.dumps({"issues": issues}))
|
|
|
23134
23634
|
commit: options.commit,
|
|
23135
23635
|
includeDocs: !options.excludeDocs
|
|
23136
23636
|
});
|
|
23637
|
+
if (await getDependencyCacheWarning(projectRoot)) logger_logger.warn("Dependency cache does not match local anvil.yaml. Fetch uses committed dependency declarations; sync dependency changes to Anvil before fetching again.");
|
|
23137
23638
|
if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
|
|
23138
23639
|
if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
|
|
23139
23640
|
if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
|
|
@@ -23144,124 +23645,643 @@ print(json.dumps({"issues": issues}))
|
|
|
23144
23645
|
process.exit(1);
|
|
23145
23646
|
}
|
|
23146
23647
|
});
|
|
23147
|
-
deps.command("status").description("Show cached dependencies").action(async ()=>{
|
|
23648
|
+
deps.command("status").description("Show cached dependencies").action(async ()=>{
|
|
23649
|
+
try {
|
|
23650
|
+
const cached = await getCachedPackages(process.cwd());
|
|
23651
|
+
if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
|
|
23652
|
+
logger_logger.info("Cached dependencies:");
|
|
23653
|
+
for (const pkg of cached)logger_logger.info(` - ${pkg}`);
|
|
23654
|
+
} catch (e) {
|
|
23655
|
+
logger_logger.error(e.message);
|
|
23656
|
+
process.exit(1);
|
|
23657
|
+
}
|
|
23658
|
+
});
|
|
23659
|
+
deps.command("clear").description("Clear cached dependencies").action(async ()=>{
|
|
23660
|
+
try {
|
|
23661
|
+
await clearDepsCache(process.cwd());
|
|
23662
|
+
logger_logger.success("Cleared .anvil/deps/");
|
|
23663
|
+
} catch (e) {
|
|
23664
|
+
logger_logger.error(e.message);
|
|
23665
|
+
process.exit(1);
|
|
23666
|
+
}
|
|
23667
|
+
});
|
|
23668
|
+
}
|
|
23669
|
+
async function applyDatabaseSchema(appId, options = {}) {
|
|
23670
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
23671
|
+
auth_setRepoContext(projectRoot);
|
|
23672
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
23673
|
+
const appYaml = external_js_yaml_default().load(await external_fs_.promises.readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf8"));
|
|
23674
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
23675
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db/infer-from-env/apply-schema`, {
|
|
23676
|
+
method: "PUT",
|
|
23677
|
+
headers: {
|
|
23678
|
+
Authorization: `Bearer ${token}`,
|
|
23679
|
+
"Content-Type": "application/json"
|
|
23680
|
+
},
|
|
23681
|
+
body: JSON.stringify({
|
|
23682
|
+
schema: appYaml?.db_schema ?? {},
|
|
23683
|
+
table_id_hints: appYaml?.table_id_hints
|
|
23684
|
+
})
|
|
23685
|
+
});
|
|
23686
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to apply database schema", response.status, await response.text()));
|
|
23687
|
+
return await response.json();
|
|
23688
|
+
}
|
|
23689
|
+
async function confirmSchemaApplication(force = false) {
|
|
23690
|
+
if (force) return true;
|
|
23691
|
+
assertCanPrompt("Applying database schema", "Pass --force to apply without prompting.");
|
|
23692
|
+
return logger_logger.confirm("Apply the app environment's schema to its database? This may delete tables, columns, or data.", true);
|
|
23693
|
+
}
|
|
23694
|
+
function registerDbCommand(program) {
|
|
23695
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
23696
|
+
const db = program.command("db").description("Manage app databases");
|
|
23697
|
+
db.command("apply-schema").description("Apply the app schema to the database for the current environment").option("-f, --force", "Apply the schema without asking for confirmation").action(async (options)=>{
|
|
23698
|
+
try {
|
|
23699
|
+
const projectRoot = process.cwd();
|
|
23700
|
+
auth_setRepoContext(projectRoot);
|
|
23701
|
+
const anvilUrl = await resolveAuthAnvilUrl();
|
|
23702
|
+
const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
23703
|
+
if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
|
|
23704
|
+
if (!await confirmSchemaApplication(options.force)) return void logger_logger.info("Schema application cancelled.");
|
|
23705
|
+
const response = await applyDatabaseSchema(appId, {
|
|
23706
|
+
anvilUrl,
|
|
23707
|
+
projectRoot
|
|
23708
|
+
});
|
|
23709
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23710
|
+
data: response
|
|
23711
|
+
});
|
|
23712
|
+
else logger_logger.success("Database schema applied.");
|
|
23713
|
+
} catch (error) {
|
|
23714
|
+
const message = errors_getErrorMessage(error);
|
|
23715
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23716
|
+
error: message
|
|
23717
|
+
});
|
|
23718
|
+
else logger_logger.error(message);
|
|
23719
|
+
process.exit(1);
|
|
23720
|
+
}
|
|
23721
|
+
});
|
|
23722
|
+
}
|
|
23723
|
+
async function ensureEnvironmentUrl(appId, envPid, options = {}) {
|
|
23724
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
23725
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
23726
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-url`, {
|
|
23727
|
+
method: "POST",
|
|
23728
|
+
headers: {
|
|
23729
|
+
Authorization: `Bearer ${token}`
|
|
23730
|
+
}
|
|
23731
|
+
});
|
|
23732
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to get environment URL", response.status, await response.text()));
|
|
23733
|
+
const { url, expires_at } = await response.json();
|
|
23734
|
+
return {
|
|
23735
|
+
url,
|
|
23736
|
+
expires_at
|
|
23737
|
+
};
|
|
23738
|
+
}
|
|
23739
|
+
async function getTemporaryUplinkKey(appId, envPid, options = {}) {
|
|
23740
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
23741
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
23742
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-uplink-key`, {
|
|
23743
|
+
method: "POST",
|
|
23744
|
+
headers: {
|
|
23745
|
+
Authorization: `Bearer ${token}`
|
|
23746
|
+
}
|
|
23747
|
+
});
|
|
23748
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to get temporary uplink key", response.status, await response.text()));
|
|
23749
|
+
const { key, url, expires_at } = await response.json();
|
|
23750
|
+
return {
|
|
23751
|
+
key,
|
|
23752
|
+
url,
|
|
23753
|
+
expires_at
|
|
23754
|
+
};
|
|
23755
|
+
}
|
|
23756
|
+
async function resolveEnvironmentContext() {
|
|
23757
|
+
const projectRoot = process.cwd();
|
|
23758
|
+
const envPid = await readEnvironmentPid(projectRoot);
|
|
23759
|
+
if (!envPid) throw new Error("No app environment associated with the current directory.");
|
|
23760
|
+
auth_setRepoContext(projectRoot);
|
|
23761
|
+
const anvilUrl = await resolveAuthAnvilUrl();
|
|
23762
|
+
const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
23763
|
+
if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
|
|
23764
|
+
return {
|
|
23765
|
+
envPid,
|
|
23766
|
+
anvilUrl,
|
|
23767
|
+
appId
|
|
23768
|
+
};
|
|
23769
|
+
}
|
|
23770
|
+
function registerEnvCommand(program) {
|
|
23771
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
23772
|
+
const env = program.command("env").description("Manage app environments");
|
|
23773
|
+
env.command("url").description("Get a temporary private app URL for the current environment").action(async ()=>{
|
|
23148
23774
|
try {
|
|
23149
|
-
const
|
|
23150
|
-
|
|
23151
|
-
|
|
23152
|
-
|
|
23153
|
-
|
|
23154
|
-
|
|
23775
|
+
const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
|
|
23776
|
+
const { url, expires_at } = await ensureEnvironmentUrl(appId, envPid, {
|
|
23777
|
+
anvilUrl
|
|
23778
|
+
});
|
|
23779
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23780
|
+
data: {
|
|
23781
|
+
url,
|
|
23782
|
+
expires_at
|
|
23783
|
+
}
|
|
23784
|
+
});
|
|
23785
|
+
else logger_logger.info(`URL: ${url}\nExpires at: ${expires_at}`);
|
|
23786
|
+
} catch (error) {
|
|
23787
|
+
const message = errors_getErrorMessage(error);
|
|
23788
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23789
|
+
error: message
|
|
23790
|
+
});
|
|
23791
|
+
else logger_logger.error(message);
|
|
23155
23792
|
process.exit(1);
|
|
23156
23793
|
}
|
|
23157
23794
|
});
|
|
23158
|
-
|
|
23795
|
+
env.command("uplink").description("Get a temporary uplink key for the current environment").action(async ()=>{
|
|
23159
23796
|
try {
|
|
23160
|
-
await
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23797
|
+
const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
|
|
23798
|
+
const { key, url, expires_at } = await getTemporaryUplinkKey(appId, envPid, {
|
|
23799
|
+
anvilUrl
|
|
23800
|
+
});
|
|
23801
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23802
|
+
data: {
|
|
23803
|
+
key,
|
|
23804
|
+
url,
|
|
23805
|
+
expires_at
|
|
23806
|
+
}
|
|
23807
|
+
});
|
|
23808
|
+
else logger_logger.info(`Key: ${key}\nURL: ${url}\nExpires at: ${expires_at}`);
|
|
23809
|
+
} catch (error) {
|
|
23810
|
+
const message = errors_getErrorMessage(error);
|
|
23811
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23812
|
+
error: message
|
|
23813
|
+
});
|
|
23814
|
+
else logger_logger.error(message);
|
|
23164
23815
|
process.exit(1);
|
|
23165
23816
|
}
|
|
23166
23817
|
});
|
|
23167
23818
|
}
|
|
23168
|
-
const
|
|
23169
|
-
function
|
|
23170
|
-
if (
|
|
23171
|
-
|
|
23172
|
-
|
|
23173
|
-
|
|
23174
|
-
|
|
23175
|
-
|
|
23819
|
+
const external_events_namespaceObject = require("events");
|
|
23820
|
+
function replError(error) {
|
|
23821
|
+
if ("string" == typeof error) return {
|
|
23822
|
+
message: error
|
|
23823
|
+
};
|
|
23824
|
+
if (error && "object" == typeof error) {
|
|
23825
|
+
const value = error;
|
|
23826
|
+
return {
|
|
23827
|
+
message: String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value)),
|
|
23828
|
+
..."string" == typeof value.type ? {
|
|
23829
|
+
type: value.type
|
|
23830
|
+
} : {},
|
|
23831
|
+
...Array.isArray(value.trace) ? {
|
|
23832
|
+
trace: value.trace.filter((frame)=>Array.isArray(frame) && "string" == typeof frame[0] && "number" == typeof frame[1])
|
|
23833
|
+
} : {}
|
|
23834
|
+
};
|
|
23835
|
+
}
|
|
23836
|
+
return {
|
|
23837
|
+
message: String(error)
|
|
23838
|
+
};
|
|
23176
23839
|
}
|
|
23177
|
-
|
|
23840
|
+
class ReplServerError extends Error {
|
|
23841
|
+
detail;
|
|
23842
|
+
constructor(detail){
|
|
23843
|
+
super(detail.message), this.detail = detail;
|
|
23844
|
+
}
|
|
23845
|
+
}
|
|
23846
|
+
async function runRepl(appId, envPid, options) {
|
|
23847
|
+
const interrupted = ()=>new Error("REPL interrupted.");
|
|
23848
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
23178
23849
|
const token = await auth_getValidAuthToken(anvilUrl);
|
|
23179
|
-
const
|
|
23850
|
+
const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/ws`);
|
|
23851
|
+
url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
|
|
23852
|
+
if (options.signal?.aborted) throw interrupted();
|
|
23853
|
+
const ws = new (external_ws_default())(url, {
|
|
23180
23854
|
headers: {
|
|
23181
23855
|
Authorization: `Bearer ${token}`
|
|
23182
|
-
}
|
|
23856
|
+
},
|
|
23857
|
+
handshakeTimeout: 30000
|
|
23183
23858
|
});
|
|
23184
|
-
|
|
23185
|
-
|
|
23186
|
-
|
|
23859
|
+
const controller = new AbortController();
|
|
23860
|
+
let repl;
|
|
23861
|
+
let nextId = 1;
|
|
23862
|
+
let output = "";
|
|
23863
|
+
let keepalive;
|
|
23864
|
+
let pongTimeout;
|
|
23865
|
+
let closeTimeout;
|
|
23866
|
+
const startupTimeout = setTimeout(()=>{
|
|
23867
|
+
controller.abort(new Error("Timed out waiting for the REPL to start."));
|
|
23868
|
+
}, 30000);
|
|
23869
|
+
const onAbort = ()=>controller.abort(interrupted());
|
|
23870
|
+
const markResponsive = ()=>{
|
|
23871
|
+
clearTimeout(pongTimeout);
|
|
23872
|
+
pongTimeout = void 0;
|
|
23873
|
+
};
|
|
23874
|
+
options.signal?.addEventListener("abort", onAbort, {
|
|
23875
|
+
once: true
|
|
23876
|
+
});
|
|
23877
|
+
ws.on("pong", markResponsive);
|
|
23878
|
+
ws.on("unexpected-response", (request, response)=>{
|
|
23879
|
+
response.resume();
|
|
23880
|
+
controller.abort(new Error(`REPL websocket handshake failed (${response.statusCode} ${response.statusMessage}).`));
|
|
23881
|
+
request.destroy();
|
|
23882
|
+
});
|
|
23883
|
+
ws.on("error", (error)=>controller.abort(new Error(`REPL websocket connection failed: ${error.message}`)));
|
|
23884
|
+
ws.on("close", (code, reason)=>{
|
|
23885
|
+
clearTimeout(closeTimeout);
|
|
23886
|
+
controller.abort(new Error(`REPL websocket disconnected unexpectedly (code ${code}${reason.length ? `: ${reason}` : ""}).`));
|
|
23887
|
+
});
|
|
23888
|
+
const messages = (0, external_events_namespaceObject.on)(ws, "message", {
|
|
23889
|
+
signal: controller.signal
|
|
23890
|
+
});
|
|
23891
|
+
function send(message) {
|
|
23892
|
+
const id = nextId++;
|
|
23893
|
+
ws.send(JSON.stringify({
|
|
23894
|
+
...message,
|
|
23895
|
+
id
|
|
23896
|
+
}), (error)=>{
|
|
23897
|
+
if (error) controller.abort(new Error(`REPL websocket send failed: ${error.message}`));
|
|
23898
|
+
});
|
|
23899
|
+
return id;
|
|
23900
|
+
}
|
|
23901
|
+
async function readUntil(matches) {
|
|
23902
|
+
while(true){
|
|
23903
|
+
const { value, done } = await messages.next();
|
|
23904
|
+
options.signal?.throwIfAborted();
|
|
23905
|
+
if (done) throw new Error("REPL websocket message stream ended unexpectedly.");
|
|
23906
|
+
markResponsive();
|
|
23907
|
+
let message;
|
|
23908
|
+
try {
|
|
23909
|
+
message = JSON.parse(value[0].toString());
|
|
23910
|
+
if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
|
|
23911
|
+
} catch {
|
|
23912
|
+
throw new Error("Invalid REPL websocket message.");
|
|
23913
|
+
}
|
|
23914
|
+
if ("REPL_UPDATE" === message.event) {
|
|
23915
|
+
if (!repl || message.repl !== repl) continue;
|
|
23916
|
+
if ("string" == typeof message.output) {
|
|
23917
|
+
output += message.output;
|
|
23918
|
+
options.onOutput?.(message.output);
|
|
23919
|
+
options.signal?.throwIfAborted();
|
|
23920
|
+
}
|
|
23921
|
+
}
|
|
23922
|
+
if (message.error) throw new ReplServerError(replError(message.error));
|
|
23923
|
+
if ("REPL_UPDATE" === message.event && message.terminated) throw new Error("REPL terminated unexpectedly.");
|
|
23924
|
+
if (matches(message)) return message;
|
|
23925
|
+
}
|
|
23187
23926
|
}
|
|
23188
|
-
|
|
23189
|
-
|
|
23190
|
-
|
|
23191
|
-
|
|
23192
|
-
|
|
23193
|
-
|
|
23927
|
+
try {
|
|
23928
|
+
await (0, external_events_namespaceObject.once)(ws, "open", {
|
|
23929
|
+
signal: controller.signal
|
|
23930
|
+
});
|
|
23931
|
+
const launchId = send({
|
|
23932
|
+
cmd: "LAUNCH_REPL"
|
|
23933
|
+
});
|
|
23934
|
+
const launched = await readUntil((message)=>message.id === launchId);
|
|
23935
|
+
if ("string" != typeof launched.repl || !launched.repl) throw new Error("Invalid REPL launch response: missing REPL ID.");
|
|
23936
|
+
repl = launched.repl;
|
|
23937
|
+
keepalive = setInterval(()=>{
|
|
23938
|
+
try {
|
|
23939
|
+
send({
|
|
23940
|
+
cmd: "REPL_KEEPALIVE",
|
|
23941
|
+
repl
|
|
23942
|
+
});
|
|
23943
|
+
pongTimeout = setTimeout(()=>controller.abort(new Error("REPL websocket heartbeat timed out.")), 10000);
|
|
23944
|
+
ws.ping();
|
|
23945
|
+
} catch (error) {
|
|
23946
|
+
controller.abort(error);
|
|
23947
|
+
}
|
|
23948
|
+
}, 20000);
|
|
23949
|
+
const isReady = (message)=>"REPL_UPDATE" === message.event && true === message.ready;
|
|
23950
|
+
await readUntil(isReady);
|
|
23951
|
+
clearTimeout(startupTimeout);
|
|
23952
|
+
send({
|
|
23953
|
+
cmd: "REPL_COMMAND",
|
|
23954
|
+
repl,
|
|
23955
|
+
command: options.code
|
|
23956
|
+
});
|
|
23957
|
+
await readUntil(isReady);
|
|
23958
|
+
return {
|
|
23959
|
+
output
|
|
23960
|
+
};
|
|
23961
|
+
} catch (error) {
|
|
23962
|
+
if (error instanceof ReplServerError) return {
|
|
23963
|
+
output,
|
|
23964
|
+
error: error.detail
|
|
23965
|
+
};
|
|
23966
|
+
const failure = controller.signal.aborted ? controller.signal.reason : error;
|
|
23967
|
+
throw failure instanceof Error ? failure : new Error(String(failure));
|
|
23968
|
+
} finally{
|
|
23969
|
+
clearTimeout(startupTimeout);
|
|
23970
|
+
clearInterval(keepalive);
|
|
23971
|
+
clearTimeout(pongTimeout);
|
|
23972
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
23973
|
+
controller.abort();
|
|
23974
|
+
await messages.return?.();
|
|
23975
|
+
if (ws.readyState === external_ws_default().OPEN) {
|
|
23976
|
+
if (repl) ws.send(JSON.stringify({
|
|
23977
|
+
id: nextId++,
|
|
23978
|
+
cmd: "TERMINATE_REPL",
|
|
23979
|
+
repl
|
|
23980
|
+
}), ()=>{});
|
|
23981
|
+
ws.close();
|
|
23982
|
+
} else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
|
|
23983
|
+
if (ws.readyState !== external_ws_default().CLOSED) {
|
|
23984
|
+
closeTimeout = setTimeout(()=>ws.terminate(), 1000);
|
|
23985
|
+
closeTimeout.unref();
|
|
23986
|
+
}
|
|
23987
|
+
}
|
|
23988
|
+
}
|
|
23989
|
+
function writeJsonReplOutput(output) {
|
|
23990
|
+
process.stdout.write(JSON.stringify({
|
|
23991
|
+
type: "repl_output",
|
|
23992
|
+
output,
|
|
23993
|
+
timestamp: new Date().toISOString()
|
|
23994
|
+
}) + "\n");
|
|
23995
|
+
}
|
|
23996
|
+
function registerReplCommand(program) {
|
|
23997
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
23998
|
+
program.command("repl <code>").description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
|
|
23999
|
+
Examples:
|
|
24000
|
+
anvil repl '1 + 2'
|
|
24001
|
+
anvil --json repl '1 + 2'
|
|
24002
|
+
anvil repl - < script.py
|
|
24003
|
+
`).action(async (code)=>{
|
|
24004
|
+
const controller = new AbortController();
|
|
24005
|
+
const onInterrupt = ()=>controller.abort();
|
|
24006
|
+
try {
|
|
24007
|
+
const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
|
|
24008
|
+
if ("-" === code) {
|
|
24009
|
+
const chunks = [];
|
|
24010
|
+
for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
24011
|
+
code = Buffer.concat(chunks).toString("utf8");
|
|
24012
|
+
}
|
|
24013
|
+
process.on("SIGINT", onInterrupt);
|
|
24014
|
+
const response = await runRepl(appId, envPid, {
|
|
24015
|
+
code,
|
|
24016
|
+
anvilUrl,
|
|
24017
|
+
signal: controller.signal,
|
|
24018
|
+
onOutput: getGlobalOutputConfig().jsonMode ? writeJsonReplOutput : (output)=>{
|
|
24019
|
+
process.stdout.write(output);
|
|
24020
|
+
}
|
|
24021
|
+
});
|
|
24022
|
+
const error = response.error;
|
|
24023
|
+
const message = error ? `${error.type ? `${error.type}: ` : ""}${error.message}` : void 0;
|
|
24024
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(!error, {
|
|
24025
|
+
data: response,
|
|
24026
|
+
error: message
|
|
24027
|
+
});
|
|
24028
|
+
else if (error) {
|
|
24029
|
+
process.stderr.write(`${message}\n`);
|
|
24030
|
+
for (const [file, line] of error.trace ?? [])process.stderr.write(` at ${file}:${line}\n`);
|
|
24031
|
+
}
|
|
24032
|
+
if (error) process.exitCode = 1;
|
|
24033
|
+
} catch (error) {
|
|
24034
|
+
const message = errors_getErrorMessage(error);
|
|
24035
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24036
|
+
error: message
|
|
24037
|
+
});
|
|
24038
|
+
else logger_logger.error(message);
|
|
24039
|
+
process.exitCode = controller.signal.aborted ? 130 : 1;
|
|
24040
|
+
} finally{
|
|
24041
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
24042
|
+
}
|
|
23194
24043
|
});
|
|
23195
|
-
return {
|
|
23196
|
-
tables: allTables
|
|
23197
|
-
};
|
|
23198
24044
|
}
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
await
|
|
23202
|
-
|
|
24045
|
+
const external_yaml_namespaceObject = require("yaml");
|
|
24046
|
+
async function encryptSecret(appId, secretName, options = {}) {
|
|
24047
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
24048
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
24049
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/encrypt`, {
|
|
24050
|
+
method: "POST",
|
|
24051
|
+
headers: {
|
|
24052
|
+
Authorization: `Bearer ${token}`,
|
|
24053
|
+
"Content-Type": "application/json"
|
|
24054
|
+
},
|
|
24055
|
+
body: JSON.stringify({
|
|
24056
|
+
name: secretName,
|
|
24057
|
+
...void 0 === options.secret ? {} : {
|
|
24058
|
+
secret: options.secret
|
|
24059
|
+
}
|
|
24060
|
+
})
|
|
23203
24061
|
});
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
async function readTableMappings(projectRoot) {
|
|
23207
|
-
const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
|
|
23208
|
-
try {
|
|
23209
|
-
const content = await promises_default().readFile(filePath, "utf-8");
|
|
23210
|
-
return JSON.parse(content);
|
|
23211
|
-
} catch {
|
|
23212
|
-
return null;
|
|
23213
|
-
}
|
|
24062
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to set secret", response.status, await response.text()));
|
|
24063
|
+
return await response.json();
|
|
23214
24064
|
}
|
|
23215
|
-
async function
|
|
23216
|
-
const
|
|
23217
|
-
await
|
|
23218
|
-
|
|
24065
|
+
async function generateSecretKey(appId, keyName, options = {}) {
|
|
24066
|
+
const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
|
|
24067
|
+
const token = await auth_getValidAuthToken(anvilUrl);
|
|
24068
|
+
const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/generate-key`, {
|
|
24069
|
+
method: "POST",
|
|
24070
|
+
headers: {
|
|
24071
|
+
Authorization: `Bearer ${token}`,
|
|
24072
|
+
"Content-Type": "application/json"
|
|
24073
|
+
},
|
|
24074
|
+
body: JSON.stringify({
|
|
24075
|
+
name: keyName
|
|
24076
|
+
})
|
|
23219
24077
|
});
|
|
24078
|
+
if (!response.ok) throw new Error(formatHttpError("Failed to generate key", response.status, await response.text()));
|
|
24079
|
+
return await response.json();
|
|
24080
|
+
}
|
|
24081
|
+
function secret_anvilYamlPath(projectRoot) {
|
|
24082
|
+
return external_path_default().join(projectRoot, "anvil.yaml");
|
|
24083
|
+
}
|
|
24084
|
+
function checkSecretTypeInDoc(doc, secretName, type) {
|
|
24085
|
+
const existingType = doc.getIn([
|
|
24086
|
+
"secrets",
|
|
24087
|
+
secretName,
|
|
24088
|
+
"type"
|
|
24089
|
+
]);
|
|
24090
|
+
if (void 0 !== existingType && existingType !== type) throw new Error(`'${secretName}' is a ${existingType} in anvil.yaml, not a ${type}`);
|
|
24091
|
+
}
|
|
24092
|
+
function secretExistsInDoc(doc, secretName, appId) {
|
|
24093
|
+
return doc.hasIn([
|
|
24094
|
+
"secrets",
|
|
24095
|
+
secretName,
|
|
24096
|
+
"value",
|
|
24097
|
+
appId
|
|
24098
|
+
]);
|
|
24099
|
+
}
|
|
24100
|
+
async function findSecretInAnvilYaml(projectRoot, secretName, appId) {
|
|
24101
|
+
const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(secret_anvilYamlPath(projectRoot), "utf8"));
|
|
24102
|
+
if (!doc.hasIn([
|
|
24103
|
+
"secrets",
|
|
24104
|
+
secretName
|
|
24105
|
+
])) return;
|
|
24106
|
+
return {
|
|
24107
|
+
type: doc.getIn([
|
|
24108
|
+
"secrets",
|
|
24109
|
+
secretName,
|
|
24110
|
+
"type"
|
|
24111
|
+
]),
|
|
24112
|
+
existsForApp: secretExistsInDoc(doc, secretName, appId)
|
|
24113
|
+
};
|
|
24114
|
+
}
|
|
24115
|
+
async function writeSecretToAnvilYaml(projectRoot, secretName, options) {
|
|
24116
|
+
const { appId, encrypted, overwrite, type } = options;
|
|
24117
|
+
const filePath = secret_anvilYamlPath(projectRoot);
|
|
24118
|
+
const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(filePath, "utf8"));
|
|
24119
|
+
checkSecretTypeInDoc(doc, secretName, type);
|
|
24120
|
+
if (secretExistsInDoc(doc, secretName, appId)) {
|
|
24121
|
+
if (!overwrite) throw new Error(`'${secretName}' already exists for app ${appId} in anvil.yaml`);
|
|
24122
|
+
if (!doc.hasIn([
|
|
24123
|
+
"secrets",
|
|
24124
|
+
secretName,
|
|
24125
|
+
"value",
|
|
24126
|
+
appId,
|
|
24127
|
+
"all"
|
|
24128
|
+
])) throw new Error(`'${secretName}' for app ${appId} in anvil.yaml cannot be overridden`);
|
|
24129
|
+
}
|
|
24130
|
+
if (doc.hasIn([
|
|
24131
|
+
"secrets",
|
|
24132
|
+
secretName
|
|
24133
|
+
])) {
|
|
24134
|
+
if (!(0, external_yaml_namespaceObject.isMap)(doc.getIn([
|
|
24135
|
+
"secrets",
|
|
24136
|
+
secretName,
|
|
24137
|
+
"value"
|
|
24138
|
+
]))) doc.setIn([
|
|
24139
|
+
"secrets",
|
|
24140
|
+
secretName,
|
|
24141
|
+
"value"
|
|
24142
|
+
], doc.createNode({}));
|
|
24143
|
+
} else doc.setIn([
|
|
24144
|
+
"secrets",
|
|
24145
|
+
secretName
|
|
24146
|
+
], doc.createNode({
|
|
24147
|
+
type,
|
|
24148
|
+
value: {}
|
|
24149
|
+
}));
|
|
24150
|
+
const value = new external_yaml_namespaceObject.Scalar(encrypted);
|
|
24151
|
+
value.type = external_yaml_namespaceObject.Scalar.QUOTE_DOUBLE;
|
|
24152
|
+
if ((0, external_yaml_namespaceObject.isMap)(doc.getIn([
|
|
24153
|
+
"secrets",
|
|
24154
|
+
secretName,
|
|
24155
|
+
"value",
|
|
24156
|
+
appId
|
|
24157
|
+
]))) doc.setIn([
|
|
24158
|
+
"secrets",
|
|
24159
|
+
secretName,
|
|
24160
|
+
"value",
|
|
24161
|
+
appId,
|
|
24162
|
+
"all"
|
|
24163
|
+
], value);
|
|
24164
|
+
else doc.setIn([
|
|
24165
|
+
"secrets",
|
|
24166
|
+
secretName,
|
|
24167
|
+
"value",
|
|
24168
|
+
appId
|
|
24169
|
+
], doc.createNode({
|
|
24170
|
+
all: value
|
|
24171
|
+
}, {
|
|
24172
|
+
flow: true
|
|
24173
|
+
}));
|
|
24174
|
+
await external_fs_.promises.writeFile(filePath, doc.toString(), "utf8");
|
|
23220
24175
|
}
|
|
23221
|
-
function
|
|
23222
|
-
|
|
23223
|
-
|
|
24176
|
+
function registerSecretCommand(program) {
|
|
24177
|
+
if ("1" !== process.env.ANVIL_AGENT_HOST) return;
|
|
24178
|
+
const secret = program.command("secret").description("Manage app secrets");
|
|
24179
|
+
secret.command("set").description("Set an app secret. The value is read from stdin, or prompted for if stdin is a terminal.").argument("<name>", "Name of the secret").option("--generate", "Have the server autogenerate the secret value instead of reading it").option("-f, --force", "Overwrite the secret if it is already set for this app").action(async (name, options)=>{
|
|
23224
24180
|
try {
|
|
23225
24181
|
const projectRoot = process.cwd();
|
|
23226
|
-
|
|
23227
|
-
const
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
|
|
23231
|
-
|
|
23232
|
-
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
23236
|
-
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
logger_logger.
|
|
24182
|
+
auth_setRepoContext(projectRoot);
|
|
24183
|
+
const anvilUrl = await resolveAuthAnvilUrl();
|
|
24184
|
+
const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
24185
|
+
if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
|
|
24186
|
+
const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
|
|
24187
|
+
appId,
|
|
24188
|
+
type: "secret",
|
|
24189
|
+
force: options.force
|
|
24190
|
+
});
|
|
24191
|
+
const value = options.generate ? void 0 : await readSecretValue(name);
|
|
24192
|
+
const response = await encryptSecret(appId, name, {
|
|
24193
|
+
anvilUrl,
|
|
24194
|
+
secret: value
|
|
24195
|
+
});
|
|
24196
|
+
await writeSecretToAnvilYaml(projectRoot, name, {
|
|
24197
|
+
appId,
|
|
24198
|
+
encrypted: response.encrypted,
|
|
24199
|
+
overwrite,
|
|
24200
|
+
type: "secret"
|
|
24201
|
+
});
|
|
24202
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
24203
|
+
data: {
|
|
24204
|
+
encrypted: response.encrypted
|
|
24205
|
+
}
|
|
24206
|
+
});
|
|
24207
|
+
else logger_logger.info(`Secret '${name}' written to anvil.yaml`);
|
|
24208
|
+
} catch (error) {
|
|
24209
|
+
const message = errors_getErrorMessage(error);
|
|
24210
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24211
|
+
error: message
|
|
24212
|
+
});
|
|
24213
|
+
else logger_logger.error(message);
|
|
23252
24214
|
process.exit(1);
|
|
23253
24215
|
}
|
|
23254
24216
|
});
|
|
23255
|
-
|
|
24217
|
+
secret.command("generate-key").description("Generate an app encryption key. The key is always generated by the server, and its value is never shown.").argument("<name>", "Name of the key").option("-f, --force", "Replace the key if it is already set for this app").action(async (name, options)=>{
|
|
23256
24218
|
try {
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
24219
|
+
const projectRoot = process.cwd();
|
|
24220
|
+
auth_setRepoContext(projectRoot);
|
|
24221
|
+
const anvilUrl = await resolveAuthAnvilUrl();
|
|
24222
|
+
const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
|
|
24223
|
+
if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
|
|
24224
|
+
const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
|
|
24225
|
+
appId,
|
|
24226
|
+
type: "key",
|
|
24227
|
+
force: options.force
|
|
24228
|
+
});
|
|
24229
|
+
const response = await generateSecretKey(appId, name, {
|
|
24230
|
+
anvilUrl
|
|
24231
|
+
});
|
|
24232
|
+
await writeSecretToAnvilYaml(projectRoot, name, {
|
|
24233
|
+
appId,
|
|
24234
|
+
encrypted: response.encrypted,
|
|
24235
|
+
overwrite,
|
|
24236
|
+
type: "key"
|
|
24237
|
+
});
|
|
24238
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
24239
|
+
data: {
|
|
24240
|
+
encrypted: response.encrypted
|
|
24241
|
+
}
|
|
24242
|
+
});
|
|
24243
|
+
else logger_logger.info(`Key '${name}' written to anvil.yaml`);
|
|
24244
|
+
} catch (error) {
|
|
24245
|
+
const message = errors_getErrorMessage(error);
|
|
24246
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
24247
|
+
error: message
|
|
24248
|
+
});
|
|
24249
|
+
else logger_logger.error(message);
|
|
23261
24250
|
process.exit(1);
|
|
23262
24251
|
}
|
|
23263
24252
|
});
|
|
23264
24253
|
}
|
|
24254
|
+
async function confirmOverwriteIfExists(projectRoot, name, options) {
|
|
24255
|
+
const { appId, type, force = false } = options;
|
|
24256
|
+
const existing = await findSecretInAnvilYaml(projectRoot, name, appId);
|
|
24257
|
+
if (existing?.type && existing.type !== type) throw new Error(`'${name}' is a ${existing.type} in anvil.yaml, not a ${type}`);
|
|
24258
|
+
if (!existing?.existsForApp) return false;
|
|
24259
|
+
if (force) return true;
|
|
24260
|
+
const message = `'${name}' already exists for app ${appId} in anvil.yaml`;
|
|
24261
|
+
if (!process.stdin.isTTY) throw new Error(`${message}. Use --force to overwrite it.`);
|
|
24262
|
+
if (!await logger_logger.confirm(`${message}. Overwrite it?`, false)) throw new Error(`Not overwriting '${name}'`);
|
|
24263
|
+
return true;
|
|
24264
|
+
}
|
|
24265
|
+
async function readSecretValue(name) {
|
|
24266
|
+
if (process.stdin.isTTY) {
|
|
24267
|
+
const { secret } = await logger_logger.prompt([
|
|
24268
|
+
{
|
|
24269
|
+
type: "password",
|
|
24270
|
+
name: "secret",
|
|
24271
|
+
message: `Value for secret '${name}':`
|
|
24272
|
+
}
|
|
24273
|
+
]);
|
|
24274
|
+
if (!secret) throw new Error("No secret value entered");
|
|
24275
|
+
return secret;
|
|
24276
|
+
}
|
|
24277
|
+
{
|
|
24278
|
+
const chunks = [];
|
|
24279
|
+
for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
24280
|
+
const secret = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
24281
|
+
if (!secret) throw new Error("No secret value received on stdin");
|
|
24282
|
+
return secret;
|
|
24283
|
+
}
|
|
24284
|
+
}
|
|
23265
24285
|
const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
|
|
23266
24286
|
const VERSION = program_packageJson.version;
|
|
23267
24287
|
setLogger(new CLILogger({
|
|
@@ -23302,7 +24322,7 @@ print(json.dumps({"issues": issues}))
|
|
|
23302
24322
|
walk(dir);
|
|
23303
24323
|
return files.sort();
|
|
23304
24324
|
}
|
|
23305
|
-
function
|
|
24325
|
+
async function validateSingleFileBasic(file) {
|
|
23306
24326
|
let fileContent;
|
|
23307
24327
|
try {
|
|
23308
24328
|
fileContent = (0, external_fs_.readFileSync)(file, "utf8");
|
|
@@ -23320,7 +24340,10 @@ print(json.dumps({"issues": issues}))
|
|
|
23320
24340
|
]
|
|
23321
24341
|
};
|
|
23322
24342
|
}
|
|
23323
|
-
const
|
|
24343
|
+
const formSpecPackageContext = "form_template.html" === inferValidationTarget(file) ? await buildFormSpecPackageContext(file) : void 0;
|
|
24344
|
+
const result = validatePath(file, fileContent, {
|
|
24345
|
+
formSpecPackageContext
|
|
24346
|
+
});
|
|
23324
24347
|
if (result.ok) return {
|
|
23325
24348
|
file,
|
|
23326
24349
|
type: result.target,
|
|
@@ -23340,6 +24363,74 @@ print(json.dumps({"issues": issues}))
|
|
|
23340
24363
|
} : {}
|
|
23341
24364
|
};
|
|
23342
24365
|
}
|
|
24366
|
+
function diagnosticsForFile(file, diagnostics, options = {}) {
|
|
24367
|
+
const absoluteFile = external_path_default().resolve(file);
|
|
24368
|
+
const globalDiagnostics = false === options.includeGlobalDiagnostics ? [] : diagnostics.filter((diagnostic)=>!diagnostic.file);
|
|
24369
|
+
const fileDiagnostics = diagnostics.filter((diagnostic)=>diagnostic.file && external_path_default().resolve(diagnostic.file) === absoluteFile);
|
|
24370
|
+
return [
|
|
24371
|
+
...fileDiagnostics,
|
|
24372
|
+
...globalDiagnostics
|
|
24373
|
+
];
|
|
24374
|
+
}
|
|
24375
|
+
function pythonFileResult(file, result, options = {}) {
|
|
24376
|
+
const issues = result.ok ? [] : diagnosticsForFile(file, result.issues, options);
|
|
24377
|
+
const warnings = diagnosticsForFile(file, result.warnings ?? [], options);
|
|
24378
|
+
if (result.ok || 0 === issues.length) return {
|
|
24379
|
+
file,
|
|
24380
|
+
type: "python",
|
|
24381
|
+
valid: true,
|
|
24382
|
+
...warnings.length > 0 ? {
|
|
24383
|
+
warnings
|
|
24384
|
+
} : {}
|
|
24385
|
+
};
|
|
24386
|
+
return {
|
|
24387
|
+
file,
|
|
24388
|
+
type: "python",
|
|
24389
|
+
valid: false,
|
|
24390
|
+
error: result.message,
|
|
24391
|
+
issues,
|
|
24392
|
+
...warnings.length > 0 ? {
|
|
24393
|
+
warnings
|
|
24394
|
+
} : {}
|
|
24395
|
+
};
|
|
24396
|
+
}
|
|
24397
|
+
function groupProjectDiagnostics(diagnostics) {
|
|
24398
|
+
const grouped = new Map();
|
|
24399
|
+
for (const diagnostic of diagnostics){
|
|
24400
|
+
const appDiagnostics = grouped.get(diagnostic.appRoot) ?? [];
|
|
24401
|
+
appDiagnostics.push(diagnostic);
|
|
24402
|
+
grouped.set(diagnostic.appRoot, appDiagnostics);
|
|
24403
|
+
}
|
|
24404
|
+
return grouped;
|
|
24405
|
+
}
|
|
24406
|
+
async function dependencyCacheDiagnostic(appRoot) {
|
|
24407
|
+
const message = await getDependencyCacheWarning(appRoot);
|
|
24408
|
+
if (!message) return;
|
|
24409
|
+
return {
|
|
24410
|
+
path: "root",
|
|
24411
|
+
message,
|
|
24412
|
+
code: "reportAnvilDependencyCache",
|
|
24413
|
+
rule: "reportAnvilDependencyCache",
|
|
24414
|
+
severity: "warning"
|
|
24415
|
+
};
|
|
24416
|
+
}
|
|
24417
|
+
async function validateSingleFile(file) {
|
|
24418
|
+
const appRoot = findAnvilAppRoot(file);
|
|
24419
|
+
const result = "python" === inferValidationTarget(file) && appRoot ? pythonFileResult(file, validatePythonWithPyright(appRoot, {
|
|
24420
|
+
files: [
|
|
24421
|
+
external_path_default().resolve(file)
|
|
24422
|
+
]
|
|
24423
|
+
})) : await validateSingleFileBasic(file);
|
|
24424
|
+
if (!appRoot) return result;
|
|
24425
|
+
const cacheDiagnostic = await dependencyCacheDiagnostic(appRoot);
|
|
24426
|
+
return cacheDiagnostic ? {
|
|
24427
|
+
...result,
|
|
24428
|
+
warnings: [
|
|
24429
|
+
...result.warnings ?? [],
|
|
24430
|
+
cacheDiagnostic
|
|
24431
|
+
]
|
|
24432
|
+
} : result;
|
|
24433
|
+
}
|
|
23343
24434
|
function logValidationFailure(fileResult) {
|
|
23344
24435
|
logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
|
|
23345
24436
|
for (const issue of fileResult.issues ?? []){
|
|
@@ -23355,8 +24446,8 @@ print(json.dumps({"issues": issues}))
|
|
|
23355
24446
|
function logValidationWarnings(fileResult) {
|
|
23356
24447
|
for (const warning of fileResult.warnings ?? [])logger_logger.warn(formatValidationWarning(fileResult.file, warning));
|
|
23357
24448
|
}
|
|
23358
|
-
function handleValidateFile(file) {
|
|
23359
|
-
const fileResult = validateSingleFile(file);
|
|
24449
|
+
async function handleValidateFile(file) {
|
|
24450
|
+
const fileResult = await validateSingleFile(file);
|
|
23360
24451
|
if (fileResult.valid) {
|
|
23361
24452
|
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23362
24453
|
data: {
|
|
@@ -23391,7 +24482,7 @@ print(json.dumps({"issues": issues}))
|
|
|
23391
24482
|
}
|
|
23392
24483
|
process.exitCode = 1;
|
|
23393
24484
|
}
|
|
23394
|
-
function handleValidateDirectory(dir) {
|
|
24485
|
+
async function handleValidateDirectory(dir) {
|
|
23395
24486
|
const files = collectValidationFiles(dir);
|
|
23396
24487
|
if (0 === files.length) {
|
|
23397
24488
|
const error = `No supported files found under ${dir}`;
|
|
@@ -23407,25 +24498,100 @@ print(json.dumps({"issues": issues}))
|
|
|
23407
24498
|
process.exitCode = 1;
|
|
23408
24499
|
return;
|
|
23409
24500
|
}
|
|
23410
|
-
const
|
|
24501
|
+
const pythonFiles = files.filter((file)=>"python" === inferValidationTarget(file));
|
|
24502
|
+
const nonPythonResults = new Map(await Promise.all(files.filter((file)=>"python" !== inferValidationTarget(file)).map(async (file)=>[
|
|
24503
|
+
file,
|
|
24504
|
+
await validateSingleFileBasic(file)
|
|
24505
|
+
])));
|
|
24506
|
+
const pythonResults = new Map();
|
|
24507
|
+
let globalPythonIssues = [];
|
|
24508
|
+
let globalPythonWarnings = [];
|
|
24509
|
+
if (pythonFiles.length > 0) {
|
|
24510
|
+
const pyright = findAnvilPyright();
|
|
24511
|
+
const filesByAppRoot = new Map();
|
|
24512
|
+
const filesWithoutAppRoot = [];
|
|
24513
|
+
for (const file of pythonFiles){
|
|
24514
|
+
const appRoot = findAnvilAppRoot(file);
|
|
24515
|
+
if (!appRoot) {
|
|
24516
|
+
filesWithoutAppRoot.push(file);
|
|
24517
|
+
continue;
|
|
24518
|
+
}
|
|
24519
|
+
const appFiles = filesByAppRoot.get(appRoot) ?? [];
|
|
24520
|
+
appFiles.push(file);
|
|
24521
|
+
filesByAppRoot.set(appRoot, appFiles);
|
|
24522
|
+
}
|
|
24523
|
+
for (const [appRoot, appFiles] of filesByAppRoot){
|
|
24524
|
+
const relativeAppRoot = external_path_default().relative(external_path_default().resolve(dir), external_path_default().resolve(appRoot));
|
|
24525
|
+
const includesWholeApp = "" === relativeAppRoot || !external_path_default().isAbsolute(relativeAppRoot) && ".." !== relativeAppRoot && !relativeAppRoot.startsWith(`..${external_path_default().sep}`);
|
|
24526
|
+
const result = validatePythonWithPyright(appRoot, {
|
|
24527
|
+
...includesWholeApp ? {} : {
|
|
24528
|
+
files: appFiles.map((file)=>external_path_default().resolve(file))
|
|
24529
|
+
},
|
|
24530
|
+
pyright
|
|
24531
|
+
});
|
|
24532
|
+
if (!result.ok) globalPythonIssues.push(...result.issues.filter((diagnostic)=>!diagnostic.file).map((diagnostic)=>({
|
|
24533
|
+
...diagnostic,
|
|
24534
|
+
appRoot
|
|
24535
|
+
})));
|
|
24536
|
+
globalPythonWarnings.push(...(result.warnings ?? []).filter((diagnostic)=>!diagnostic.file).map((diagnostic)=>({
|
|
24537
|
+
...diagnostic,
|
|
24538
|
+
appRoot
|
|
24539
|
+
})));
|
|
24540
|
+
for (const file of appFiles)pythonResults.set(file, pythonFileResult(file, result, {
|
|
24541
|
+
includeGlobalDiagnostics: false
|
|
24542
|
+
}));
|
|
24543
|
+
}
|
|
24544
|
+
for (const file of filesWithoutAppRoot)pythonResults.set(file, await validateSingleFileBasic(file));
|
|
24545
|
+
}
|
|
24546
|
+
const appRoots = new Set(files.map((file)=>findAnvilAppRoot(file)).filter((appRoot)=>null !== appRoot));
|
|
24547
|
+
for (const appRoot of appRoots){
|
|
24548
|
+
const cacheDiagnostic = await dependencyCacheDiagnostic(appRoot);
|
|
24549
|
+
if (cacheDiagnostic) globalPythonWarnings.push({
|
|
24550
|
+
...cacheDiagnostic,
|
|
24551
|
+
appRoot
|
|
24552
|
+
});
|
|
24553
|
+
}
|
|
24554
|
+
const fileResults = files.map((file)=>pythonResults.get(file) ?? nonPythonResults.get(file));
|
|
23411
24555
|
const failedResults = fileResults.filter((result)=>!result.valid);
|
|
23412
|
-
|
|
23413
|
-
|
|
24556
|
+
const valid = 0 === failedResults.length && 0 === globalPythonIssues.length;
|
|
24557
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(valid, {
|
|
24558
|
+
error: valid ? void 0 : "Validation failed",
|
|
23414
24559
|
data: {
|
|
23415
24560
|
file: dir,
|
|
23416
|
-
valid
|
|
24561
|
+
valid,
|
|
23417
24562
|
count: fileResults.length,
|
|
23418
|
-
files: fileResults
|
|
24563
|
+
files: fileResults,
|
|
24564
|
+
...globalPythonIssues.length > 0 ? {
|
|
24565
|
+
issues: globalPythonIssues
|
|
24566
|
+
} : {},
|
|
24567
|
+
...globalPythonWarnings.length > 0 ? {
|
|
24568
|
+
warnings: globalPythonWarnings
|
|
24569
|
+
} : {}
|
|
23419
24570
|
}
|
|
23420
24571
|
});
|
|
23421
|
-
else
|
|
23422
|
-
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
|
|
24572
|
+
else {
|
|
24573
|
+
for (const [appRoot, issues] of groupProjectDiagnostics(globalPythonIssues))logValidationFailure({
|
|
24574
|
+
file: appRoot,
|
|
24575
|
+
type: "python",
|
|
24576
|
+
valid: false,
|
|
24577
|
+
error: "Python semantic validation failed",
|
|
24578
|
+
issues
|
|
24579
|
+
});
|
|
24580
|
+
for (const [appRoot, warnings] of groupProjectDiagnostics(globalPythonWarnings))logValidationWarnings({
|
|
24581
|
+
file: appRoot,
|
|
24582
|
+
type: "python",
|
|
24583
|
+
valid: true,
|
|
24584
|
+
warnings
|
|
24585
|
+
});
|
|
24586
|
+
for (const fileResult of fileResults)if (fileResult.valid) {
|
|
24587
|
+
logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
|
|
24588
|
+
logValidationWarnings(fileResult);
|
|
24589
|
+
} else {
|
|
24590
|
+
logValidationFailure(fileResult);
|
|
24591
|
+
logValidationWarnings(fileResult);
|
|
24592
|
+
}
|
|
23427
24593
|
}
|
|
23428
|
-
if (
|
|
24594
|
+
if (!valid) process.exitCode = 1;
|
|
23429
24595
|
}
|
|
23430
24596
|
function getUpdateInstructions(platform) {
|
|
23431
24597
|
if ("win32" === platform) return [
|
|
@@ -23536,11 +24702,14 @@ print(json.dumps({"issues": issues}))
|
|
|
23536
24702
|
registerVersionCommand(program, VERSION);
|
|
23537
24703
|
registerConfigureCommand(program, VERSION);
|
|
23538
24704
|
registerDepsCommand(program);
|
|
23539
|
-
|
|
24705
|
+
registerDbCommand(program);
|
|
24706
|
+
registerEnvCommand(program);
|
|
24707
|
+
registerReplCommand(program);
|
|
24708
|
+
registerSecretCommand(program);
|
|
23540
24709
|
program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
|
|
23541
24710
|
await handleUpdateCommand();
|
|
23542
24711
|
});
|
|
23543
|
-
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)=>{
|
|
24712
|
+
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(async (file)=>{
|
|
23544
24713
|
let pathStat;
|
|
23545
24714
|
try {
|
|
23546
24715
|
pathStat = (0, external_fs_.statSync)(file);
|
|
@@ -23549,8 +24718,8 @@ print(json.dumps({"issues": issues}))
|
|
|
23549
24718
|
process.exitCode = 1;
|
|
23550
24719
|
return;
|
|
23551
24720
|
}
|
|
23552
|
-
if (pathStat.isDirectory()) return void handleValidateDirectory(file);
|
|
23553
|
-
handleValidateFile(file);
|
|
24721
|
+
if (pathStat.isDirectory()) return void await handleValidateDirectory(file);
|
|
24722
|
+
await handleValidateFile(file);
|
|
23554
24723
|
});
|
|
23555
24724
|
if (watchCommand) {
|
|
23556
24725
|
const watchOptions = watchCommand.options.map((opt)=>{
|