From 60f6ef0b74d7a00b50748ac9ac3ce72855537954 Mon Sep 17 00:00:00 2001
From: Steven Bazyl <sbazyl@google.com>
Date: Tue, 3 Aug 2021 11:31:18 -0600
Subject: [PATCH 1/5] fix: Don't require package.json for simple commands
 (#840)

Moves ts2gas to a dynamic import only when transpiling typescript code.
---
 src/files.ts | 49 +++++++++++++++++++++++++++----------------------
 1 file changed, 27 insertions(+), 22 deletions(-)

diff --git a/src/files.ts b/src/files.ts
index b350a351..faf6a00a 100644
--- a/src/files.ts
+++ b/src/files.ts
@@ -4,7 +4,6 @@ import multimatch from 'multimatch';
 import path from 'path';
 import pMap from 'p-map';
 import recursive from 'recursive-readdir';
-import ts2gas from 'ts2gas';
 import typescript from 'typescript';
 
 import {loadAPICredentials, script} from './auth.js';
@@ -41,20 +40,22 @@ interface ProjectFile {
   readonly type: string;
 }
 
-const projectFileWithContent = (file: ProjectFile, transpileOptions: TranspileOptions): ProjectFile => {
-  const source = fs.readFileSync(file.name).toString();
-  const type = getApiFileType(file.name);
-
-  return type === 'TS'
-    ? // Transpile TypeScript to Google Apps Script
-      // @see github.com/grant/ts2gas
-      {
-        ...file,
-        source: ts2gas(source, transpileOptions),
-        type: 'SERVER_JS',
-      }
-    : {...file, source, type};
-};
+async function transpile(source: string, transpileOptions: TranspileOptions): Promise<string> {
+  const ts2gas = await import('ts2gas');
+  return ts2gas.default(source, transpileOptions);
+}
+
+async function projectFileWithContent(file: ProjectFile, transpileOptions: TranspileOptions): Promise<ProjectFile> {
+  const content = await fs.readFile(file.name);
+  let source = content.toString();
+  let type = getApiFileType(file.name);
+
+  if (type === 'TS') {
+    source = await transpile(source, transpileOptions);
+    type = 'SERVER_JS';
+  }
+  return {...file, source, type};
+}
 
 const ignoredProjectFile = (file: ProjectFile): ProjectFile => ({...file, source: '', isIgnored: true, type: ''});
 
@@ -102,7 +103,8 @@ export const getAllProjectFiles = async (rootDir: string = path.join('.', '/')):
     });
     files.sort((a, b) => a.name.localeCompare(b.name));
 
-    return getContentOfProjectFiles(files).map((file: ProjectFile): ProjectFile => {
+    const filesWithContent = await getContentOfProjectFiles(files);
+    return filesWithContent.map((file: ProjectFile): ProjectFile => {
       // Loop through files that are not ignored from `.claspignore`
       if (!file.isIgnored) {
         // Prevent node_modules/@types/
@@ -142,14 +144,16 @@ export const splitProjectFiles = (files: ProjectFile[]): [ProjectFile[], Project
   files.filter(file => file.isIgnored),
 ];
 
-const getContentOfProjectFiles = (files: ProjectFile[]) => {
+async function getContentOfProjectFiles(files: ProjectFile[]) {
   const transpileOpttions = getTranspileOptions();
 
-  return files.map(file => (file.isIgnored ? file : projectFileWithContent(file, transpileOpttions)));
-};
+  const getContent = (file: ProjectFile) => (file.isIgnored ? file : projectFileWithContent(file, transpileOpttions));
+  return Promise.all(files.map(getContent));
+}
 
-const getAppsScriptFilesFromProjectFiles = (files: ProjectFile[], rootDir: string) =>
-  getContentOfProjectFiles(files).map((file): AppsScriptFile => {
+async function getAppsScriptFilesFromProjectFiles(files: ProjectFile[], rootDir: string) {
+  const filesWithContent = await getContentOfProjectFiles(files);
+  return filesWithContent.map(file => {
     const {name, source, type} = file;
 
     return {
@@ -158,6 +162,7 @@ const getAppsScriptFilesFromProjectFiles = (files: ProjectFile[], rootDir: strin
       type, // The file extension
     };
   });
+}
 
 // This statement customizes the order in which the files are pushed.
 // It puts the files in the setting's filePushOrder first.
@@ -373,7 +378,7 @@ export const pushFiles = async (silent = false) => {
 
     if (toPush.length > 0) {
       const orderedFiles = getOrderedProjectFiles(toPush, filePushOrder);
-      const files = getAppsScriptFilesFromProjectFiles(orderedFiles, rootDir ?? path.join('.', '/'));
+      const files = await getAppsScriptFilesFromProjectFiles(orderedFiles, rootDir ?? path.join('.', '/'));
       const filenames = orderedFiles.map(file => file.name);
 
       // Start pushing.

From 07d37185021f700e3ba5d9a242a8b561e533292f Mon Sep 17 00:00:00 2001
From: Steven Bazyl <sbazyl@google.com>
Date: Tue, 3 Aug 2021 15:35:53 -0600
Subject: [PATCH 2/5] fix: Shut down server faster during logins. Avoids long
 pause after login completes.

---
 package-lock.json | 30 ++++++++++++++++++++++++++++++
 package.json      |  2 ++
 src/auth.ts       |  4 +++-
 3 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/package-lock.json b/package-lock.json
index 288f4374..f38ab7ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
         "p-map": "^5.0.0",
         "read-pkg-up": "^8.0.0",
         "recursive-readdir": "^2.2.2",
+        "server-destroy": "^1.0.1",
         "split-lines": "^3.0.0",
         "strip-bom": "^5.0.0",
         "ts2gas": "^4.0.0",
@@ -49,6 +50,7 @@
         "@types/mocha": "^8.2.2",
         "@types/node": "^12.20.15",
         "@types/recursive-readdir": "^2.2.0",
+        "@types/server-destroy": "^1.0.1",
         "@types/tmp": "^0.2.0",
         "@types/wtfnode": "^0.7.0",
         "chai": "^4.3.4",
@@ -710,6 +712,15 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/server-destroy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@types/server-destroy/-/server-destroy-1.0.1.tgz",
+      "integrity": "sha512-77QGr7waZbE0Y0uF+G+uH3H3SmhyA78Jf2r5r7QSrpg0U3kSXduWpGjzP9PvPLR/KCy+kHjjpnugRHsYTnHopg==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/through": {
       "version": "0.0.30",
       "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz",
@@ -6246,6 +6257,11 @@
         "randombytes": "^2.1.0"
       }
     },
+    "node_modules/server-destroy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+      "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0="
+    },
     "node_modules/set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -7703,6 +7719,15 @@
         "@types/node": "*"
       }
     },
+    "@types/server-destroy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@types/server-destroy/-/server-destroy-1.0.1.tgz",
+      "integrity": "sha512-77QGr7waZbE0Y0uF+G+uH3H3SmhyA78Jf2r5r7QSrpg0U3kSXduWpGjzP9PvPLR/KCy+kHjjpnugRHsYTnHopg==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/through": {
       "version": "0.0.30",
       "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz",
@@ -11731,6 +11756,11 @@
         "randombytes": "^2.1.0"
       }
     },
+    "server-destroy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+      "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0="
+    },
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
diff --git a/package.json b/package.json
index e05c9278..db00874f 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
     "p-map": "^5.0.0",
     "read-pkg-up": "^8.0.0",
     "recursive-readdir": "^2.2.2",
+    "server-destroy": "^1.0.1",
     "split-lines": "^3.0.0",
     "strip-bom": "^5.0.0",
     "ts2gas": "^4.0.0",
@@ -97,6 +98,7 @@
     "@types/mocha": "^8.2.2",
     "@types/node": "^12.20.15",
     "@types/recursive-readdir": "^2.2.0",
+    "@types/server-destroy": "^1.0.1",
     "@types/tmp": "^0.2.0",
     "@types/wtfnode": "^0.7.0",
     "chai": "^4.3.4",
diff --git a/src/auth.ts b/src/auth.ts
index 0d32c2b6..7c7aea14 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -17,6 +17,7 @@ import type {ReadonlyDeep} from 'type-fest';
 
 import type {ClaspToken} from './dotfile';
 import type {ClaspCredentials} from './utils';
+import enableDestroy from 'server-destroy';
 
 /**
  * Authentication with Google's APIs.
@@ -217,6 +218,7 @@ const authorizeWithLocalhost = async (
   // the server port needed to set up the Oauth2Client.
   const server = await new Promise<Server>(resolve => {
     const s = createServer();
+    enableDestroy(s);
     s.listen(0, () => resolve(s));
   });
   const {port} = server.address() as AddressInfo;
@@ -240,7 +242,7 @@ const authorizeWithLocalhost = async (
     console.log(LOG.AUTHORIZE(authUrl));
     (async () => await open(authUrl))();
   });
-  server.close();
+  server.destroy();
 
   return (await client.getToken(authCode)).tokens;
 };

From 3dc2ff6e8f03f7677283b28cd91cfea8f54cf7cd Mon Sep 17 00:00:00 2001
From: Steven Bazyl <sbazyl@google.com>
Date: Mon, 9 Aug 2021 13:20:43 -0600
Subject: [PATCH 3/5] Rework config paths -- removes PathProxy, fixes a few
 issues with CLI overrides not being honored.

---
 package-lock.json      |   1 +
 package.json           |   1 +
 src/apiutils.ts        |   2 +-
 src/commands/clone.ts  |  13 ++--
 src/commands/create.ts |  16 +++--
 src/commands/logout.ts |  36 ++--------
 src/commands/logs.ts   |   8 +--
 src/commands/open.ts   |   4 +-
 src/commands/push.ts   |   7 +-
 src/conf.ts            | 155 +++++++++++++++++++++--------------------
 src/dotfile.ts         |  13 ++--
 src/files.ts           |   8 ++-
 src/index.ts           |  26 ++++---
 src/manifest.ts        |   8 +--
 src/messages.ts        |  26 +++----
 src/path-proxy.ts      | 131 ----------------------------------
 src/utils.ts           |  25 ++-----
 test/test.ts           |   6 +-
 18 files changed, 174 insertions(+), 312 deletions(-)
 delete mode 100644 src/path-proxy.ts

diff --git a/package-lock.json b/package-lock.json
index f38ab7ec..73e38ad7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "commander": "^7.2.0",
         "debounce": "^1.2.1",
         "dotf": "^2.0.0",
+        "find-up": "^5.0.0",
         "fs-extra": "^10.0.0",
         "fuzzy": "^0.1.3",
         "google-auth-library": "^7.1.2",
diff --git a/package.json b/package.json
index db00874f..93e70c44 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,7 @@
     "commander": "^7.2.0",
     "debounce": "^1.2.1",
     "dotf": "^2.0.0",
+    "find-up": "^5.0.0",
     "fs-extra": "^10.0.0",
     "fuzzy": "^0.1.3",
     "google-auth-library": "^7.1.2",
diff --git a/src/apiutils.ts b/src/apiutils.ts
index 09acd9c3..84d97362 100644
--- a/src/apiutils.ts
+++ b/src/apiutils.ts
@@ -50,7 +50,7 @@ const getProjectIdOrDie = async (): Promise<string> => {
     return projectId;
   }
 
-  throw new ClaspError(ERROR.NO_GCLOUD_PROJECT);
+  throw new ClaspError(ERROR.NO_GCLOUD_PROJECT());
 };
 
 // /**
diff --git a/src/commands/clone.ts b/src/commands/clone.ts
index 7049f046..5cbcd33f 100644
--- a/src/commands/clone.ts
+++ b/src/commands/clone.ts
@@ -8,6 +8,9 @@ import {ERROR, LOG} from '../messages.js';
 import {extractScriptId} from '../urls.js';
 import {checkIfOnlineOrDie, saveProject, spinner} from '../utils.js';
 import status from './status.js';
+import {Conf} from '../conf.js';
+
+const config = Conf.get();
 
 interface CommandOption {
   readonly rootDir: string;
@@ -26,18 +29,20 @@ export default async (
   versionNumber: number | undefined,
   options: CommandOption
 ): Promise<void> => {
+  if (options.rootDir) {
+    config.projectRootDirectory = options.rootDir;
+  }
   await checkIfOnlineOrDie();
   if (hasProject()) {
-    throw new ClaspError(ERROR.FOLDER_EXISTS);
+    throw new ClaspError(ERROR.FOLDER_EXISTS());
   }
 
   const id = scriptId ? extractScriptId(scriptId) : await getScriptId();
 
   spinner.start(LOG.CLONING);
 
-  const {rootDir} = options;
-  await saveProject({scriptId: id, rootDir}, false);
-  await writeProjectFiles(await fetchProject(id, versionNumber), rootDir);
+  await saveProject({scriptId: id, rootDir: config.projectRootDirectory}, false);
+  await writeProjectFiles(await fetchProject(id, versionNumber), config.projectRootDirectory);
   await status();
 };
 
diff --git a/src/commands/create.ts b/src/commands/create.ts
index 00e5c543..b773a365 100644
--- a/src/commands/create.ts
+++ b/src/commands/create.ts
@@ -13,6 +13,9 @@ import {
   spinner,
   stopSpinner,
 } from '../utils.js';
+import {Conf} from '../conf.js';
+
+const config = Conf.get();
 
 interface CommandOption {
   readonly parentId?: string;
@@ -30,10 +33,14 @@ interface CommandOption {
  *                        If not specified, clasp will default to the current directory.
  */
 export default async (options: CommandOption): Promise<void> => {
+  if (options.rootDir) {
+    config.projectRootDirectory = options.rootDir;
+  }
+
   // Handle common errors.
   await checkIfOnlineOrDie();
   if (hasProject()) {
-    throw new ClaspError(ERROR.FOLDER_EXISTS);
+    throw new ClaspError(ERROR.FOLDER_EXISTS());
   }
 
   await loadAPICredentials();
@@ -98,10 +105,9 @@ export default async (options: CommandOption): Promise<void> => {
 
   const scriptId = data.scriptId ?? '';
   console.log(LOG.CREATE_PROJECT_FINISH(filetype, scriptId));
-  const {rootDir} = options;
-  await saveProject({scriptId, rootDir, parentId: parentId ? [parentId] : undefined}, false);
+  await saveProject({scriptId, rootDir: config.projectRootDirectory, parentId: parentId ? [parentId] : undefined}, false);
 
-  if (!manifestExists(rootDir)) {
-    await writeProjectFiles(await fetchProject(scriptId), rootDir); // Fetches appsscript.json, o.w. `push` breaks
+  if (!manifestExists(config.projectRootDirectory)) {
+    await writeProjectFiles(await fetchProject(scriptId), config.projectRootDirectory); // Fetches appsscript.json, o.w. `push` breaks
   }
 };
diff --git a/src/commands/logout.ts b/src/commands/logout.ts
index ba3cbad3..1f0c0d6f 100644
--- a/src/commands/logout.ts
+++ b/src/commands/logout.ts
@@ -1,40 +1,16 @@
 import {Conf} from '../conf.js';
-import {DOTFILE} from '../dotfile.js';
-import {hasOauthClientSettings} from '../utils.js';
+import fs from 'fs';
 
-const {auth} = Conf.get();
+const config = Conf.get();
 
 /**
  * Logs out the user by deleting credentials.
  */
 export default async (): Promise<void> => {
-  let previousPath: string | undefined;
-
-  if (hasOauthClientSettings(true)) {
-    if (auth.isDefault()) {
-      // If no local auth defined, try current directory
-      previousPath = auth.path;
-      auth.path = '.';
-    }
-
-    await DOTFILE.AUTH().delete();
-
-    if (previousPath) {
-      auth.path = previousPath;
-    }
+  if (config.auth && fs.existsSync(config.auth)) {
+    fs.unlinkSync(config.auth);
   }
-
-  if (hasOauthClientSettings()) {
-    if (!auth.isDefault()) {
-      // If local auth defined, try with default (global)
-      previousPath = auth.path;
-      auth.path = '';
-    }
-
-    await DOTFILE.AUTH().delete();
-
-    if (previousPath) {
-      auth.path = previousPath;
-    }
+  if (config.authLocal && fs.existsSync(config.authLocal)) {
+    fs.unlinkSync(config.authLocal);
   }
 };
diff --git a/src/commands/logs.ts b/src/commands/logs.ts
index 16735c37..d9bb681c 100644
--- a/src/commands/logs.ts
+++ b/src/commands/logs.ts
@@ -153,12 +153,12 @@ const setupLogs = async (projectSettings: ProjectSettings): Promise<string> => {
 
     const dotfile = DOTFILE.PROJECT();
     if (!dotfile) {
-      throw new ClaspError(ERROR.SETTINGS_DNE);
+      throw new ClaspError(ERROR.SETTINGS_DNE());
     }
 
     const settings = await dotfile.read<ProjectSettings>();
     if (!settings.scriptId) {
-      throw new ClaspError(ERROR.SCRIPT_ID_DNE);
+      throw new ClaspError(ERROR.SCRIPT_ID_DNE());
     }
 
     const {projectId} = await projectIdPrompt();
@@ -186,7 +186,7 @@ const fetchAndPrintLogs = async (
 ): Promise<void> => {
   // Validate projectId
   if (!projectId) {
-    throw new ClaspError(ERROR.NO_GCLOUD_PROJECT);
+    throw new ClaspError(ERROR.NO_GCLOUD_PROJECT());
   }
 
   if (!isValidProjectId(projectId)) {
@@ -195,7 +195,7 @@ const fetchAndPrintLogs = async (
 
   const {isLocalCreds} = await loadAPICredentials();
 
-  spinner.start(`${isLocalCreds ? LOG.LOCAL_CREDS : ''}${LOG.GRAB_LOGS}`);
+  spinner.start(`${isLocalCreds ? LOG.LOCAL_CREDS() : ''}${LOG.GRAB_LOGS}`);
 
   // Create a time filter (timestamp >= "2016-11-29T23:00:00Z")
   // https://cloud.google.com/logging/docs/view/advanced-filters#search-by-time
diff --git a/src/commands/open.ts b/src/commands/open.ts
index b8d94ce9..3d4c20c4 100644
--- a/src/commands/open.ts
+++ b/src/commands/open.ts
@@ -42,7 +42,7 @@ export default async (scriptId: string, options: CommandOption): Promise<void> =
   if (options.creds) {
     const {projectId} = projectSettings;
     if (!projectId) {
-      throw new ClaspError(ERROR.NO_GCLOUD_PROJECT);
+      throw new ClaspError(ERROR.NO_GCLOUD_PROJECT());
     }
 
     console.log(LOG.OPEN_CREDS(projectId));
@@ -69,7 +69,7 @@ export default async (scriptId: string, options: CommandOption): Promise<void> =
 const openAddon = async (projectSettings: ProjectSettings) => {
   const {parentId: parentIdList = []} = projectSettings;
   if (parentIdList.length === 0) {
-    throw new ClaspError(ERROR.NO_PARENT_ID);
+    throw new ClaspError(ERROR.NO_PARENT_ID());
   }
 
   if (parentIdList.length > 1) {
diff --git a/src/commands/push.ts b/src/commands/push.ts
index 65a9bafe..2999394c 100644
--- a/src/commands/push.ts
+++ b/src/commands/push.ts
@@ -19,9 +19,10 @@ import type {ProjectSettings} from '../dotfile';
 
 const {debounce} = debouncePkg;
 const {readFileSync} = fs;
-const {project} = Conf.get();
 const WATCH_DEBOUNCE_MS = 1000;
 
+const config = Conf.get();
+
 interface CommandOption {
   readonly watch?: boolean;
   readonly force?: boolean;
@@ -88,8 +89,8 @@ const confirmManifestUpdate = async (): Promise<boolean> => (await overwriteProm
  * @returns {Promise<boolean>}
  */
 const manifestHasChanges = async (projectSettings: ProjectSettings): Promise<boolean> => {
-  const {scriptId, rootDir = project.resolvedDir} = projectSettings;
-  const localManifest = readFileSync(path.join(rootDir, PROJECT_MANIFEST_FILENAME), FS_OPTIONS);
+  const {scriptId, rootDir = config.projectRootDirectory} = projectSettings;
+  const localManifest = readFileSync(path.join(rootDir!, PROJECT_MANIFEST_FILENAME), FS_OPTIONS);
   const remoteFiles = await fetchProject(scriptId, undefined, true);
   const remoteManifest = remoteFiles.find(file => file.name === PROJECT_MANIFEST_BASENAME);
   if (remoteManifest) {
diff --git a/src/conf.ts b/src/conf.ts
index c95b87ef..db324a8f 100644
--- a/src/conf.ts
+++ b/src/conf.ts
@@ -2,7 +2,7 @@ import os from 'os';
 import path from 'path';
 
 import {PROJECT_NAME} from './constants.js';
-import {PathProxy} from './path-proxy.js';
+import findUp from 'find-up';
 
 /**
  * supported environment variables
@@ -18,62 +18,97 @@ enum ENV {
 /**
  * A Singleton class to hold configuration related objects.
  * Use the `get()` method to access the unique singleton instance.
+ *
+ * Resolution order for paths is:
+ * - Explicitly set paths (via CLI option)
+ * - Env var
+ * - Well-known location
+ *
+ *
  */
 export class Conf {
+  private _root: string | undefined;
+  private _projectConfig: string | undefined;
+  private _ignore: string | undefined;
+  private _auth: string | undefined;
+  private _authLocal: string | undefined;
+
   private static _instance: Conf;
-  /**
-   * This dotfile saves clasp project information, local to project directory.
-   */
-  readonly project: PathProxy;
-  /**
-   * This dotfile stores information about ignoring files on `push`. Like .gitignore.
-   */
-  readonly ignore: IgnoreFile;
-  /**
-   * This dotfile saves auth information. Should never be committed.
-   * There are 2 types: personal & global:
-   * - Global: In the $HOME directory.
-   * - Personal: In the local directory.
-   * @see {ClaspToken}
-   */
-  readonly auth: AuthFile;
-  // Local auth for backwards compatibility
-  readonly authLocal: AuthFile;
-  // readonly manifest: PathProxy;
 
   /**
    * Private to prevent direct construction calls with the `new` operator.
    */
-  private constructor() {
-    /**
-     * Helper to set the PathProxy path if an environment variables is set.
-     *
-     * *Note: Empty values (i.e. '') are not accounted for.*
-     */
-    const setPathWithEnvVar = (varName: string, file: PathProxy) => {
-      const envVar = process.env[varName];
-      if (envVar) {
-        file.path = envVar;
+  private constructor() {}
+
+  set projectRootDirectory(path: string | undefined) {
+    this._root = path;
+    this._projectConfig = undefined; // Force recalculation of path if root chanaged
+  }
+
+  get projectRootDirectory() {
+    if (this._root === undefined) {
+      const configPath = findUp.sync(`.${PROJECT_NAME}.json`);
+      if (configPath !== undefined) {
+        this._root = path.dirname(configPath);
+      } else {
+        this._root = process.cwd();
       }
-    };
+    }
+    return this._root;
+  }
+
+  set projectConfig(filePath: string | undefined) {
+    this._projectConfig = filePath;
+    if (filePath) {
+      this._root = path.dirname(filePath); // Root dir must be same dir as config
+    }
+  }
+
+  get projectConfig() {
+    if (this._projectConfig === undefined && this.projectRootDirectory) {
+      this._projectConfig = this.buildPathOrUseEnv(`.${PROJECT_NAME}.json`, this.projectRootDirectory, ENV.DOT_CLASP_PROJECT);
+    }
+    return this._projectConfig;
+  }
+
+  set ignore(path: string | undefined) {
+    this._ignore = path;
+  }
+
+  get ignore() {
+    if (this._ignore === undefined && this.projectRootDirectory) {
+      this._ignore = this.buildPathOrUseEnv(`.${PROJECT_NAME}ignore`, this.projectRootDirectory, ENV.DOT_CLASP_IGNORE);
+    }
+    return this._ignore;
+  }
 
-    // default `project` path is `./.clasp.json`
-    this.project = new PathProxy({dir: '.', base: `.${PROJECT_NAME}.json`});
+  set auth(path: string | undefined) {
+    this._auth = path;
+  }
+
+  get auth() {
+    if (this._auth === undefined) {
+      this._auth = this.buildPathOrUseEnv(`.${PROJECT_NAME}rc.json`, os.homedir(), ENV.DOT_CLASP_AUTH);
+    }
+    return this._auth;
+  }
 
-    // default `ignore` path is `~/.claspignore`
-    // IgnoreFile class implements custom `.resolve()` rules
-    this.ignore = new IgnoreFile({dir: os.homedir(), base: `.${PROJECT_NAME}ignore`});
+  set authLocal(path: string | undefined) {
+    this._authLocal = path;
+  }
 
-    // default `auth` path is `~/.clasprc.json`
-    // default local auth path is './.clasprc.json'
-    this.auth = new AuthFile({dir: os.homedir(), base: `.${PROJECT_NAME}rc.json`});
-    this.authLocal = new AuthFile({dir: '.', base: `.${PROJECT_NAME}rc.json`});
+  get authLocal() {
+    if (this._authLocal === undefined && this.projectRootDirectory) {
+      this._authLocal = this.buildPathOrUseEnv(`.${PROJECT_NAME}rc.json`, this.projectRootDirectory, ENV.DOT_CLASP_AUTH);
+    }
+    return this._authLocal;
+  }
 
-    // resolve environment variables
-    setPathWithEnvVar(ENV.DOT_CLASP_PROJECT, this.project);
-    setPathWithEnvVar(ENV.DOT_CLASP_IGNORE, this.ignore);
-    setPathWithEnvVar(ENV.DOT_CLASP_AUTH, this.auth);
-    setPathWithEnvVar(ENV.DOT_CLASP_AUTH, this.authLocal);
+  private buildPathOrUseEnv(filename: string, root: string, envName?: string): string {
+    if (envName && process.env[envName] !== undefined) {
+      return process.env[envName]!;
+    }
+    return path.join(root, filename);
   }
 
   /**
@@ -89,33 +124,3 @@ export class Conf {
     return Conf._instance;
   }
 }
-
-class AuthFile extends PathProxy {
-  /**
-   * Rules to resolves path:
-   *
-   * - if default path, use as is
-   * - otherwise use super.resolve()
-   *
-   * @returns {string}
-   */
-  resolve(): string {
-    return this.isDefault() ? path.join(this._default.dir, this._default.base) : super.resolve();
-  }
-}
-
-class IgnoreFile extends PathProxy {
-  /**
-   * Rules to resolves path:
-   *
-   * - if default, use the **project** directory and the default base filename
-   * - otherwise use super.resolve()
-   *
-   * @returns {string}
-   */
-  resolve(): string {
-    return this.isDefault() ? path.join(Conf.get().project.resolvedDir, this._default.base) : super.resolve();
-  }
-}
-
-// TODO: add more subclasses if necessary
diff --git a/src/dotfile.ts b/src/dotfile.ts
index 2f45b91a..0a1d8158 100644
--- a/src/dotfile.ts
+++ b/src/dotfile.ts
@@ -24,7 +24,7 @@ import type {Credentials, OAuth2ClientOptions} from 'google-auth-library';
 
 export type {Dotfile} from 'dotf';
 
-const {auth, authLocal, ignore, project} = Conf.get();
+const config = Conf.get();
 
 // Project settings file (Saved in .clasp.json)
 export interface ProjectSettings {
@@ -58,8 +58,9 @@ export const DOTFILE = {
    * @return {Promise<string[]>} A list of file glob patterns
    */
   IGNORE: async () => {
-    const ignorePath = ignore.resolve();
-    const content = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, FS_OPTIONS) : defaultClaspignore;
+    const ignorePath = config.ignore;
+    const content =
+      ignorePath && fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, FS_OPTIONS) : defaultClaspignore;
 
     return splitLines(stripBom(content)).filter((name: string) => name.length > 0);
   },
@@ -70,7 +71,7 @@ export const DOTFILE = {
    */
   PROJECT: () => {
     // ! TODO: currently limited if filename doesn't start with a dot '.'
-    const {dir, base} = path.parse(project.resolve());
+    const {dir, base} = path.parse(config.projectConfig!);
     if (base[0] === '.') {
       return dotf(dir || '.', base.slice(1));
     }
@@ -78,9 +79,9 @@ export const DOTFILE = {
   },
   // Stores {ClaspCredentials}
   AUTH: (local?: boolean) => {
-    const configPath = local ? authLocal : auth;
+    const configPath = local ? config.authLocal : config.auth;
     // ! TODO: currently limited if filename doesn't start with a dot '.'
-    const {dir, base} = path.parse(configPath.resolve());
+    const {dir, base} = path.parse(configPath!);
     if (base[0] === '.') {
       return dotf(dir || '.', base.slice(1));
     }
diff --git a/src/files.ts b/src/files.ts
index faf6a00a..db723930 100644
--- a/src/files.ts
+++ b/src/files.ts
@@ -24,7 +24,7 @@ import {
 import type {TranspileOptions} from 'typescript';
 
 const {parseConfigFileTextToJson} = typescript;
-const {project} = Conf.get();
+const config = Conf.get();
 
 // An Apps Script API File
 interface AppsScriptFile {
@@ -207,14 +207,16 @@ export const getLocalFileType = (type: string, fileExtension?: string): string =
  * Returns true if the user has a clasp project.
  * @returns {boolean} If .clasp.json exists.
  */
-export const hasProject = (): boolean => fs.existsSync(project.resolve());
+export const hasProject = (): boolean => {
+  return config.projectConfig !== undefined && fs.existsSync(config.projectConfig);
+};
 
 /**
  * Returns in tsconfig.json.
  * @returns {TranspileOptions} if tsconfig.json not exists, return an empty object.
  */
 const getTranspileOptions = (): TranspileOptions => {
-  const tsconfigPath = path.join(project.resolvedDir, 'tsconfig.json');
+  const tsconfigPath = path.join(config.projectRootDirectory!, 'tsconfig.json');
 
   return fs.existsSync(tsconfigPath)
     ? {
diff --git a/src/index.ts b/src/index.ts
index 4279b122..f772b152 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -52,13 +52,14 @@ import versions from './commands/versions.js';
 import {Conf} from './conf.js';
 import {PROJECT_NAME} from './constants.js';
 import {spinner, stopSpinner} from './utils.js';
+import fs from "fs";
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
 
 let beforeExit = () => {};
 
 // instantiate the config singleton (and loads environment variables as a side effect)
-const {auth, ignore, project} = Conf.get();
+const config = Conf.get();
 
 // Ensure any unhandled exception won't go unnoticed
 loudRejection();
@@ -83,8 +84,8 @@ commander.name(PROJECT_NAME).usage('<command> [options]').description(`${PROJECT
  */
 commander
   .option('-A, --auth <file>', "path to an auth file or a folder with a '.clasprc.json' file.")
-  .on('option:auth', () => {
-    auth.path = commander['auth'];
+  .on('option:auth', (auth) => {
+    config.auth = auth;
   });
 
 /**
@@ -92,8 +93,8 @@ commander
  */
 commander
   .option('-I, --ignore <file>', "path to an ignore file or a folder with a '.claspignore' file.")
-  .on('option:ignore', () => {
-    ignore.path = commander['ignore'];
+  .on('option:ignore', (ignore) => {
+    config.ignore = ignore;
   });
 
 /**
@@ -101,8 +102,13 @@ commander
  */
 commander
   .option('-P, --project <file>', "path to a project file or to a folder with a '.clasp.json' file.")
-  .on('option:project', () => {
-    project.path = commander['project'];
+  .on('option:project', (path) => {
+    const stats = fs.lstatSync(path);
+    if (stats.isDirectory()) {
+      config.projectRootDirectory = path;
+    } else {
+      config.projectConfig = path;
+    }
   });
 
 /**
@@ -389,9 +395,9 @@ commander
   .command('paths')
   .description('List current config files path')
   .action(() => {
-    console.log('project', project.path, project.isDefault(), project.resolve());
-    console.log('ignore', ignore.path, ignore.isDefault(), ignore.resolve());
-    console.log('auth', auth.path, auth.isDefault(), auth.resolve());
+    console.log('project', config.projectConfig);
+    console.log('ignore', config.ignore);
+    console.log('auth', config.auth);
   });
 
 const [_bin, _sourcePath, ...args] = process.argv;
diff --git a/src/manifest.ts b/src/manifest.ts
index 8be6a6a2..c758bc10 100644
--- a/src/manifest.ts
+++ b/src/manifest.ts
@@ -11,14 +11,14 @@ import {getProjectSettings, parseJsonOrDie} from './utils.js';
 
 import type {AdvancedService} from './apis';
 
-const {project} = Conf.get();
+const config = Conf.get();
 
 /*** Gets the path to manifest for given `rootDir` */
 const getManifestPath = (rootDir: string): string => path.join(rootDir, PROJECT_MANIFEST_FILENAME);
 
 /** Gets the `rootDir` from given project */
 const getRootDir = (projectSettings: ProjectSettings): string =>
-  typeof projectSettings.rootDir === 'string' ? projectSettings.rootDir : project.resolvedDir;
+  typeof projectSettings.rootDir === 'string' ? projectSettings.rootDir : config.projectRootDirectory!;
 
 /**
  * Checks if the rootDir appears to be a valid project.
@@ -27,8 +27,8 @@ const getRootDir = (projectSettings: ProjectSettings): string =>
  *
  * @return {boolean} True if valid project, false otherwise
  */
-export const manifestExists = (rootDir: string = project.resolvedDir): boolean =>
-  fs.existsSync(getManifestPath(rootDir));
+export const manifestExists = (rootDir = config.projectRootDirectory): boolean =>
+  rootDir !== undefined && fs.existsSync(getManifestPath(rootDir));
 
 /**
  * Reads the appsscript.json manifest file.
diff --git a/src/messages.ts b/src/messages.ts
index 80c65a8d..0074cf1d 100644
--- a/src/messages.ts
+++ b/src/messages.ts
@@ -4,7 +4,7 @@ import {Conf} from './conf.js';
 import {PROJECT_MANIFEST_FILENAME, PROJECT_NAME} from './constants.js';
 import {URL} from './urls.js';
 
-const {auth, ignore, project} = Conf.get();
+const config = Conf.get();
 
 /** Human friendly Google Drive file type name */
 const fileTypeName = new Map<string, string>([
@@ -44,7 +44,7 @@ Forgot ${PROJECT_NAME} commands? Get help:\n  ${PROJECT_NAME} --help`,
   DEPLOYMENT_COUNT: 'Unable to deploy; Scripts may only have up to 20 versioned deployments at a time.',
   DRIVE: 'Something went wrong with the Google Drive API',
   EXECUTE_ENTITY_NOT_FOUND: 'Script API executable not published/deployed.',
-  FOLDER_EXISTS: `Project file (${project.resolve()}) already exists.`,
+  FOLDER_EXISTS: () => `Project file (${config.projectConfig}) already exists.`,
   FS_DIR_WRITE: 'Could not create directory.',
   FS_FILE_WRITE: 'Could not write file.',
   INVALID_JSON: 'Input params not Valid JSON string. Please fix and try again',
@@ -58,8 +58,8 @@ Forgot ${PROJECT_NAME} commands? Get help:\n  ${PROJECT_NAME} --help`,
   NO_CREDENTIALS: (local: boolean) =>
     `Could not read API credentials. Are you logged in ${local ? 'locally' : 'globally'}?`,
   NO_FUNCTION_NAME: 'N/A',
-  NO_GCLOUD_PROJECT: `No projectId found in your ${project.resolve()} file.`,
-  NO_PARENT_ID: `No parentId or empty parentId found in your ${project.resolve()} file.`,
+  NO_GCLOUD_PROJECT: () => `No projectId found in your ${config.projectConfig} file.`,
+  NO_PARENT_ID: () => `No parentId or empty parentId found in your ${config.projectConfig} file.`,
   NO_LOCAL_CREDENTIALS: `Requires local crendetials:\n\n  ${PROJECT_NAME} login --creds <file.json>`,
   NO_MANIFEST: (filename: string) => `Manifest: ${filename} invalid. \`create\` or \`clone\` a project first.`,
   NO_NESTED_PROJECTS: '\nNested clasp projects are not supported.',
@@ -76,14 +76,14 @@ Forgot ${PROJECT_NAME} commands? Get help:\n  ${PROJECT_NAME} --help`,
   RATE_LIMIT: 'Rate limit exceeded. Check quota.',
   RUN_NODATA: 'Script execution API returned no data.',
   READ_ONLY_DELETE: 'Unable to delete read-only deployment.',
-  SCRIPT_ID_DNE: `No scriptId found in your ${project.resolve()} file.`,
+  SCRIPT_ID_DNE: () => `No scriptId found in your ${config.projectConfig} file.`,
   SCRIPT_ID_INCORRECT: (scriptId: string) => `The scriptId "${scriptId}" looks incorrect.
 Did you provide the correct scriptId?`,
   SCRIPT_ID: `Could not find script.
 Did you provide the correct scriptId?
 Are you logged in to the correct account with the script?`,
-  SETTINGS_DNE: `
-No valid ${project.resolve()} project file. You may need to \`create\` or \`clone\` a project first.`,
+  SETTINGS_DNE: () => `
+No valid ${config.projectConfig} project file. You may need to \`create\` or \`clone\` a project first.`,
   UNAUTHENTICATED_LOCAL: 'Error: Local client credentials unauthenticated. Check scopes/authorization.',
   UNAUTHENTICATED: 'Error: Unauthenticated request: Please try again.',
   UNKNOWN_KEY: (key: string) => `Unknown key "${key}"`,
@@ -102,9 +102,9 @@ export const LOG = {
   AUTH_PAGE_SUCCESSFUL: 'Logged in! You may close this page. ', // HTML Redirect Page
   AUTH_SUCCESSFUL: 'Authorization successful.',
   AUTHORIZE: (authUrl: string) => `🔑 Authorize ${PROJECT_NAME} by visiting this url:\n${authUrl}\n`,
-  CLONE_SUCCESS: (
-    fileCount: number
-  ) => `Warning: files in subfolder are not accounted for unless you set a '${ignore.resolve()}' file.
+  CLONE_SUCCESS: (fileCount: number) => `Warning: files in subfolder are not accounted for unless you set a '${
+    config.ignore
+  }' file.
 Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`,
   CLONING: 'Cloning files…',
   CLONE_SCRIPT_QUESTION: 'Clone which script?',
@@ -129,7 +129,7 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`,
   GET_PROJECT_ID_INSTRUCTIONS: `Go to *Resource > Cloud Platform Project…* and copy your projectId
 (including "project-id-")`,
   GIVE_DESCRIPTION: 'Give a description: ',
-  LOCAL_CREDS: `Using local credentials: ${auth.resolve()} 🔐 `,
+  LOCAL_CREDS: () =>`Using local credentials: ${config.authLocal} 🔐 `,
   LOGIN: (isLocal: boolean) => `Logging in ${isLocal ? 'locally' : 'globally'}…`,
   LOGS_SETUP: 'Finished setting up logs.\n',
   NO_GCLOUD_PROJECT: `No projectId found. Running ${PROJECT_NAME} logs --setup.`,
@@ -148,9 +148,9 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`,
   PUSHING: 'Pushing files…',
   SAVED_CREDS: (isLocalCreds: boolean) =>
     isLocalCreds
-      ? `Local credentials saved to: ${auth.resolve()}.
+      ? `Local credentials saved to: ${config.authLocal}.
 *Be sure to never commit this file!* It's basically a password.`
-      : `Default credentials saved to: ${auth.resolve()}.`,
+      : `Default credentials saved to: ${config.auth}.`,
   SCRIPT_LINK: (scriptId: string) => `https://script.google.com/d/${scriptId}/edit`,
   // SCRIPT_RUN: (functionName: string) => `Executing: ${functionName}`,
   STACKDRIVER_SETUP: 'Setting up StackDriver Logging.',
diff --git a/src/path-proxy.ts b/src/path-proxy.ts
deleted file mode 100644
index 196ea020..00000000
--- a/src/path-proxy.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import fs from 'fs-extra';
-import path from 'path';
-
-import {ClaspError} from './clasp-error.js';
-
-/** A path broken down into a `dir`ectory and a `base` filename */
-type BrokenPath = Pick<path.ParsedPath, 'dir' | 'base'>;
-
-export class PathProxy {
-  protected _default: BrokenPath;
-  protected _userDefined: string | undefined;
-
-  /**
-   * Handles a path to a file.
-   *
-   * - Constructor requires a default path (directory and filename)
-   * - Path can be overridden with the `path` accessor.
-   * - The `resolve()` method implements specific rules to define the effective path to the proxied file.
-   *
-   * @param {BrokenPath} defaultPath default path
-   */
-  constructor(defaultPath: BrokenPath) {
-    this._default = defaultPath;
-  }
-
-  /**
-   * Returns the current (raw and unresolved) defined path to the proxied file.
-   *
-   * *Note: for most uses, prefer the `resolve()` method in order to retreive a file's path*
-   *
-   * @returns {string}
-   */
-  get path(): string {
-    return this._userDefined ?? path.join(this._default.dir, this._default.base);
-  }
-
-  /**
-   * Sets the current (raw and unresolved) path to the proxied file.
-   *
-   * *Note: passing an empty string restores the default path*
-   */
-  set path(userDefined: string) {
-    this._userDefined = userDefined === path.join(this._default.dir, this._default.base) ? undefined : userDefined;
-  }
-
-  /**
-   * Returns true if current path is the default.
-   *
-   * @returns {boolean}
-   */
-  isDefault(): boolean {
-    return !this._userDefined || this._userDefined === path.join(this._default.dir, this._default.base);
-  }
-
-  /**
-   * Returns the resolved directory to the proxied file.
-   *
-   * *Note: for most uses, prefer the `.resolve()` method in order to retreive a file's path*
-   *
-   * @returns {string}
-   */
-  get resolvedDir(): string {
-    return path.dirname(this.resolve());
-  }
-
-  /**
-   * Resolves the current active path
-   *
-   * @returns {string}
-   */
-  resolve(): string {
-    return this._userDefined
-      ? resolvePath(this._userDefined, this._default.base)
-      : path.join(this._default.dir, this._default.base);
-  }
-}
-
-/**
- * Attempts to resolve a path with the following rules:
- *
- * - if path exists and points to a file: use it as is
- * - if path exists and points to a directory: append the default base filename to the path
- * - if path partially resolves to an existing directory but base filename does not exists: use it as is
- * - otherwise throw an error
- *
- * @param {string} pathToResolve the path to resolve
- * @param {string} baseFilename the default base filename
- *
- * @returns {string}
- */
-const resolvePath = (pathToResolve: string, baseFilename: string) => {
-  if (fs.existsSync(pathToResolve)) {
-    return appendBaseIfIsDirectory(pathToResolve, baseFilename);
-  }
-
-  const parsedPath = path.parse(pathToResolve);
-
-  if (parsedPath.dir === '' || fs.lstatSync(parsedPath.dir).isDirectory()) {
-    return pathToResolve; // Assume fullpath to missing file
-  }
-
-  // TODO: improve support for unresolved paths
-  throw new ClaspError(`Unrecognized path ${pathToResolve}`);
-};
-
-/**
- * Attempts to resolve an **existing** path using the following rules:
- *
- * - if path exists and points to a file: use it as is
- * - if path exists and points to a directory: append the default base filename to the path
- * - otherwise throw an error
- *
- * @param {string} somePath the path to resolve
- * @param {string} baseFilename the default base filename
- *
- * @returns {string}
- */
-const appendBaseIfIsDirectory = (somePath: string, baseFilename: string): string => {
-  const stats = fs.lstatSync(somePath);
-
-  if (stats.isFile()) {
-    return somePath;
-  }
-
-  if (stats.isDirectory()) {
-    return path.join(somePath, baseFilename);
-  }
-
-  // TODO: improve support for other stats types (stats.isSymbolicLink() ? )
-  throw new ClaspError(`Unrecognized path ${somePath}`);
-};
diff --git a/src/utils.ts b/src/utils.ts
index 1ec6ed76..2ff543c3 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -15,7 +15,7 @@ import {ERROR, LOG} from './messages.js';
 
 import type {ClaspToken, ProjectSettings} from './dotfile';
 
-const {auth} = Conf.get();
+const config = Conf.get();
 
 /**
  * Returns input string with uppercased first character
@@ -48,21 +48,10 @@ export interface ClaspCredentials {
  * @return {boolean}
  */
 export const hasOauthClientSettings = (local = false): boolean => {
-  let previousPath: string | undefined;
-
-  if (local && auth.isDefault()) {
-    // if no local auth defined, try current directory
-    previousPath = auth.path;
-    auth.path = '.';
+  if (local) {
+    return config.authLocal !== undefined && fs.existsSync(config.authLocal);
   }
-
-  const result = (local ? !auth.isDefault() : auth.isDefault()) && fs.existsSync(auth.resolve());
-
-  if (previousPath) {
-    auth.path = previousPath;
-  }
-
-  return result;
+  return config.auth !== undefined && fs.existsSync(config.auth);
 };
 
 /**
@@ -135,7 +124,7 @@ export const getWebApplicationURL = (value: Readonly<scriptV1.Schema$Deployment>
  * Gets default project name.
  * @return {string} default project name.
  */
-export const getDefaultProjectName = (): string => capitalize(path.basename(process.cwd()));
+export const getDefaultProjectName = (): string => capitalize(path.basename(config.projectRootDirectory!));
 
 /**
  * Gets the project settings from the project dotfile.
@@ -159,11 +148,11 @@ export const getProjectSettings = async (): Promise<ProjectSettings> => {
           return settings;
         }
       } catch (error) {
-        throw new ClaspError(ERROR.SETTINGS_DNE); // Never found a dotfile
+        throw new ClaspError(ERROR.SETTINGS_DNE()); // Never found a dotfile
       }
     }
 
-    throw new ClaspError(ERROR.SETTINGS_DNE); // Never found a dotfile
+    throw new ClaspError(ERROR.SETTINGS_DNE()); // Never found a dotfile
   } catch (error) {
     if (error instanceof ClaspError) {
       throw error;
diff --git a/test/test.ts b/test/test.ts
index b5ed84b2..a6f7e135 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -279,20 +279,20 @@ describe('Test all functions while logged out', () => {
     expect(result.status).to.equal(1);
     // Should be ERROR.NO_CREDENTIALS
     // see: https://github.com/google/clasp/issues/278
-    expect(result.stderr).to.contain(ERROR.SETTINGS_DNE);
+    expect(result.stderr).to.contain(ERROR.SETTINGS_DNE());
   });
   it('should fail to open (no .clasp.json file)', () => {
     const result = spawnSync(CLASP, ['open'], {encoding: 'utf8'});
     expect(result.status).to.equal(1);
     // Should be ERROR.NO_CREDENTIALS
     // see: https://github.com/google/clasp/issues/278
-    expect(result.stderr).to.contain(ERROR.SETTINGS_DNE);
+    expect(result.stderr).to.contain(ERROR.SETTINGS_DNE());
   });
   it('should fail to show logs (no .clasp.json file)', () => {
     const result = spawnSync(CLASP, ['logs'], {encoding: 'utf8'});
     expect(result.status).to.equal(1);
     // Should be ERROR.NO_CREDENTIALS
     // see: https://github.com/google/clasp/issues/278
-    expect(result.stderr).to.contain(ERROR.SETTINGS_DNE);
+    expect(result.stderr).to.contain(ERROR.SETTINGS_DNE());
   });
 });

From 37ae4ae1703bad0adac72848788270eca47e9726 Mon Sep 17 00:00:00 2001
From: Steven Bazyl <sbazyl@google.com>
Date: Mon, 9 Aug 2021 13:32:07 -0600
Subject: [PATCH 4/5] Delint/prettify

---
 src/commands/create.ts |  5 ++++-
 src/conf.ts            | 12 ++++++++++--
 src/index.ts           |  8 ++++----
 src/messages.ts        |  2 +-
 4 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/commands/create.ts b/src/commands/create.ts
index b773a365..ef910fed 100644
--- a/src/commands/create.ts
+++ b/src/commands/create.ts
@@ -105,7 +105,10 @@ export default async (options: CommandOption): Promise<void> => {
 
   const scriptId = data.scriptId ?? '';
   console.log(LOG.CREATE_PROJECT_FINISH(filetype, scriptId));
-  await saveProject({scriptId, rootDir: config.projectRootDirectory, parentId: parentId ? [parentId] : undefined}, false);
+  await saveProject(
+    {scriptId, rootDir: config.projectRootDirectory, parentId: parentId ? [parentId] : undefined},
+    false
+  );
 
   if (!manifestExists(config.projectRootDirectory)) {
     await writeProjectFiles(await fetchProject(scriptId), config.projectRootDirectory); // Fetches appsscript.json, o.w. `push` breaks
diff --git a/src/conf.ts b/src/conf.ts
index db324a8f..9620c7bf 100644
--- a/src/conf.ts
+++ b/src/conf.ts
@@ -66,7 +66,11 @@ export class Conf {
 
   get projectConfig() {
     if (this._projectConfig === undefined && this.projectRootDirectory) {
-      this._projectConfig = this.buildPathOrUseEnv(`.${PROJECT_NAME}.json`, this.projectRootDirectory, ENV.DOT_CLASP_PROJECT);
+      this._projectConfig = this.buildPathOrUseEnv(
+        `.${PROJECT_NAME}.json`,
+        this.projectRootDirectory,
+        ENV.DOT_CLASP_PROJECT
+      );
     }
     return this._projectConfig;
   }
@@ -99,7 +103,11 @@ export class Conf {
 
   get authLocal() {
     if (this._authLocal === undefined && this.projectRootDirectory) {
-      this._authLocal = this.buildPathOrUseEnv(`.${PROJECT_NAME}rc.json`, this.projectRootDirectory, ENV.DOT_CLASP_AUTH);
+      this._authLocal = this.buildPathOrUseEnv(
+        `.${PROJECT_NAME}rc.json`,
+        this.projectRootDirectory,
+        ENV.DOT_CLASP_AUTH
+      );
     }
     return this._authLocal;
   }
diff --git a/src/index.ts b/src/index.ts
index f772b152..139201a1 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -52,7 +52,7 @@ import versions from './commands/versions.js';
 import {Conf} from './conf.js';
 import {PROJECT_NAME} from './constants.js';
 import {spinner, stopSpinner} from './utils.js';
-import fs from "fs";
+import fs from 'fs';
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
 
@@ -84,7 +84,7 @@ commander.name(PROJECT_NAME).usage('<command> [options]').description(`${PROJECT
  */
 commander
   .option('-A, --auth <file>', "path to an auth file or a folder with a '.clasprc.json' file.")
-  .on('option:auth', (auth) => {
+  .on('option:auth', auth => {
     config.auth = auth;
   });
 
@@ -93,7 +93,7 @@ commander
  */
 commander
   .option('-I, --ignore <file>', "path to an ignore file or a folder with a '.claspignore' file.")
-  .on('option:ignore', (ignore) => {
+  .on('option:ignore', ignore => {
     config.ignore = ignore;
   });
 
@@ -102,7 +102,7 @@ commander
  */
 commander
   .option('-P, --project <file>', "path to a project file or to a folder with a '.clasp.json' file.")
-  .on('option:project', (path) => {
+  .on('option:project', path => {
     const stats = fs.lstatSync(path);
     if (stats.isDirectory()) {
       config.projectRootDirectory = path;
diff --git a/src/messages.ts b/src/messages.ts
index 0074cf1d..2466bc8c 100644
--- a/src/messages.ts
+++ b/src/messages.ts
@@ -129,7 +129,7 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`,
   GET_PROJECT_ID_INSTRUCTIONS: `Go to *Resource > Cloud Platform Project…* and copy your projectId
 (including "project-id-")`,
   GIVE_DESCRIPTION: 'Give a description: ',
-  LOCAL_CREDS: () =>`Using local credentials: ${config.authLocal} 🔐 `,
+  LOCAL_CREDS: () => `Using local credentials: ${config.authLocal} 🔐 `,
   LOGIN: (isLocal: boolean) => `Logging in ${isLocal ? 'locally' : 'globally'}…`,
   LOGS_SETUP: 'Finished setting up logs.\n',
   NO_GCLOUD_PROJECT: `No projectId found. Running ${PROJECT_NAME} logs --setup.`,

From e4cd9528f5180db8ca02a44706a9a1b62dbaaa36 Mon Sep 17 00:00:00 2001
From: Steven Bazyl <sbazyl@google.com>
Date: Mon, 9 Aug 2021 15:11:07 -0600
Subject: [PATCH 5/5] Standarize using fs-extra instead of fs

---
 src/commands/logout.ts | 2 +-
 src/index.ts           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/commands/logout.ts b/src/commands/logout.ts
index 1f0c0d6f..94887b38 100644
--- a/src/commands/logout.ts
+++ b/src/commands/logout.ts
@@ -1,5 +1,5 @@
 import {Conf} from '../conf.js';
-import fs from 'fs';
+import fs from 'fs-extra';
 
 const config = Conf.get();
 
diff --git a/src/index.ts b/src/index.ts
index 139201a1..2b7488bb 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -52,7 +52,7 @@ import versions from './commands/versions.js';
 import {Conf} from './conf.js';
 import {PROJECT_NAME} from './constants.js';
 import {spinner, stopSpinner} from './utils.js';
-import fs from 'fs';
+import fs from 'fs-extra';
 
 const __dirname = dirname(fileURLToPath(import.meta.url));