@google/clasp 3.0.6-alpha → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +21 -2
  2. package/build/src/auth/auth.js +54 -10
  3. package/build/src/auth/auth_code_flow.js +51 -0
  4. package/build/src/auth/credential_store.js +13 -0
  5. package/build/src/auth/file_credential_store.js +62 -7
  6. package/build/src/auth/localhost_auth_code_flow.js +47 -5
  7. package/build/src/auth/serverless_auth_code_flow.js +35 -0
  8. package/build/src/commands/clone-script.js +37 -5
  9. package/build/src/commands/create-deployment.js +31 -6
  10. package/build/src/commands/create-script.js +65 -24
  11. package/build/src/commands/create-version.js +21 -1
  12. package/build/src/commands/delete-deployment.js +36 -5
  13. package/build/src/commands/delete-script.js +41 -0
  14. package/build/src/commands/disable-api.js +20 -1
  15. package/build/src/commands/enable-api.js +20 -1
  16. package/build/src/commands/list-apis.js +24 -1
  17. package/build/src/commands/list-deployments.js +35 -5
  18. package/build/src/commands/list-scripts.js +26 -2
  19. package/build/src/commands/list-versions.js +35 -7
  20. package/build/src/commands/login.js +36 -10
  21. package/build/src/commands/logout.js +23 -1
  22. package/build/src/commands/open-apis.js +20 -1
  23. package/build/src/commands/open-container.js +20 -1
  24. package/build/src/commands/open-credentials.js +20 -1
  25. package/build/src/commands/open-logs.js +20 -1
  26. package/build/src/commands/open-script.js +20 -1
  27. package/build/src/commands/open-webapp.js +20 -1
  28. package/build/src/commands/program.js +48 -7
  29. package/build/src/commands/pull.js +54 -13
  30. package/build/src/commands/push.js +49 -9
  31. package/build/src/commands/run-function.js +56 -13
  32. package/build/src/commands/setup-logs.js +20 -1
  33. package/build/src/commands/show-authorized-user.js +29 -2
  34. package/build/src/commands/show-file-status.js +17 -2
  35. package/build/src/commands/start-mcp.js +17 -1
  36. package/build/src/commands/tail-logs.js +20 -5
  37. package/build/src/commands/update-deployment.js +32 -6
  38. package/build/src/commands/utils.js +68 -0
  39. package/build/src/constants.js +15 -0
  40. package/build/src/core/apis.js +13 -3
  41. package/build/src/core/clasp.js +71 -12
  42. package/build/src/core/files.js +135 -32
  43. package/build/src/core/functions.js +36 -0
  44. package/build/src/core/logs.js +29 -0
  45. package/build/src/core/manifest.js +13 -0
  46. package/build/src/core/project.js +154 -7
  47. package/build/src/core/services.js +105 -16
  48. package/build/src/core/utils.js +57 -1
  49. package/build/src/experiments.js +23 -0
  50. package/build/src/index.js +2 -0
  51. package/build/src/intl.js +28 -0
  52. package/build/src/mcp/server.js +82 -6
  53. package/docs/run.md +8 -2
  54. package/package.json +3 -3
@@ -1,3 +1,19 @@
1
+ // Copyright 2025 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // This file defines the main `Clasp` class, which orchestrates all core
15
+ // functionalities of the CLI, including configuration management, API
16
+ // interactions, and file operations.
1
17
  import path from 'path';
2
18
  import Debug from 'debug';
3
19
  import { findUpSync } from 'find-up';
@@ -22,7 +38,16 @@ const DEFAULT_CLASP_IGNORE = [
22
38
  '.git/**',
23
39
  'node_modules/**',
24
40
  ];
41
+ /**
42
+ * Main class for interacting with Google Apps Script projects.
43
+ * It encapsulates all core functionalities like file management,
44
+ * project settings, API interactions, and authentication.
45
+ */
25
46
  export class Clasp {
47
+ /**
48
+ * Creates an instance of the Clasp class.
49
+ * @param {ClaspOptions} options - Configuration options for the Clasp instance.
50
+ */
26
51
  constructor(options) {
27
52
  debug('Creating clasp instance with options: %O', options);
28
53
  this.options = options;
@@ -45,15 +70,30 @@ export class Clasp {
45
70
  }
46
71
  return undefined;
47
72
  }
73
+ /**
74
+ * Configures the Clasp instance with a specific Apps Script project ID.
75
+ * This is a fluent method and returns the `Clasp` instance for chaining.
76
+ * @param {string} scriptId - The ID of the Apps Script project.
77
+ * @returns {this} The current Clasp instance.
78
+ * @throws {Error} If the project is already set.
79
+ */
48
80
  withScriptId(scriptId) {
49
81
  if (this.options.project) {
50
- throw new Error('Science project already set, create new instance instead');
82
+ debug('Project is already configured, overriding scriptId with %s', scriptId);
51
83
  }
52
84
  this.options.project = {
53
85
  scriptId,
54
86
  };
55
87
  return this;
56
88
  }
89
+ /**
90
+ * Sets the content directory for the project files.
91
+ * This directory is where clasp looks for source files (e.g., `.js`, `.html`).
92
+ * If a relative path is provided, it's resolved against the project's root directory.
93
+ * This is a fluent method and returns the `Clasp` instance for chaining.
94
+ * @param {string} contentDir - The path to the content directory.
95
+ * @returns {this} The current Clasp instance.
96
+ */
57
97
  withContentDir(contentDir) {
58
98
  if (!path.isAbsolute(contentDir)) {
59
99
  contentDir = path.resolve(this.options.files.projectRootDir, contentDir);
@@ -62,38 +102,55 @@ export class Clasp {
62
102
  return this;
63
103
  }
64
104
  }
105
+ /**
106
+ * Initializes and returns a Clasp instance.
107
+ * This function searches for project configuration files (`.clasp.json`, `.claspignore`),
108
+ * loads them if found, or sets up default configurations if not. It then creates
109
+ * and returns a new `Clasp` object configured with these settings.
110
+ * @param {InitOptions} options - Options for initializing the Clasp instance.
111
+ * @returns {Promise<Clasp>} A promise that resolves to a configured Clasp instance.
112
+ */
65
113
  export async function initClaspInstance(options) {
66
114
  var _a;
67
115
  debug('Initializing clasp instance');
116
+ // Attempt to find the project root directory and .clasp.json config file.
68
117
  const projectRoot = await findProjectRootdDir(options.configFile);
118
+ // If no .clasp.json is found, set up a default Clasp instance.
69
119
  if (!projectRoot) {
120
+ // Use the provided rootDir option or default to the current working directory.
70
121
  const dir = (_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd();
71
122
  debug(`No project found, defaulting to ${dir}`);
72
123
  const rootDir = path.resolve(dir);
124
+ // Default path for .clasp.json if one were to be created.
73
125
  const configFilePath = path.resolve(rootDir, '.clasp.json');
74
126
  const ignoreFile = await findIgnoreFile(rootDir, options.ignoreFile);
75
127
  const ignoreRules = await loadIgnoreFileOrDefaults(ignoreFile);
128
+ // Create a Clasp instance with default file settings and no project-specific config.
76
129
  return new Clasp({
77
130
  credentials: options.credentials,
78
- configFilePath,
131
+ configFilePath, // Path where .clasp.json would be.
79
132
  files: {
80
133
  projectRootDir: rootDir,
81
- contentDir: rootDir,
134
+ contentDir: rootDir, // By default, content directory is the root directory.
82
135
  ignoreFilePath: ignoreFile,
83
136
  ignorePatterns: ignoreRules,
84
- filePushOrder: [],
85
- skipSubdirectories: false,
86
- fileExtensions: readFileExtensions({}),
137
+ filePushOrder: [], // No specific push order.
138
+ skipSubdirectories: false, // Process subdirectories by default.
139
+ fileExtensions: readFileExtensions({}), // Default file extensions.
87
140
  },
141
+ // No project options (scriptId, projectId, parentId) as .clasp.json was not found.
88
142
  });
89
143
  }
144
+ // If .clasp.json is found, load its configuration.
90
145
  debug('Project config found at %s', projectRoot.configPath);
91
146
  const ignoreFile = await findIgnoreFile(projectRoot.rootDir, options.ignoreFile);
92
147
  const ignoreRules = await loadIgnoreFileOrDefaults(ignoreFile);
93
148
  const content = await fs.readFile(projectRoot.configPath, { encoding: 'utf8' });
94
- const config = JSON.parse(content);
149
+ const config = JSON.parse(content); // Parse the JSON content of .clasp.json.
150
+ // Determine file extensions, push order, and content directory from the loaded config.
95
151
  const fileExtensions = readFileExtensions(config);
96
- const filePushOrder = config.filePushOrder || [];
152
+ const filePushOrder = config.filePushOrder || []; // Default to empty array if not specified.
153
+ // Content directory can be specified by `srcDir` or `rootDir` in .clasp.json, defaulting to project root.
97
154
  const contentDir = path.resolve(projectRoot.rootDir, config.srcDir || config.rootDir || '.');
98
155
  return new Clasp({
99
156
  credentials: options.credentials,
@@ -115,13 +172,14 @@ export async function initClaspInstance(options) {
115
172
  });
116
173
  }
117
174
  function readFileExtensions(config) {
118
- let scriptExtensions = ['js', 'gs'];
119
- let htmlExtensions = ['html'];
120
- let jsonExtensions = ['json'];
175
+ let scriptExtensions = ['js', 'gs']; // Default script file extensions.
176
+ let htmlExtensions = ['html']; // Default HTML file extensions.
177
+ let jsonExtensions = ['json']; // Default JSON file extensions (primarily for appsscript.json).
178
+ // Support for legacy `fileExtension` setting (singular).
121
179
  if (config === null || config === void 0 ? void 0 : config.fileExtension) {
122
- // legacy fileExtension setting
123
180
  scriptExtensions = [config.fileExtension];
124
181
  }
182
+ // Support for current `scriptExtensions` setting (plural, array).
125
183
  if (config === null || config === void 0 ? void 0 : config.scriptExtensions) {
126
184
  scriptExtensions = ensureStringArray(config.scriptExtensions);
127
185
  }
@@ -131,6 +189,7 @@ function readFileExtensions(config) {
131
189
  if (config === null || config === void 0 ? void 0 : config.jsonExtensions) {
132
190
  jsonExtensions = ensureStringArray(config.jsonExtensions);
133
191
  }
192
+ // Ensure all extensions are lowercase and start with a dot.
134
193
  const fixupExtension = (ext) => {
135
194
  ext = ext.toLowerCase().trim();
136
195
  if (!ext.startsWith('.')) {
@@ -1,3 +1,19 @@
1
+ // Copyright 2025 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // This file manages the synchronization of files between the local filesystem
15
+ // and the Google Apps Script project. It handles pulling, pushing, collecting
16
+ // local files, watching for changes, and resolving file types and conflicts.
1
17
  import path from 'path';
2
18
  import chalk from 'chalk';
3
19
  import chokidar from 'chokidar';
@@ -25,11 +41,12 @@ async function getLocalFiles(rootDir, ignorePatterns, recursive) {
25
41
  let fdirBuilder = new fdir().withBasePath().withRelativePaths();
26
42
  if (!recursive) {
27
43
  debug('Not recursive, limiting depth to current directory');
28
- fdirBuilder = fdirBuilder.withMaxDepth(0);
44
+ fdirBuilder = fdirBuilder.withMaxDepth(0); // Limit crawling to the current directory if not recursive
29
45
  }
30
46
  const files = await fdirBuilder.crawl(rootDir).withPromise();
31
47
  let filteredFiles;
32
48
  if (ignorePatterns && ignorePatterns.length) {
49
+ // Filter out files that are explicitly ignored by the .claspignore file or default ignore patterns.
33
50
  filteredFiles = micromatch.not(files, ignorePatterns, { dot: true });
34
51
  debug('Filtered %d files from ignore rules', files.length - filteredFiles.length);
35
52
  }
@@ -51,12 +68,15 @@ function createFilenameConflictChecker() {
51
68
  const files = new Set();
52
69
  return (file) => {
53
70
  if (file.type !== 'SERVER_JS') {
54
- return file;
71
+ return file; // Conflict check only applies to SERVER_JS files
55
72
  }
56
73
  const parsedPath = path.parse(file.localPath);
74
+ // Create a key based on directory and name (without extension) to detect conflicts
75
+ // e.g. `src/Code.js` and `src/Code.gs` would conflict.
57
76
  const key = path.format({ dir: parsedPath.dir, name: parsedPath.name });
58
77
  if (files.has(key)) {
59
78
  throw new Error('Conflicting files found', {
79
+ // TODO: Better error message, show conflicting files
60
80
  cause: {
61
81
  code: 'FILE_CONFLICT',
62
82
  value: key,
@@ -84,19 +104,21 @@ function getFileType(fileName, fileExtensions) {
84
104
  function getFileExtension(type, fileExtensions) {
85
105
  // TODO - Include project setting override
86
106
  const extensionFor = (type, defaultValue) => {
107
+ // Prioritize the first extension defined for a type in .clasp.json if available.
87
108
  if (fileExtensions[type] && fileExtensions[type][0]) {
88
109
  return fileExtensions[type][0];
89
110
  }
90
- return defaultValue;
111
+ return defaultValue; // Fallback to default if no specific extension is configured.
91
112
  };
92
113
  switch (type) {
93
114
  case 'SERVER_JS':
94
- return extensionFor('SERVER_JS', '.js');
115
+ return extensionFor('SERVER_JS', '.js'); // Default to .js for server-side JavaScript
95
116
  case 'JSON':
96
- return extensionFor('JSON', '.json');
117
+ return extensionFor('JSON', '.json'); // Default to .json for JSON files (e.g. appsscript.json)
97
118
  case 'HTML':
98
- return extensionFor('HTML', '.html');
119
+ return extensionFor('HTML', '.html'); // Default to .html for HTML files
99
120
  default:
121
+ // This case should ideally not be reached if file types are correctly identified.
100
122
  throw new Error('Invalid file type', {
101
123
  cause: {
102
124
  code: 'INVALID_FILE_TYPE',
@@ -124,10 +146,27 @@ function debounceFileChanges(callback, delayMs) {
124
146
  }, delayMs);
125
147
  };
126
148
  }
149
+ /**
150
+ * Manages operations related to project files, including fetching remote files,
151
+ * collecting local files based on ignore patterns, watching for local changes,
152
+ * pushing local changes to the remote project, and pulling remote files
153
+ * to the local filesystem.
154
+ */
127
155
  export class Files {
156
+ /**
157
+ * Constructs a new Files instance.
158
+ * @param {ClaspOptions} options - Configuration options for file operations.
159
+ */
128
160
  constructor(options) {
129
161
  this.options = options;
130
162
  }
163
+ /**
164
+ * Fetches the content of a script project from Google Drive.
165
+ * @param {number} [versionNumber] - Optional version number to fetch.
166
+ * If not specified, the latest version (HEAD) is fetched.
167
+ * @returns {Promise<ProjectFile[]>} A promise that resolves to an array of project files.
168
+ * @throws {Error} If there's an API error or authentication/configuration issues.
169
+ */
131
170
  async fetchRemote(versionNumber) {
132
171
  var _a;
133
172
  debug('Fetching remote files, version %s', versionNumber !== null && versionNumber !== void 0 ? versionNumber : 'HEAD');
@@ -161,6 +200,12 @@ export class Files {
161
200
  handleApiError(error);
162
201
  }
163
202
  }
203
+ /**
204
+ * Collects all local files in the project's content directory, respecting ignore patterns.
205
+ * It reads the content of each file and determines its type.
206
+ * @returns {Promise<ProjectFile[]>} A promise that resolves to an array of local project files.
207
+ * @throws {Error} If the project is not configured or there's a file conflict.
208
+ */
164
209
  async collectLocalFiles() {
165
210
  var _a;
166
211
  debug('Collecting local files');
@@ -193,34 +238,45 @@ export class Files {
193
238
  }));
194
239
  return files.filter((f) => f !== undefined);
195
240
  }
241
+ /**
242
+ * Watches for changes in local project files and triggers callbacks.
243
+ * @param {() => Promise<void> | void} onReady - Callback executed when the watcher is ready.
244
+ * @param {(files: string[]) => Promise<void> | void} onFilesChanged - Callback executed
245
+ * when files are added, changed, or deleted, with a debounced list of changed file paths.
246
+ * @returns {() => Promise<void>} A function that can be called to stop watching.
247
+ */
196
248
  watchLocalFiles(onReady, onFilesChanged) {
197
249
  var _a;
198
250
  const ignorePatterns = (_a = this.options.files.ignorePatterns) !== null && _a !== void 0 ? _a : [];
199
- const collector = debounceFileChanges(onFilesChanged, 500);
251
+ const collector = debounceFileChanges(onFilesChanged, 500); // Debounce changes to avoid rapid firing
200
252
  const onChange = async (path) => {
201
253
  debug('Have file changes: %s', path);
202
- collector(path);
254
+ collector(path); // Collect changed paths
203
255
  };
204
256
  let matcher;
205
257
  if (ignorePatterns && ignorePatterns.length) {
258
+ // Custom matcher function for chokidar to respect .claspignore patterns.
259
+ // This is necessary because chokidar's `ignored` option expects specific formats.
206
260
  matcher = (file, stats) => {
207
261
  if (!(stats === null || stats === void 0 ? void 0 : stats.isFile())) {
208
- return false;
262
+ return false; // Only consider files for ignore matching
209
263
  }
264
+ // Normalize file path relative to project root for consistent matching with ignorePatterns.
210
265
  file = path.relative(this.options.files.projectRootDir, file);
266
+ // Check if the file is NOT in the list of files to keep (i.e., it should be ignored).
211
267
  const ignore = micromatch.not([file], ignorePatterns, { dot: true }).length === 0;
212
268
  return ignore;
213
269
  };
214
270
  }
215
271
  const watcher = chokidar.watch(this.options.files.contentDir, {
216
- persistent: true,
217
- ignoreInitial: true,
218
- cwd: this.options.files.contentDir,
219
- ignored: matcher,
272
+ persistent: true, // Keep watching until explicitly closed
273
+ ignoreInitial: true, // Don't trigger 'add' events for existing files on startup
274
+ cwd: this.options.files.contentDir, // Watch paths relative to contentDir
275
+ ignored: matcher, // Use custom ignore logic if patterns are present
220
276
  });
221
- watcher.on('ready', onReady); // Push on start
222
- watcher.on('add', onChange);
223
- watcher.on('change', onChange);
277
+ watcher.on('ready', onReady); // Callback when initial scan is complete
278
+ watcher.on('add', onChange); // On new file addition
279
+ watcher.on('change', onChange); // On file content change
224
280
  watcher.on('unlink', onChange);
225
281
  watcher.on('error', err => {
226
282
  debug('Unexpected error during watch: %O', err);
@@ -230,16 +286,30 @@ export class Files {
230
286
  await watcher.close();
231
287
  };
232
288
  }
289
+ /**
290
+ * Compares local files with remote files (HEAD version) to identify changes.
291
+ * A file is considered changed if it's new locally or its content differs from the remote version.
292
+ * @returns {Promise<ProjectFile[]>} A promise that resolves to an array of project files that have changed.
293
+ */
233
294
  async getChangedFiles() {
234
295
  const [localFiles, remoteFiles] = await Promise.all([this.collectLocalFiles(), this.fetchRemote()]);
296
+ // Iterate over local files and compare with their remote counterparts.
235
297
  return localFiles.reduce((changed, localFile) => {
236
298
  const remote = remoteFiles.find(f => f.localPath === localFile.localPath);
299
+ // A file is considered changed if it doesn't exist remotely or if its source content differs.
237
300
  if (!remote || remote.source !== localFile.source) {
238
301
  changed.push(localFile);
239
302
  }
240
303
  return changed;
241
304
  }, []);
242
305
  }
306
+ /**
307
+ * Identifies files present in the local content directory that are not tracked
308
+ * by the Apps Script project (i.e., not in `.claspignore` or `appsscript.json`'s `filePushOrder`
309
+ * and not matching supported file extensions).
310
+ * @returns {Promise<string[]>} A promise that resolves to an array of untracked file paths,
311
+ * collapsed to their common parent directories where applicable.
312
+ */
243
313
  async getUntrackedFiles() {
244
314
  debug('Collecting untracked files');
245
315
  assertScriptConfigured(this.options);
@@ -252,33 +322,46 @@ export class Files {
252
322
  for (const file of projectFiles) {
253
323
  debug('Found tracked file %s', file.localPath);
254
324
  trackedFiles.add(file.localPath);
255
- // Save all parent paths to allow quick lookup.
256
- // Allows collapsing the unfiltered files to the common parent directory
325
+ // Save all parent paths of tracked files to allow quick lookup.
326
+ // This helps in collapsing untracked file paths to their nearest common untracked parent.
257
327
  const dirs = parentDirs(file.localPath);
258
328
  dirs.forEach(dir => dirsWithIncludedFiles.add(dir));
259
329
  }
330
+ // Get all files in the content directory without applying ignore rules yet.
260
331
  const allFiles = await getUnfilteredLocalFiles(contentDir);
261
332
  for (const file of allFiles) {
262
333
  const resolvedPath = path.relative(cwd, file);
263
334
  if (trackedFiles.has(resolvedPath)) {
264
- // Tracked file, skip
335
+ // If the file is already tracked (i.e., part of the project to be pushed), skip it.
265
336
  continue;
266
337
  }
267
- // Reduce path to nearest parent directory with no project files included
338
+ // Reduce path to the nearest parent directory that itself does not contain any tracked files.
339
+ // This groups untracked files under their common untracked root.
340
+ // For example, if 'node_modules/lib/a.js' and 'node_modules/lib/b.js' are untracked,
341
+ // and 'node_modules/lib' contains no tracked files, this will report 'node_modules/lib/'.
268
342
  let excludedPath = resolvedPath;
269
343
  for (const dir of parentDirs(resolvedPath)) {
270
344
  if (dirsWithIncludedFiles.has(dir)) {
345
+ // Stop if we reach a directory that is a parent of some tracked file.
271
346
  break;
272
347
  }
273
- excludedPath = path.normalize(`${dir}/`);
348
+ excludedPath = path.normalize(`${dir}/`); // Mark as directory
274
349
  }
275
- debug('Found untracked file %s', excludedPath);
350
+ debug('Found untracked file/directory %s', excludedPath);
276
351
  untrackedFiles.add(excludedPath);
277
352
  }
278
353
  const untrackedFilesArray = Array.from(untrackedFiles);
279
- untrackedFilesArray.sort((a, b) => a.localeCompare(b));
354
+ untrackedFilesArray.sort((a, b) => a.localeCompare(b)); // Sort for consistent output
280
355
  return untrackedFilesArray;
281
356
  }
357
+ /**
358
+ * Pushes local project files to the Google Apps Script project.
359
+ * Files are sorted according to `filePushOrder` from the manifest if specified.
360
+ * Handles API errors, including syntax errors in pushed files.
361
+ * @returns {Promise<ProjectFile[]>} A promise that resolves to an array of files that were pushed.
362
+ * Returns an empty array if no files were found to push.
363
+ * @throws {Error} If there's an API error, authentication/configuration issues, or a syntax error in the code.
364
+ */
282
365
  async push() {
283
366
  var _a;
284
367
  debug('Pushing files');
@@ -295,22 +378,22 @@ export class Files {
295
378
  files.sort((a, b) => {
296
379
  const indexA = filePushOrder.indexOf(a.localPath);
297
380
  const indexB = filePushOrder.indexOf(b.localPath);
381
+ // If neither file is in the push order, sort them alphabetically.
298
382
  if (indexA === -1 && indexB === -1) {
299
- // Neither has explicit order, sort by name
300
383
  return a.localPath.localeCompare(b.localPath);
301
384
  }
385
+ // If only file B is in the push order, file B comes first.
302
386
  if (indexA === -1) {
303
- // B has explicit priority, is first
304
387
  return 1;
305
388
  }
389
+ // If only file A is in the push order, file A comes first.
306
390
  if (indexB === -1) {
307
- // A has explicit priority, is first
308
391
  return -1;
309
392
  }
310
- // Both prioritized, use rank
393
+ // If both files are in the push order, sort by their index in the push order.
311
394
  return indexA - indexB;
312
395
  });
313
- // Start pushing.
396
+ // Prepare file objects for the Apps Script API request.
314
397
  try {
315
398
  const scriptFiles = files.map(f => ({
316
399
  name: f.remotePath,
@@ -345,6 +428,13 @@ export class Files {
345
428
  handleApiError(error);
346
429
  }
347
430
  }
431
+ /**
432
+ * Checks if any files specified in the `filePushOrder` of the manifest
433
+ * were not actually pushed. This can help identify misconfigurations.
434
+ * @param {ProjectFile[]} pushedFiles - An array of files that were successfully pushed.
435
+ * @returns {void} This method does not return a value but may have side effects (e.g. logging) if implemented.
436
+ * Currently, it only calculates missing files but doesn't do anything with the result.
437
+ */
348
438
  checkMissingFilesFromPushOrder(pushedFiles) {
349
439
  var _a;
350
440
  const missingFiles = [];
@@ -355,6 +445,14 @@ export class Files {
355
445
  }
356
446
  }
357
447
  }
448
+ /**
449
+ * Fetches remote project files (optionally a specific version) and writes them
450
+ * to the local filesystem, overwriting existing files.
451
+ * @param {number} [version] - Optional version number to pull. If not specified,
452
+ * the latest version (HEAD) is pulled.
453
+ * @returns {Promise<ProjectFile[]>} A promise that resolves to an array of files that were pulled.
454
+ * @throws {Error} If there's an API error or authentication/configuration issues.
455
+ */
358
456
  async pull(version) {
359
457
  debug('Pulling files');
360
458
  assertAuthenticated(this.options);
@@ -384,25 +482,30 @@ function extractSyntaxError(error, files) {
384
482
  var _a;
385
483
  let message = error.message;
386
484
  let snippet = '';
485
+ // Try to parse the error message for syntax error details.
486
+ // Example: "Syntax error: Missing ; before statement. line: 1 file: Code"
387
487
  const re = /Syntax error: (.+) line: (\d+) file: (.+)/;
388
488
  const [, errorName, lineNum, fileName] = (_a = re.exec(error.message)) !== null && _a !== void 0 ? _a : [];
389
489
  if (fileName === undefined) {
490
+ // If parsing fails, it's not a recognized syntax error format.
390
491
  return undefined;
391
492
  }
392
493
  message = `${errorName} - "${fileName}:${lineNum}"`;
393
- // Get formatted code snippet
394
- const contextCount = 4;
494
+ // Attempt to create a code snippet for the error.
495
+ const contextCount = 4; // Number of lines before and after the error line to include.
395
496
  const errFile = files.find((x) => x.remotePath === fileName);
396
497
  if (!errFile || !errFile.source) {
498
+ // If the source file of the error cannot be found, no snippet can be generated.
397
499
  return undefined;
398
500
  }
399
501
  const srcLines = errFile.source.split('\n');
400
- const errIndex = Math.max(parseInt(lineNum) - 1, 0);
502
+ const errIndex = Math.max(parseInt(lineNum) - 1, 0); // 0-based index
401
503
  const preIndex = Math.max(errIndex - contextCount, 0);
402
504
  const postIndex = Math.min(errIndex + contextCount + 1, srcLines.length);
505
+ // Format the snippet with dim context lines and a bold error line.
403
506
  const preLines = chalk.dim(` ${srcLines.slice(preIndex, errIndex).join('\n ')}`);
404
507
  const errLine = chalk.bold(`⇒ ${srcLines[errIndex]}`);
405
508
  const postLines = chalk.dim(` ${srcLines.slice(errIndex + 1, postIndex).join('\n ')}`);
406
509
  snippet = preLines + '\n' + errLine + '\n' + postLines;
407
- return { message, snippet };
510
+ return { message, snippet }; // Return the formatted message and snippet.
408
511
  }
@@ -1,11 +1,35 @@
1
+ // Copyright 2025 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // This file defines the `Functions` class, which provides methods for
15
+ // listing and executing functions within a Google Apps Script project.
1
16
  import Debug from 'debug';
2
17
  import { google } from 'googleapis';
3
18
  import { assertAuthenticated, assertScriptConfigured, handleApiError } from './utils.js';
4
19
  const debug = Debug('clasp:core');
20
+ /**
21
+ * Provides methods for interacting with functions within a Google Apps Script project,
22
+ * such as listing available functions and executing them remotely.
23
+ */
5
24
  export class Functions {
6
25
  constructor(options) {
7
26
  this.options = options;
8
27
  }
28
+ /**
29
+ * Retrieves a list of all function names in the Apps Script project.
30
+ * @returns {Promise<string[]>} A promise that resolves to an array of function names.
31
+ * @throws {Error} If there's an API error or authentication/configuration issues.
32
+ */
9
33
  async getFunctionNames() {
10
34
  debug('Fetching runnable functions');
11
35
  assertAuthenticated(this.options);
@@ -21,6 +45,18 @@ export class Functions {
21
45
  }
22
46
  return files.flatMap(file => { var _a, _b; return (_b = (_a = file.functionSet) === null || _a === void 0 ? void 0 : _a.values) !== null && _b !== void 0 ? _b : []; }).map(func => func.name);
23
47
  }
48
+ /**
49
+ * Executes a specified function in the Apps Script project.
50
+ * @param {string} functionName - The name of the function to run.
51
+ * @param {unknown[]} parameters - An array of parameters to pass to the function.
52
+ * These will be JSON-stringified.
53
+ * @param {boolean} [devMode=true] - Whether to run the function in development mode.
54
+ * Dev mode executes the latest saved code instead of the last deployed version.
55
+ * @returns {Promise<any>} A promise that resolves to the response from the function execution,
56
+ * or undefined if an error occurs before the API call.
57
+ * @throws {Error} If there's an API error, authentication/configuration issues,
58
+ * or if the function execution itself returns an error.
59
+ */
24
60
  async runFunction(functionName, parameters, devMode = true) {
25
61
  debug('Running script function %s', functionName);
26
62
  assertAuthenticated(this.options);
@@ -1,12 +1,41 @@
1
+ // Copyright 2025 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // This file defines the `Logs` class, responsible for fetching log entries
15
+ // from Google Cloud Logging for the associated Apps Script project.
1
16
  import Debug from 'debug';
2
17
  import { google } from 'googleapis';
3
18
  import { fetchWithPages } from './utils.js';
4
19
  import { assertAuthenticated, assertGcpProjectConfigured, handleApiError } from './utils.js';
5
20
  const debug = Debug('clasp:core');
21
+ /**
22
+ * Facilitates the retrieval of log entries from Google Cloud Logging
23
+ * for the Apps Script project associated with the current clasp project.
24
+ */
6
25
  export class Logs {
7
26
  constructor(options) {
8
27
  this.options = options;
9
28
  }
29
+ /**
30
+ * Retrieves log entries from Google Cloud Logging for the configured GCP project.
31
+ * Logs are fetched in descending order of timestamp.
32
+ * @param {Date} [since] - Optional date to filter logs. Only entries with a timestamp
33
+ * greater than or equal to this date will be returned.
34
+ * @returns {Promise<{results: logging_v2.Schema$LogEntry[], partialResults: boolean} | undefined>}
35
+ * A promise that resolves to an object containing the log entries and a flag indicating
36
+ * if results are partial (due to pagination limits), or undefined if an error occurs.
37
+ * @throws {Error} If there's an API error or authentication/configuration issues.
38
+ */
10
39
  async getLogEntries(since) {
11
40
  debug('Fetching logs');
12
41
  assertAuthenticated(this.options);
@@ -1 +1,14 @@
1
+ // Copyright 2025 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
1
14
  export {};