@anvil-works/anvil-cli 0.7.0-canary.11 → 0.7.0-canary.13

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/dist/cli.js CHANGED
@@ -13718,7 +13718,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13718
13718
  }
13719
13719
  function getCredentialHelperModulePath(options) {
13720
13720
  const currentModulePath = options?.currentModulePath ?? __filename;
13721
- if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
13721
+ if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
13722
13722
  return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
13723
13723
  }
13724
13724
  async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
@@ -14689,6 +14689,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14689
14689
  throw errors_createGitError.commandFailed("status", e.message);
14690
14690
  }
14691
14691
  }
14692
+ async discardModeOnlyChanges(paths) {
14693
+ const discarded = [];
14694
+ for (const relativePath of paths)try {
14695
+ const headMode = await this.getHeadFileMode(relativePath);
14696
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14697
+ const contentUnchanged = await this.hasSameContentAsHead(relativePath);
14698
+ if (!contentUnchanged) continue;
14699
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14700
+ discarded.push(relativePath);
14701
+ } catch (e) {
14702
+ throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
14703
+ }
14704
+ return discarded;
14705
+ }
14706
+ async discardStagedModeOnlyChanges(paths) {
14707
+ const discarded = [];
14708
+ for (const relativePath of paths)try {
14709
+ const headMode = await this.getHeadFileMode(relativePath);
14710
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14711
+ const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14712
+ if (!indexContentUnchanged) continue;
14713
+ await this.git.raw([
14714
+ "reset",
14715
+ "-q",
14716
+ "HEAD",
14717
+ "--",
14718
+ relativePath
14719
+ ]);
14720
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14721
+ discarded.push(relativePath);
14722
+ } catch (e) {
14723
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14724
+ }
14725
+ return discarded;
14726
+ }
14727
+ async getHeadFileMode(relativePath) {
14728
+ const output = await this.git.raw([
14729
+ "ls-tree",
14730
+ "HEAD",
14731
+ "--",
14732
+ relativePath
14733
+ ]);
14734
+ const match = output.match(/^(\d{6})\s/);
14735
+ return match?.[1] ?? null;
14736
+ }
14737
+ async hasSameContentAsHead(relativePath) {
14738
+ const headHash = (await this.git.revparse([
14739
+ `HEAD:${relativePath}`
14740
+ ])).trim();
14741
+ const worktreeHash = (await this.git.raw([
14742
+ "hash-object",
14743
+ "--",
14744
+ relativePath
14745
+ ])).trim();
14746
+ return headHash === worktreeHash;
14747
+ }
14748
+ async hasSameIndexContentAsHead(relativePath) {
14749
+ const headHash = (await this.git.revparse([
14750
+ `HEAD:${relativePath}`
14751
+ ])).trim();
14752
+ const indexHash = (await this.git.revparse([
14753
+ `:${relativePath}`
14754
+ ])).trim();
14755
+ return headHash === indexHash;
14756
+ }
14757
+ async restoreExecutableBit(relativePath, executable) {
14758
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14759
+ const stat = await external_fs_.promises.stat(filePath);
14760
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14761
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14762
+ }
14692
14763
  async hasUncommittedChanges() {
14693
14764
  const status = await this.getStatus();
14694
14765
  return !status.isClean;
@@ -14828,6 +14899,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14828
14899
  throw errors_createGitError.commandFailed("rebase --abort", e.message);
14829
14900
  }
14830
14901
  }
14902
+ async isAncestor(ancestorRef, descendantRef) {
14903
+ try {
14904
+ const mergeBase = (await this.git.raw([
14905
+ "merge-base",
14906
+ ancestorRef,
14907
+ descendantRef
14908
+ ])).trim();
14909
+ const ancestorCommit = (await this.git.revparse([
14910
+ ancestorRef
14911
+ ])).trim();
14912
+ return mergeBase === ancestorCommit;
14913
+ } catch {
14914
+ return false;
14915
+ }
14916
+ }
14917
+ async mergeFastForward(ref) {
14918
+ try {
14919
+ await this.git.raw([
14920
+ "merge",
14921
+ "--ff-only",
14922
+ ref
14923
+ ]);
14924
+ } catch (e) {
14925
+ throw errors_createGitError.commandFailed("merge --ff-only", e.message);
14926
+ }
14927
+ }
14831
14928
  async deleteRef(ref) {
14832
14929
  try {
14833
14930
  await this.git.raw([
@@ -14872,11 +14969,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14872
14969
  } catch (error) {}
14873
14970
  }
14874
14971
  }
14875
- async function getStagedFileChanges(git) {
14972
+ async function getStagedFileChanges(git, gitService) {
14876
14973
  try {
14877
14974
  const status = await git.status();
14878
14975
  const renames = status.renamed || [];
14879
14976
  const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
14977
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14880
14978
  const stagedRenames = [];
14881
14979
  for (const f of stagedFiles)if ("R" === f.index) {
14882
14980
  const rename = renames.find((r)=>r.to === f.path);
@@ -14889,19 +14987,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14889
14987
  const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14890
14988
  const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14891
14989
  const changes = [];
14892
- for (const f of stagedFiles)if ("R" !== f.index) {
14893
- if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14894
- path: f.path,
14895
- type: "unlink"
14896
- });
14897
- else if ("A" === f.index || "?" === f.index) changes.push({
14898
- path: f.path,
14899
- type: "add"
14900
- });
14901
- else changes.push({
14902
- path: f.path,
14903
- type: "change"
14904
- });
14990
+ for (const f of stagedFiles){
14991
+ if ("R" !== f.index) {
14992
+ if (!stagedModeOnlyPaths.has(f.path)) {
14993
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14994
+ path: f.path,
14995
+ type: "unlink"
14996
+ });
14997
+ else if ("A" === f.index || "?" === f.index) changes.push({
14998
+ path: f.path,
14999
+ type: "add"
15000
+ });
15001
+ else changes.push({
15002
+ path: f.path,
15003
+ type: "change"
15004
+ });
15005
+ }
15006
+ }
14905
15007
  }
14906
15008
  changes.push(...stagedRenames);
14907
15009
  return changes;
@@ -17657,13 +17759,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17657
17759
  }
17658
17760
  async getStagedChanges() {
17659
17761
  try {
17660
- return await getStagedFileChanges(this.config.gitService.getGit());
17762
+ return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
17661
17763
  } catch (e) {
17662
17764
  logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
17663
17765
  return [];
17664
17766
  }
17665
17767
  }
17666
17768
  async getUnstagedChanges(status) {
17769
+ const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
17667
17770
  const renames = status.renamed;
17668
17771
  const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
17669
17772
  const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
@@ -17683,7 +17786,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17683
17786
  const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
17684
17787
  const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
17685
17788
  return [
17686
- ...status.modified.map((f)=>({
17789
+ ...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
17687
17790
  path: f,
17688
17791
  type: "change"
17689
17792
  })),
@@ -17891,7 +17994,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17891
17994
  await this.handleConflict(json_resp, originalFilePaths, retryCount);
17892
17995
  return;
17893
17996
  }
17894
- if (200 === resp.status) return void await this.handleSuccess(json_resp);
17997
+ if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
17895
17998
  logger_logger.error(`Unexpected response status: ${resp.status}`);
17896
17999
  throw {
17897
18000
  type: "server_error",
@@ -17994,13 +18097,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17994
18097
  reason: "Unknown conflict"
17995
18098
  };
17996
18099
  }
17997
- async handleSuccess(json_resp) {
18100
+ async handleSuccess(json_resp, originalFilePaths) {
17998
18101
  const version = json_resp.version;
17999
- this.config.setCommitId(version);
18102
+ const oldCommitId = json_resp.previous_commit ?? null;
18103
+ const commitIdBeforePostSaveSync = this.config.getCommitId();
18104
+ this.emit("saved", {
18105
+ oldCommitId,
18106
+ newCommitId: version,
18107
+ branch: this.config.getCurrentBranch()
18108
+ });
18000
18109
  if (this.postSaveCallback) {
18001
18110
  logger_logger.progressUpdate("sync", "Syncing back");
18002
- await this.postSaveCallback();
18111
+ await this.postSaveCallback(new Set(originalFilePaths));
18003
18112
  }
18113
+ if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
18004
18114
  }
18005
18115
  async rerouteChanges(changes) {
18006
18116
  logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
@@ -18071,12 +18181,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18071
18181
  try {
18072
18182
  const oldCommitId = this.config.getCommitId();
18073
18183
  let locallyChangedFiles;
18184
+ let initialStatus;
18074
18185
  if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
18075
18186
  else {
18076
- const status = await this.config.gitService.getStatus();
18077
- locallyChangedFiles = buildLocalChangesSet(status);
18187
+ initialStatus = await this.config.gitService.getStatus();
18188
+ locallyChangedFiles = buildLocalChangesSet(initialStatus);
18078
18189
  }
18079
- await this.fetchAndResetFromAnvil(locallyChangedFiles);
18190
+ let syncMode;
18191
+ if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
18192
+ else {
18193
+ await this.fetchAndResetFromAnvil(locallyChangedFiles);
18194
+ syncMode = "preservation";
18195
+ }
18196
+ if ("clean-move" === syncMode) return {
18197
+ localOnlyChanges: [],
18198
+ conflicts: []
18199
+ };
18080
18200
  const newCommitId = await this.config.gitService.getCommitId();
18081
18201
  this.config.setCommitId(newCommitId);
18082
18202
  const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
@@ -18101,15 +18221,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18101
18221
  }
18102
18222
  }
18103
18223
  async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
18224
+ const oldCommitId = this.config.getCommitId();
18225
+ await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
18226
+ }
18227
+ async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
18228
+ return this.withFetchedAnvilRef(async (tempRef)=>{
18229
+ const localHead = await this.config.gitService.getCommitId();
18230
+ if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
18231
+ else {
18232
+ const statusBeforeHardReset = await this.config.gitService.getStatus();
18233
+ if (!this.isCleanForFastForward(statusBeforeHardReset)) {
18234
+ for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
18235
+ await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
18236
+ return "preservation";
18237
+ }
18238
+ await this.config.gitService.reset(tempRef, "hard");
18239
+ }
18240
+ const newCommitId = await this.config.gitService.getCommitId();
18241
+ this.config.setCommitId(newCommitId);
18242
+ await this.config.editorYaml.reload();
18243
+ return "clean-move";
18244
+ });
18245
+ }
18246
+ async withFetchedAnvilRef(callback) {
18104
18247
  const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
18105
18248
  this.config.setAuthToken(validToken);
18106
18249
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
18107
18250
  const currentBranch = this.config.getCurrentBranch();
18108
18251
  const tempRef = `anvil-sync-temp-${Date.now()}`;
18109
- const oldCommitId = this.config.getCommitId();
18110
- await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18252
+ try {
18253
+ await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18254
+ return await callback(tempRef);
18255
+ } finally{
18256
+ await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
18257
+ }
18258
+ }
18259
+ async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
18111
18260
  await this.config.gitService.reset(tempRef, "mixed");
18112
- await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
18113
18261
  const newCommitId = await this.config.gitService.getCommitId();
18114
18262
  await this.config.gitService.checkout([
18115
18263
  ".anvil_editor.yaml",
@@ -18119,6 +18267,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18119
18267
  await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
18120
18268
  await this.discardFormattingOnlyYamlChanges();
18121
18269
  }
18270
+ isCleanForFastForward(status) {
18271
+ return status.isClean && 0 === status.modified.length && 0 === status.notAdded.length && 0 === status.created.length && 0 === status.deleted.length && 0 === status.staged.length && 0 === status.renamed.length;
18272
+ }
18122
18273
  async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
18123
18274
  try {
18124
18275
  const status = await this.config.gitService.getStatus();
@@ -18332,11 +18483,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18332
18483
  isPaused: ()=>this.isPausedForUserInput
18333
18484
  });
18334
18485
  this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
18335
- this.saveProcessor.setPostSaveCallback(()=>this.syncManager.fetchAndResetFromAnvil());
18486
+ this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
18336
18487
  this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
18337
18488
  this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
18338
18489
  this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
18339
18490
  this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
18491
+ this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
18340
18492
  this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
18341
18493
  this.queueDependencyRefresh("anvil.yaml dependencies changed");
18342
18494
  });
@@ -18344,6 +18496,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18344
18496
  this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
18345
18497
  this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
18346
18498
  }
18499
+ async syncAfterSave(savedFilePaths) {
18500
+ const status = await this.gitService.getStatus();
18501
+ const locallyChangedFiles = buildLocalChangesSet(status);
18502
+ if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
18503
+ return this.syncManager.syncRemoteChanges(locallyChangedFiles);
18504
+ }
18347
18505
  async connectWebSocket() {
18348
18506
  this.wsClient = new WebSocketClient({
18349
18507
  appId: this.appId,
@@ -20170,12 +20328,396 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20170
20328
  });
20171
20329
  checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
20172
20330
  }
20331
+ const defaultGitAuthDoctorDeps = {
20332
+ getAppAuthBinding: getAppAuthBinding,
20333
+ getAccountsForUrl: auth_getAccountsForUrl,
20334
+ hasTokensForUrl: hasTokensForUrl,
20335
+ runCredentialFill: runGitCredentialFill
20336
+ };
20337
+ function getHighestStatus(statuses) {
20338
+ if (statuses.includes("error")) return "error";
20339
+ if (statuses.includes("warning")) return "warning";
20340
+ return "ok";
20341
+ }
20342
+ function finding(status, code, message) {
20343
+ return {
20344
+ status,
20345
+ code,
20346
+ message
20347
+ };
20348
+ }
20349
+ function redactSecrets(value) {
20350
+ return value.replace(/^password=.*$/gim, "password=<redacted>").replace(/(https?:\/\/git:)[^@\s]+@/g, "$1<redacted>@").replace(/(access[_-]?token=)[^&\s]+/gi, "$1<redacted>").replace(/(refresh[_-]?token=)[^&\s]+/gi, "$1<redacted>");
20351
+ }
20352
+ function splitShellCommand(command) {
20353
+ const tokens = [];
20354
+ let current = "";
20355
+ let state = "normal";
20356
+ for(let i = 0; i < command.length; i++){
20357
+ const ch = command[i];
20358
+ if ("single" === state) {
20359
+ if ("'" === ch) state = "normal";
20360
+ else current += ch;
20361
+ continue;
20362
+ }
20363
+ if ("double" === state) {
20364
+ if ("\"" === ch) state = "normal";
20365
+ else if ("\\" === ch && i + 1 < command.length && [
20366
+ "$",
20367
+ "`",
20368
+ "\"",
20369
+ "\\",
20370
+ "\n"
20371
+ ].includes(command[i + 1])) current += command[++i];
20372
+ else current += ch;
20373
+ continue;
20374
+ }
20375
+ if (/\s/.test(ch)) {
20376
+ if (current) {
20377
+ tokens.push(current);
20378
+ current = "";
20379
+ }
20380
+ } else if ("'" === ch) state = "single";
20381
+ else if ("\"" === ch) state = "double";
20382
+ else if ("\\" === ch && i + 1 < command.length) current += command[++i];
20383
+ else current += ch;
20384
+ }
20385
+ if ("normal" !== state) throw new Error("Unterminated quote in helper command");
20386
+ if (current) tokens.push(current);
20387
+ return tokens;
20388
+ }
20389
+ function parseCredentialHelperCommand(command) {
20390
+ const trimmed = command.trim();
20391
+ if (!trimmed || !trimmed.startsWith("!")) return {};
20392
+ const tokens = splitShellCommand(trimmed.slice(1).trim());
20393
+ return {
20394
+ execPath: tokens[0],
20395
+ helperPath: tokens[1]
20396
+ };
20397
+ }
20398
+ function readHelperModulePath(helperPath) {
20399
+ try {
20400
+ const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
20401
+ const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
20402
+ return match?.[2];
20403
+ } catch {
20404
+ return;
20405
+ }
20406
+ }
20407
+ function inspectCredentialHelperCommand(raw) {
20408
+ const details = {
20409
+ raw
20410
+ };
20411
+ try {
20412
+ const parsed = parseCredentialHelperCommand(raw);
20413
+ details.execPath = parsed.execPath;
20414
+ details.helperPath = parsed.helperPath;
20415
+ } catch (e) {
20416
+ details.parseError = e.message;
20417
+ return details;
20418
+ }
20419
+ if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
20420
+ if (details.helperPath) {
20421
+ details.helperExists = external_fs_default().existsSync(details.helperPath);
20422
+ if (details.helperExists) {
20423
+ try {
20424
+ const stat = external_fs_default().statSync(details.helperPath);
20425
+ details.helperExecutable = (73 & stat.mode) !== 0;
20426
+ } catch {
20427
+ details.helperExecutable = false;
20428
+ }
20429
+ details.modulePath = readHelperModulePath(details.helperPath);
20430
+ if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
20431
+ }
20432
+ }
20433
+ return details;
20434
+ }
20435
+ function detectAnvilRemote(remote) {
20436
+ const fetchUrl = remote.refs.fetch;
20437
+ const out = {
20438
+ name: remote.name,
20439
+ fetchUrl
20440
+ };
20441
+ if (!fetchUrl) return out;
20442
+ const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
20443
+ if (httpMatch) {
20444
+ const [, protocol, host, appId] = httpMatch;
20445
+ return {
20446
+ ...out,
20447
+ appId,
20448
+ anvilUrl: normalizeAnvilUrl(`${protocol}://${host}`),
20449
+ transport: "https"
20450
+ };
20451
+ }
20452
+ const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
20453
+ if (sshMatch) {
20454
+ const [, , host, appId] = sshMatch;
20455
+ return {
20456
+ ...out,
20457
+ appId,
20458
+ anvilUrl: normalizeAnvilUrl(host),
20459
+ transport: "ssh"
20460
+ };
20461
+ }
20462
+ return out;
20463
+ }
20464
+ async function getLocalConfigValues(repoPath, key) {
20465
+ try {
20466
+ const output = await esm_default(repoPath).raw([
20467
+ "config",
20468
+ "--local",
20469
+ "--get-all",
20470
+ key
20471
+ ]);
20472
+ const values = output.split(/\r?\n/);
20473
+ if ("" === values[values.length - 1]) values.pop();
20474
+ return values;
20475
+ } catch {
20476
+ return [];
20477
+ }
20478
+ }
20479
+ function buildCredentialInput(anvilUrl, appId) {
20480
+ const url = new URL(anvilUrl);
20481
+ return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
20482
+ }
20483
+ async function runGitCredentialFill(repoPath, input) {
20484
+ return new Promise((resolve)=>{
20485
+ const child = (0, external_child_process_namespaceObject.spawn)("git", [
20486
+ "credential",
20487
+ "fill"
20488
+ ], {
20489
+ cwd: repoPath,
20490
+ env: {
20491
+ ...process.env,
20492
+ GIT_TERMINAL_PROMPT: "0"
20493
+ },
20494
+ stdio: [
20495
+ "pipe",
20496
+ "pipe",
20497
+ "pipe"
20498
+ ]
20499
+ });
20500
+ let stdout = "";
20501
+ let stderr = "";
20502
+ child.stdout.setEncoding("utf8");
20503
+ child.stderr.setEncoding("utf8");
20504
+ child.stdout.on("data", (chunk)=>{
20505
+ stdout += chunk;
20506
+ });
20507
+ child.stderr.on("data", (chunk)=>{
20508
+ stderr += chunk;
20509
+ });
20510
+ child.on("error", (error)=>{
20511
+ resolve({
20512
+ exitCode: null,
20513
+ stdout: redactSecrets(stdout),
20514
+ stderr: redactSecrets(stderr),
20515
+ error: redactSecrets(error.message)
20516
+ });
20517
+ });
20518
+ child.on("close", (code)=>{
20519
+ resolve({
20520
+ exitCode: code,
20521
+ stdout: redactSecrets(stdout),
20522
+ stderr: redactSecrets(stderr)
20523
+ });
20524
+ });
20525
+ child.stdin.end(input);
20526
+ });
20527
+ }
20528
+ async function buildAppReport(repoPath, remote, deps) {
20529
+ const appId = remote.appId;
20530
+ const anvilUrl = remote.anvilUrl;
20531
+ const url = new URL(anvilUrl);
20532
+ const credentialScope = `${url.protocol}//${url.host}`;
20533
+ const helperConfigKey = `credential.${credentialScope}.helper`;
20534
+ const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
20535
+ const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
20536
+ const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
20537
+ const findings = [];
20538
+ if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
20539
+ else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
20540
+ for (const helper of helperDetails){
20541
+ if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
20542
+ if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
20543
+ if (helper.helperPath) {
20544
+ if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
20545
+ else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
20546
+ } else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
20547
+ if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
20548
+ if (helper.modulePath && "cli.js" === external_path_default().basename(helper.modulePath)) findings.push(finding("error", "helper_module_cli_entry", `Generated helper points at the CLI entry instead of the API entry: ${helper.modulePath}`));
20549
+ }
20550
+ const binding = await deps.getAppAuthBinding(repoPath, appId);
20551
+ if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
20552
+ if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
20553
+ const authUrl = binding.url ? normalizeAnvilUrl(binding.url) : anvilUrl;
20554
+ let accounts = [];
20555
+ let authUsername = binding.username;
20556
+ let hasUsableAuth = false;
20557
+ try {
20558
+ accounts = await deps.getAccountsForUrl(authUrl);
20559
+ if (authUsername || 1 !== accounts.length) {
20560
+ if (!authUsername && accounts.length > 1) findings.push(finding("error", "multiple_accounts_without_binding", `Multiple accounts are logged in for ${authUrl}; bind one with anvil checkout --user.`));
20561
+ } else authUsername = accounts[0];
20562
+ hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
20563
+ if (!hasUsableAuth) findings.push(finding("error", "missing_usable_auth", authUsername ? `No usable auth tokens found for ${authUrl} as ${authUsername}.` : `No usable auth tokens found for ${authUrl}.`));
20564
+ } catch (e) {
20565
+ findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
20566
+ }
20567
+ const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
20568
+ const credentialSimulation = {
20569
+ status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
20570
+ exitCode: fillResult.exitCode,
20571
+ stdout: redactSecrets(fillResult.stdout),
20572
+ stderr: redactSecrets(fillResult.stderr),
20573
+ error: fillResult.error ? redactSecrets(fillResult.error) : void 0
20574
+ };
20575
+ if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
20576
+ const report = {
20577
+ appId,
20578
+ anvilUrl,
20579
+ remoteName: remote.name,
20580
+ remoteUrl: remote.fetchUrl,
20581
+ credentialScope,
20582
+ helperConfigKey,
20583
+ helperValues,
20584
+ helperDetails,
20585
+ binding: {
20586
+ url: binding.url,
20587
+ username: binding.username,
20588
+ hasUrl: !!binding.url,
20589
+ hasUsername: !!binding.username
20590
+ },
20591
+ auth: {
20592
+ url: authUrl,
20593
+ username: authUsername,
20594
+ accounts,
20595
+ hasUsableAuth
20596
+ },
20597
+ credentialSimulation,
20598
+ findings
20599
+ };
20600
+ return report;
20601
+ }
20602
+ async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
20603
+ const deps = options.deps ?? defaultGitAuthDoctorDeps;
20604
+ const inputPath = external_path_default().resolve(repoPathInput);
20605
+ const git = esm_default(inputPath);
20606
+ const repoPath = (await git.raw([
20607
+ "rev-parse",
20608
+ "--show-toplevel"
20609
+ ])).trim();
20610
+ const rawGitDir = (await git.raw([
20611
+ "rev-parse",
20612
+ "--git-dir"
20613
+ ])).trim();
20614
+ const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
20615
+ setRepoContext(repoPath);
20616
+ const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
20617
+ const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
20618
+ const findings = [];
20619
+ if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
20620
+ const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
20621
+ const status = getHighestStatus([
20622
+ ...findings.map((item)=>item.status),
20623
+ ...apps.flatMap((app)=>app.findings.map((item)=>item.status))
20624
+ ]);
20625
+ return {
20626
+ repoPath,
20627
+ gitDir,
20628
+ remotes,
20629
+ apps,
20630
+ findings,
20631
+ status
20632
+ };
20633
+ }
20634
+ function formatStatus(status) {
20635
+ if ("ok" === status) return external_chalk_default().green("[ok]");
20636
+ if ("warning" === status) return external_chalk_default().yellow("[warn]");
20637
+ return external_chalk_default().red("[error]");
20638
+ }
20639
+ function formatGitAuthDoctorReport(report) {
20640
+ const lines = [];
20641
+ lines.push(external_chalk_default().bold("Git auth doctor"));
20642
+ lines.push(`Repository: ${report.repoPath}`);
20643
+ lines.push(`Git dir: ${report.gitDir}`);
20644
+ lines.push(`Overall: ${formatStatus(report.status)}`);
20645
+ lines.push("");
20646
+ lines.push(external_chalk_default().bold("Detected remotes:"));
20647
+ if (0 === report.remotes.length) lines.push(" (none)");
20648
+ else for (const remote of report.remotes){
20649
+ const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
20650
+ lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
20651
+ }
20652
+ for (const app of report.apps){
20653
+ lines.push("");
20654
+ lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
20655
+ lines.push(` URL: ${app.anvilUrl}`);
20656
+ lines.push(` Credential scope: ${app.credentialScope}`);
20657
+ lines.push(` Helper config: ${app.helperConfigKey}`);
20658
+ if (0 === app.helperValues.length) lines.push(" (none)");
20659
+ else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
20660
+ for (const helper of app.helperDetails){
20661
+ lines.push(` Helper command: ${helper.raw}`);
20662
+ lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
20663
+ lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
20664
+ if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
20665
+ }
20666
+ lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
20667
+ lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
20668
+ lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
20669
+ if (app.credentialSimulation) {
20670
+ lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
20671
+ if (app.credentialSimulation.stdout.trim()) {
20672
+ lines.push(" stdout:");
20673
+ for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
20674
+ }
20675
+ if (app.credentialSimulation.stderr.trim()) {
20676
+ lines.push(" stderr:");
20677
+ for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
20678
+ }
20679
+ }
20680
+ if (app.findings.length > 0) {
20681
+ lines.push(" Findings:");
20682
+ for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
20683
+ }
20684
+ }
20685
+ if (report.findings.length > 0) {
20686
+ lines.push("");
20687
+ lines.push(external_chalk_default().bold("Repository findings:"));
20688
+ for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
20689
+ }
20690
+ return lines.join("\n");
20691
+ }
20692
+ function registerGitAuthCommand(program) {
20693
+ const gitAuthCommand = program.command("git-auth").description("Diagnose Anvil Git authentication");
20694
+ gitAuthCommand.command("doctor").description("Diagnose why plain Git commands cannot authenticate to Anvil").option("--repo <PATH>", "Repository path to diagnose", process.cwd()).action(async (options)=>{
20695
+ try {
20696
+ const report = await buildGitAuthDoctorReport(options.repo || process.cwd());
20697
+ if (getGlobalOutputConfig().jsonMode) logJsonResult("error" !== report.status, {
20698
+ data: report
20699
+ });
20700
+ else logger_logger.info(formatGitAuthDoctorReport(report));
20701
+ if ("error" === report.status) process.exitCode = 1;
20702
+ } catch (e) {
20703
+ const message = redactSecrets(e.message);
20704
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
20705
+ error: message
20706
+ });
20707
+ else logger_logger.error(`Git auth doctor failed: ${message}`);
20708
+ process.exitCode = 1;
20709
+ }
20710
+ });
20711
+ }
20173
20712
  const gitCredential_defaultDeps = {
20174
20713
  getValidAuthToken: auth_getValidAuthToken,
20175
20714
  getAccountsForUrl: auth_getAccountsForUrl,
20176
20715
  getAppAuthBinding: getAppAuthBinding,
20177
20716
  cwd: ()=>process.cwd()
20178
20717
  };
20718
+ function sanitizeCredentialError(message) {
20719
+ return message.replace(/^password=.*$/gim, "password=<redacted>").replace(/(https?:\/\/git:)[^@\s]+@/g, "$1<redacted>@").replace(/(access[_-]?token=)[^&\s]+/gi, "$1<redacted>").replace(/(refresh[_-]?token=)[^&\s]+/gi, "$1<redacted>");
20720
+ }
20179
20721
  function parseGitCredentialRequest(rawInput) {
20180
20722
  const out = {};
20181
20723
  const lines = rawInput.split(/\r?\n/);
@@ -20236,7 +20778,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20236
20778
  }).description("Internal helper command for Git HTTPS authentication").action(async (operation)=>{
20237
20779
  try {
20238
20780
  await executeGitCredentialOperation(operation);
20239
- } catch {
20781
+ } catch (e) {
20782
+ if ("1" === process.env["ANVIL_GIT_CREDENTIAL_DEBUG"]) {
20783
+ const message = e instanceof Error ? e.message : String(e);
20784
+ process.stderr.write(`anvil git-credential failed: ${sanitizeCredentialError(message)}\n`);
20785
+ }
20240
20786
  process.exit(1);
20241
20787
  }
20242
20788
  });
@@ -21945,6 +22491,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
21945
22491
  });
21946
22492
  const watchCommand = registerWatchCommand(program);
21947
22493
  registerCheckoutCommand(program);
22494
+ registerGitAuthCommand(program);
21948
22495
  registerGitCredentialCommand(program);
21949
22496
  registerLoginCommand(program);
21950
22497
  registerLogoutCommand(program);