@google/clasp 3.0.6-alpha → 3.1.1

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 +35 -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 +39 -2
  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 +10 -4
  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 `Project` class, which is responsible for managing
15
+ // Google Apps Script project metadata, lifecycle operations (creation, versions,
16
+ // deployments), and local project configuration settings.
1
17
  import Debug from 'debug';
2
18
  import fs from 'fs/promises';
3
19
  import { google } from 'googleapis';
@@ -5,6 +21,12 @@ import { fetchWithPages } from './utils.js';
5
21
  import { assertAuthenticated, assertScriptConfigured, handleApiError } from './utils.js';
6
22
  import path from 'path';
7
23
  const debug = Debug('clasp:core');
24
+ /**
25
+ * Manages Google Apps Script project settings and interactions with the
26
+ * Apps Script API for operations like creating projects, versions,
27
+ * and deployments. It also handles reading and writing the local
28
+ * `.clasp.json` configuration file and the `appsscript.json` manifest.
29
+ */
8
30
  export class Project {
9
31
  constructor(options) {
10
32
  this.options = options;
@@ -22,10 +44,23 @@ export class Project {
22
44
  return (_a = this.options.project) === null || _a === void 0 ? void 0 : _a.parentId;
23
45
  }
24
46
  // TODO - Do we need the assertion or can just use accessor?
47
+ /**
48
+ * Retrieves the Google Cloud Platform (GCP) project ID associated with the script.
49
+ * Asserts that the script is configured before returning the ID.
50
+ * @returns {string | undefined} The GCP project ID, or undefined if not set.
51
+ * @throws {Error} If the script is not configured.
52
+ */
25
53
  getProjectId() {
26
54
  assertScriptConfigured(this.options);
27
55
  return this.options.project.projectId;
28
56
  }
57
+ /**
58
+ * Creates a new standalone Apps Script project.
59
+ * @param {string} name - The title for the new script project.
60
+ * @param {string} [parentId] - Optional ID of a Google Drive folder to create the script in.
61
+ * @returns {Promise<string>} A promise that resolves to the script ID of the newly created project.
62
+ * @throws {Error} If there's an API error or authentication issues.
63
+ */
29
64
  async createScript(name, parentId) {
30
65
  var _a;
31
66
  debug('Creating script %s', name);
@@ -56,6 +91,40 @@ export class Project {
56
91
  handleApiError(error);
57
92
  }
58
93
  }
94
+ /**
95
+ * Moves the specified Google Drive file to the trash.
96
+ * @returns {Promise<void>} A promise that resolves when the file is successfully trashed.
97
+ */
98
+ async trashScript() {
99
+ var _a;
100
+ debug('Deleting script %s', (_a = this.options.project) === null || _a === void 0 ? void 0 : _a.scriptId);
101
+ assertAuthenticated(this.options);
102
+ assertScriptConfigured(this.options);
103
+ const fileId = this.options.project.scriptId;
104
+ const credentials = this.options.credentials;
105
+ const drive = google.drive({ version: 'v3', auth: credentials });
106
+ try {
107
+ const requestOptions = {
108
+ fileId,
109
+ requestBody: {
110
+ trashed: true,
111
+ },
112
+ };
113
+ debug('Trashing script with request %O', requestOptions);
114
+ await drive.files.update(requestOptions);
115
+ }
116
+ catch (error) {
117
+ handleApiError(error);
118
+ }
119
+ }
120
+ /**
121
+ * Creates a new Google Drive file (e.g., Sheet, Doc) and a bound Apps Script project for it.
122
+ * @param {string} name - The title for the new Drive file and script project.
123
+ * @param {string} mimeType - The MIME type of the Drive file to create (e.g., 'application/vnd.google-apps.spreadsheet').
124
+ * @returns {Promise<{scriptId: string; parentId: string}>} A promise that resolves to an object
125
+ * containing the script ID and the parent Drive file ID.
126
+ * @throws {Error} If there's an API error or authentication issues.
127
+ */
59
128
  async createWithContainer(name, mimeType) {
60
129
  var _a;
61
130
  debug('Creating container bound script %s (%s)', name, mimeType);
@@ -66,6 +135,7 @@ export class Project {
66
135
  let parentId;
67
136
  const credentials = this.options.credentials;
68
137
  const drive = google.drive({ version: 'v3', auth: credentials });
138
+ // Create the container file (e.g., Google Sheet, Doc) using the Drive API.
69
139
  try {
70
140
  const requestOptions = {
71
141
  requestBody: {
@@ -75,7 +145,7 @@ export class Project {
75
145
  };
76
146
  debug('Creating project with request %O', requestOptions);
77
147
  const res = await drive.files.create(requestOptions);
78
- parentId = res.data.id;
148
+ parentId = res.data.id; // Get the ID of the newly created container file.
79
149
  debug('Created container %s', parentId);
80
150
  if (!parentId) {
81
151
  throw new Error('Unexpected error, container ID missing from response.');
@@ -84,12 +154,20 @@ export class Project {
84
154
  catch (error) {
85
155
  handleApiError(error);
86
156
  }
157
+ // Once the container is created, create an Apps Script project bound to it.
87
158
  const scriptId = await this.createScript(name, parentId);
88
159
  return {
89
- parentId,
160
+ parentId, // Return the ID of the container.
90
161
  scriptId,
91
162
  };
92
163
  }
164
+ /**
165
+ * Lists Apps Script projects accessible by the authenticated user from Google Drive.
166
+ * @returns {Promise<{results: Script[], partialResults: boolean} | undefined>}
167
+ * A promise that resolves to an object containing an array of script projects
168
+ * (with name and ID) and a flag indicating if results are partial, or undefined on error.
169
+ * @throws {Error} If there's an API error or authentication issues.
170
+ */
93
171
  async listScripts() {
94
172
  debug('Fetching scripts');
95
173
  assertAuthenticated(this.options);
@@ -116,6 +194,12 @@ export class Project {
116
194
  handleApiError(error);
117
195
  }
118
196
  }
197
+ /**
198
+ * Creates a new immutable version of the Apps Script project.
199
+ * @param {string} [description=''] - An optional description for the new version.
200
+ * @returns {Promise<number>} A promise that resolves to the newly created version number.
201
+ * @throws {Error} If there's an API error or authentication/configuration issues.
202
+ */
119
203
  async version(description = '') {
120
204
  var _a;
121
205
  debug('Creating version: %s', description);
@@ -141,6 +225,13 @@ export class Project {
141
225
  handleApiError(error);
142
226
  }
143
227
  }
228
+ /**
229
+ * Lists all immutable versions of the Apps Script project.
230
+ * @returns {Promise<{results: script_v1.Schema$Version[], partialResults: boolean} | undefined>}
231
+ * A promise that resolves to an object containing an array of version objects
232
+ * and a flag indicating if results are partial, or undefined on error.
233
+ * @throws {Error} If there's an API error or authentication/configuration issues.
234
+ */
144
235
  async listVersions() {
145
236
  debug('Fetching versions');
146
237
  assertAuthenticated(this.options);
@@ -168,6 +259,13 @@ export class Project {
168
259
  handleApiError(error);
169
260
  }
170
261
  }
262
+ /**
263
+ * Lists all deployments for the Apps Script project.
264
+ * @returns {Promise<{results: script_v1.Schema$Deployment[], partialResults: boolean} | undefined>}
265
+ * A promise that resolves to an object containing an array of deployment objects
266
+ * and a flag indicating if results are partial, or undefined on error.
267
+ * @throws {Error} If there's an API error or authentication/configuration issues.
268
+ */
171
269
  async listDeployments() {
172
270
  debug('Listing deployments');
173
271
  assertAuthenticated(this.options);
@@ -195,10 +293,21 @@ export class Project {
195
293
  handleApiError(error);
196
294
  }
197
295
  }
296
+ /**
297
+ * Creates a new deployment or updates an existing one for the Apps Script project.
298
+ * If `versionNumber` is not provided, a new script version is created with the given `description`.
299
+ * @param {string} [description=''] - Description for the new version (if created) or deployment.
300
+ * @param {string} [deploymentId] - Optional ID of an existing deployment to update. If not provided, a new deployment is created.
301
+ * @param {number} [versionNumber] - Optional specific script version number to deploy.
302
+ * @returns {Promise<script_v1.Schema$Deployment>} A promise that resolves to the deployment object.
303
+ * @throws {Error} If there's an API error or authentication/configuration issues.
304
+ */
198
305
  async deploy(description = '', deploymentId, versionNumber) {
199
306
  debug('Deploying project: %s (%s)', description, versionNumber !== null && versionNumber !== void 0 ? versionNumber : 'HEAD');
200
307
  assertAuthenticated(this.options);
201
308
  assertScriptConfigured(this.options);
309
+ // If no specific versionNumber is provided for deployment,
310
+ // create a new version of the script with the given description.
202
311
  if (versionNumber === undefined) {
203
312
  versionNumber = await this.version(description);
204
313
  }
@@ -207,9 +316,10 @@ export class Project {
207
316
  const script = google.script({ version: 'v1', auth: credentials });
208
317
  try {
209
318
  let deployment;
319
+ // If no deploymentId is provided, create a new deployment.
210
320
  if (!deploymentId) {
211
321
  const requestOptions = {
212
- scriptId: scriptId,
322
+ scriptId: scriptId, // The scriptId must be provided in the request body for create.
213
323
  requestBody: {
214
324
  description: description !== null && description !== void 0 ? description : '',
215
325
  versionNumber: versionNumber,
@@ -221,14 +331,15 @@ export class Project {
221
331
  deployment = res.data;
222
332
  }
223
333
  else {
334
+ // If a deploymentId is provided, update the existing deployment.
224
335
  const requestOptions = {
225
- scriptId: scriptId,
226
- deploymentId: deploymentId,
336
+ scriptId: scriptId, // Path parameter for the scriptId.
337
+ deploymentId: deploymentId, // Path parameter for the deploymentId to update.
227
338
  requestBody: {
228
339
  deploymentConfig: {
229
340
  description: description !== null && description !== void 0 ? description : '',
230
341
  versionNumber: versionNumber,
231
- scriptId: scriptId,
342
+ scriptId: scriptId, // The scriptId also needs to be in the deploymentConfig.
232
343
  manifestFileName: 'appsscript',
233
344
  },
234
345
  },
@@ -237,12 +348,20 @@ export class Project {
237
348
  const res = await script.projects.deployments.update(requestOptions);
238
349
  deployment = res.data;
239
350
  }
240
- return deployment;
351
+ return deployment; // Return the created or updated deployment object.
241
352
  }
242
353
  catch (error) {
243
354
  handleApiError(error);
244
355
  }
245
356
  }
357
+ /**
358
+ * Retrieves the entry points for a specific deployment of the Apps Script project.
359
+ * Entry points define how the script can be executed (e.g., as a web app, API executable).
360
+ * @param {string} deploymentId - The ID of the deployment.
361
+ * @returns {Promise<script_v1.Schema$EntryPoint[] | undefined>} A promise that resolves to an array
362
+ * of entry point objects, or undefined if an error occurs.
363
+ * @throws {Error} If there's an API error or authentication/configuration issues.
364
+ */
246
365
  async entryPoints(deploymentId) {
247
366
  var _a, _b;
248
367
  assertAuthenticated(this.options);
@@ -259,6 +378,12 @@ export class Project {
259
378
  handleApiError(error);
260
379
  }
261
380
  }
381
+ /**
382
+ * Deletes a specific deployment of the Apps Script project.
383
+ * @param {string} deploymentId - The ID of the deployment to delete.
384
+ * @returns {Promise<void>} A promise that resolves when the deployment is deleted.
385
+ * @throws {Error} If there's an API error or authentication/configuration issues.
386
+ */
262
387
  async undeploy(deploymentId) {
263
388
  debug('Deleting deployment %s', deploymentId);
264
389
  assertAuthenticated(this.options);
@@ -278,6 +403,12 @@ export class Project {
278
403
  handleApiError(error);
279
404
  }
280
405
  }
406
+ /**
407
+ * Writes the current project settings (script ID, root directory, parent ID, project ID,
408
+ * file extensions, push order, skip subdirectories) to the `.clasp.json` file.
409
+ * @returns {Promise<void>} A promise that resolves when the settings are written.
410
+ * @throws {Error} If the script is not configured or there's a file system error.
411
+ */
281
412
  async updateSettings() {
282
413
  debug('Updating settings');
283
414
  assertScriptConfigured(this.options);
@@ -295,16 +426,32 @@ export class Project {
295
426
  };
296
427
  await fs.writeFile(this.options.configFilePath, JSON.stringify(settings, null, 2));
297
428
  }
429
+ /**
430
+ * Sets the Google Cloud Platform (GCP) project ID for the current Apps Script project
431
+ * and updates the `.clasp.json` file.
432
+ * @param {string | undefined} projectId - The GCP project ID to set.
433
+ * @returns {Promise<void>} A promise that resolves when the project ID is set and settings are updated.
434
+ * @throws {Error} If the script is not configured.
435
+ */
298
436
  async setProjectId(projectId) {
299
437
  debug('Setting project ID %s in file %s', projectId, this.options.configFilePath);
300
438
  assertScriptConfigured(this.options);
301
439
  this.options.project.projectId = projectId;
302
440
  this.updateSettings();
303
441
  }
442
+ /**
443
+ * Checks if a script project is currently configured (i.e., if a script ID is set).
444
+ * @returns {boolean} True if a script ID is set, false otherwise.
445
+ */
304
446
  exists() {
305
447
  var _a;
306
448
  return ((_a = this.options.project) === null || _a === void 0 ? void 0 : _a.scriptId) !== undefined;
307
449
  }
450
+ /**
451
+ * Reads and parses the `appsscript.json` manifest file from the project's content directory.
452
+ * @returns {Promise<Manifest>} A promise that resolves to the parsed manifest object.
453
+ * @throws {Error} If the script is not configured or the manifest file cannot be read/parsed.
454
+ */
308
455
  async readManifest() {
309
456
  debug('Reading manifest');
310
457
  assertScriptConfigured(this.options);
@@ -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 `Services` class, which handles the management of
15
+ // Google Cloud Platform (GCP) services and Advanced Google Services for an
16
+ // Apps Script project, including enabling, disabling, and listing services.
1
17
  import path from 'path';
2
18
  import Debug from 'debug';
3
19
  import fs from 'fs/promises';
@@ -6,10 +22,23 @@ import { PUBLIC_ADVANCED_SERVICES } from './apis.js';
6
22
  import { assertGcpProjectConfigured, handleApiError } from './utils.js';
7
23
  import { fetchWithPages } from './utils.js';
8
24
  const debug = Debug('clasp:core');
25
+ /**
26
+ * Manages the Google Cloud Platform (GCP) services and Advanced Google Services
27
+ * associated with an Apps Script project. This includes listing available and
28
+ * enabled services, as well as enabling or disabling services for the project.
29
+ */
9
30
  export class Services {
10
31
  constructor(config) {
11
32
  this.options = config;
12
33
  }
34
+ /**
35
+ * Retrieves a list of Google Cloud Platform (GCP) services that are currently
36
+ * enabled for the associated Apps Script project.
37
+ * @returns {Promise<Service[] | undefined>} A promise that resolves to an array of enabled
38
+ * services (with id, name, and description), or undefined if an error occurs.
39
+ * Filters for services that are also listed as public advanced services.
40
+ * @throws {Error} If the GCP project is not configured or authentication fails.
41
+ */
13
42
  async getEnabledServices() {
14
43
  debug('Fetching enabled services');
15
44
  assertGcpProjectConfigured(this.options);
@@ -35,24 +64,31 @@ export class Services {
35
64
  maxResults: 10000,
36
65
  });
37
66
  // Filter out the disabled ones. Print the enabled ones.
67
+ // Filter out the disabled ones. Print the enabled ones.
38
68
  const truncateName = (name) => {
69
+ // Service names from API might be like 'sheets.googleapis.com'.
70
+ // We only want the 'sheets' part for matching with PUBLIC_ADVANCED_SERVICES.
39
71
  const i = name.indexOf('.');
40
72
  if (i !== -1) {
41
73
  return name.slice(0, i);
42
74
  }
43
75
  return name;
44
76
  };
77
+ // Get a list of serviceIds from our known public advanced services.
45
78
  const allowedIds = PUBLIC_ADVANCED_SERVICES.map(service => service.serviceId);
79
+ // Map the raw service list from API to our simplified `Service` type
80
+ // and filter them to include only those that are known public advanced services.
46
81
  return serviceList.results
47
82
  .map(service => {
48
83
  var _a, _b, _c, _d, _e, _f;
49
84
  return ({
50
- id: (_a = service.name) !== null && _a !== void 0 ? _a : '',
51
- name: truncateName((_c = (_b = service.config) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : 'Unknown name'),
85
+ id: (_a = service.name) !== null && _a !== void 0 ? _a : '', // Full name like 'sheets.googleapis.com'
86
+ name: truncateName((_c = (_b = service.config) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : 'Unknown name'), // Short name like 'sheets'
52
87
  description: (_f = (_e = (_d = service.config) === null || _d === void 0 ? void 0 : _d.documentation) === null || _e === void 0 ? void 0 : _e.summary) !== null && _f !== void 0 ? _f : '',
53
88
  });
54
89
  })
55
90
  .filter(service => {
91
+ // Only include services that are in our `PUBLIC_ADVANCED_SERVICES` list.
56
92
  return allowedIds.indexOf(service.name) !== -1;
57
93
  });
58
94
  }
@@ -60,19 +96,33 @@ export class Services {
60
96
  handleApiError(error);
61
97
  }
62
98
  }
99
+ /**
100
+ * Retrieves a list of all publicly available Google Advanced Services that can be
101
+ * enabled for an Apps Script project.
102
+ * @returns {Promise<Service[] | undefined>} A promise that resolves to an array of available
103
+ * services (with id, name, and description), or undefined if an error occurs.
104
+ * @throws {Error} If there's an API error.
105
+ */
63
106
  async getAvailableServices() {
64
107
  var _a;
65
108
  debug('Fetching available services');
66
109
  const discovery = google.discovery({ version: 'v1' });
67
110
  try {
111
+ // Fetch the list of all discoverable APIs. 'preferred: true' typically gets the recommended versions.
68
112
  const { data } = await discovery.apis.list({
69
113
  preferred: true,
70
114
  });
115
+ // Get a list of serviceIds from our known public advanced services for filtering.
71
116
  const allowedIds = PUBLIC_ADVANCED_SERVICES.map(service => service.serviceId);
72
117
  const allServices = (_a = data.items) !== null && _a !== void 0 ? _a : [];
118
+ // Type guard to ensure the service item has the properties we expect and is a known advanced service.
73
119
  const isValidService = (s) => {
74
- return (s.id !== undefined && s.name !== undefined && allowedIds.indexOf(s.name) !== -1 && s.description !== undefined);
120
+ return (s.id !== undefined &&
121
+ s.name !== undefined &&
122
+ allowedIds.indexOf(s.name) !== -1 && // Check if the service's short name is in our list
123
+ s.description !== undefined);
75
124
  };
125
+ // Filter the list of all discoverable APIs to only include valid, known advanced services.
76
126
  const services = allServices.filter(isValidService).sort((a, b) => a.id.localeCompare(b.id));
77
127
  debug('Available services: %O', services);
78
128
  return services;
@@ -81,6 +131,17 @@ export class Services {
81
131
  handleApiError(error);
82
132
  }
83
133
  }
134
+ /**
135
+ * Enables a specified Google Advanced Service for the Apps Script project.
136
+ * This involves two steps:
137
+ * 1. Enabling the corresponding service in the Google Cloud Platform (GCP) project.
138
+ * 2. Updating the `appsscript.json` manifest file to include the service in its dependencies.
139
+ * @param {string} serviceName - The service ID (e.g., 'sheets', 'docs') of the service to enable.
140
+ * @returns {Promise<void>} A promise that resolves when the service is enabled.
141
+ * @throws {Error} If the service name is not provided, the manifest file doesn't exist,
142
+ * the service is not a valid advanced service, or if there's an API error or
143
+ * authentication/configuration issue.
144
+ */
84
145
  async enableService(serviceName) {
85
146
  var _a;
86
147
  debug('Enabling service %s', serviceName);
@@ -94,39 +155,59 @@ export class Services {
94
155
  const manifestExists = await hasReadWriteAccess(manifestPath);
95
156
  if (!manifestExists) {
96
157
  debug('Manifest file at %s does not exist', manifestPath);
97
- throw new Error('Manifest file does not exist.');
158
+ throw new Error('Manifest file does not exist.'); // Prerequisite: manifest must exist.
98
159
  }
160
+ // Find the service details from our list of known public advanced services.
99
161
  const advancedService = PUBLIC_ADVANCED_SERVICES.find(service => service.serviceId === serviceName);
100
162
  if (!advancedService) {
101
- throw new Error('Service is not a valid advanced service.');
163
+ throw new Error('Service is not a valid advanced service.'); // Ensure it's a known service.
102
164
  }
103
- // Do not update manifest if not valid advanced service
165
+ // Update the manifest file to include the new service.
104
166
  debug('Service is an advanced service, updating manifest');
105
167
  const content = await fs.readFile(manifestPath);
106
168
  const manifest = JSON.parse(content.toString());
169
+ // Ensure the dependencies structure exists and add the service if not already present.
107
170
  if ((_a = manifest.dependencies) === null || _a === void 0 ? void 0 : _a.enabledAdvancedServices) {
171
+ // Check if the service (by its userSymbol) is already in the manifest.
108
172
  if (manifest.dependencies.enabledAdvancedServices.findIndex(s => s.userSymbol === advancedService.userSymbol) === -1) {
109
173
  manifest.dependencies.enabledAdvancedServices.push(advancedService);
110
174
  }
111
175
  }
112
176
  else if (manifest.dependencies) {
177
+ // If 'dependencies' exists but 'enabledAdvancedServices' doesn't, create it.
113
178
  manifest.dependencies.enabledAdvancedServices = [advancedService];
114
179
  }
115
180
  else {
181
+ // If 'dependencies' itself doesn't exist, create the full structure.
116
182
  manifest.dependencies = { enabledAdvancedServices: [advancedService] };
117
183
  }
118
184
  debug('Updating manifest at %s with %j', manifestPath, manifest);
119
185
  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
186
+ // Enable the corresponding service in the GCP project via the Service Usage API.
120
187
  debug('Enabling GCP service %s.googleapis.com', serviceName);
121
188
  const serviceUsage = google.serviceusage({ version: 'v1', auth: this.options.credentials });
122
- const resourceName = `projects/${projectId}/services/${serviceName}.googleapis.com`;
189
+ const resourceName = `projects/${projectId}/services/${serviceName}.googleapis.com`; // Construct the service resource name.
123
190
  try {
124
191
  await serviceUsage.services.enable({ name: resourceName });
125
192
  }
126
193
  catch (error) {
194
+ // Note: If this GCP API call fails, the manifest will have been updated,
195
+ // but the service might not be enabled in GCP. This could lead to an inconsistent state.
196
+ // More robust error handling might involve reverting manifest changes or providing specific guidance.
127
197
  handleApiError(error);
128
198
  }
129
199
  }
200
+ /**
201
+ * Disables a specified Google Advanced Service for the Apps Script project.
202
+ * This involves two steps:
203
+ * 1. Disabling the corresponding service in the Google Cloud Platform (GCP) project.
204
+ * 2. Removing the service from the `appsscript.json` manifest file's dependencies.
205
+ * @param {string} serviceName - The service ID (e.g., 'sheets', 'docs') of the service to disable.
206
+ * @returns {Promise<void>} A promise that resolves when the service is disabled.
207
+ * @throws {Error} If the service name is not provided, the manifest file doesn't exist,
208
+ * the service is not a valid advanced service, or if there's an API error or
209
+ * authentication/configuration issue.
210
+ */
130
211
  async disableService(serviceName) {
131
212
  var _a;
132
213
  debug('Disabling service %s', serviceName);
@@ -140,30 +221,38 @@ export class Services {
140
221
  const manifestExists = await hasReadWriteAccess(manifestPath);
141
222
  if (!manifestExists) {
142
223
  debug('Manifest file at %s does not exist', manifestPath);
143
- throw new Error('Manifest file does not exist.');
224
+ throw new Error('Manifest file does not exist.'); // Prerequisite: manifest must exist.
144
225
  }
226
+ // Find the service details from our list of known public advanced services.
145
227
  const advancedService = PUBLIC_ADVANCED_SERVICES.find(service => service.serviceId === serviceName);
146
228
  if (!advancedService) {
147
- throw new Error('Service is not a valid advanced service.');
229
+ throw new Error('Service is not a valid advanced service.'); // Ensure it's a known service.
148
230
  }
149
- // Do not update manifest if not valid advanced service
231
+ // Update the manifest file to remove the service.
150
232
  debug('Service is an advanced service, updating manifest');
151
233
  const content = await fs.readFile(manifestPath);
152
234
  const manifest = JSON.parse(content.toString());
235
+ // If dependencies or enabledAdvancedServices array doesn't exist, or service not found, nothing to do for manifest.
153
236
  if (!((_a = manifest.dependencies) === null || _a === void 0 ? void 0 : _a.enabledAdvancedServices)) {
154
- debug('Service enabled in manifest, skipping manifest update');
155
- return;
237
+ debug('Service not listed as enabled in manifest, skipping manifest update for disabling.');
238
+ // Continue to attempt disabling at GCP level, as manifest might be out of sync.
156
239
  }
157
- manifest.dependencies.enabledAdvancedServices = manifest.dependencies.enabledAdvancedServices.filter(service => service.serviceId !== serviceName);
158
- debug('Updating manifest at %s with %j', manifestPath, manifest);
159
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
240
+ else {
241
+ // Filter out the service to be disabled.
242
+ manifest.dependencies.enabledAdvancedServices = manifest.dependencies.enabledAdvancedServices.filter(service => service.serviceId !== serviceName);
243
+ debug('Updating manifest at %s with %j', manifestPath, manifest);
244
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
245
+ }
246
+ // Disable the corresponding service in the GCP project via the Service Usage API.
160
247
  debug('Disabling GCP service %s.googleapis.com', serviceName);
161
248
  const serviceUsage = google.serviceusage({ version: 'v1', auth: this.options.credentials });
162
- const resourceName = `projects/${projectId}/services/${serviceName}.googleapis.com`;
249
+ const resourceName = `projects/${projectId}/services/${serviceName}.googleapis.com`; // Construct the service resource name.
163
250
  try {
164
251
  await serviceUsage.services.disable({ name: resourceName });
165
252
  }
166
253
  catch (error) {
254
+ // Note: Similar to enableService, if this GCP API call fails, the manifest might have been updated,
255
+ // potentially leading to an inconsistent state (service disabled in manifest but still active in GCP).
167
256
  handleApiError(error);
168
257
  }
169
258
  }
@@ -1,6 +1,28 @@
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 provides utility types, assertion functions, and helper functions
15
+ // (like for API pagination and error handling) used across the core modules
16
+ // of clasp.
1
17
  import Debug from 'debug';
2
18
  import { GaxiosError } from 'googleapis-common';
3
19
  const debug = Debug('clasp:core');
20
+ /**
21
+ * Asserts that the provided ClaspOptions include credentials.
22
+ * Throws an error if credentials are not set. This also acts as a type guard.
23
+ * @param {ClaspOptions} options - The Clasp options to check.
24
+ * @throws {Error} If `options.credentials` is not set.
25
+ */
4
26
  export function assertAuthenticated(options) {
5
27
  if (!options.credentials) {
6
28
  debug('Credentials not set in options: %O', options);
@@ -11,6 +33,13 @@ export function assertAuthenticated(options) {
11
33
  });
12
34
  }
13
35
  }
36
+ /**
37
+ * Asserts that the provided ClaspOptions include essential script project configurations.
38
+ * Throws an error if `scriptId`, `projectRootDir`, `configFilePath`, or `contentDir` are missing.
39
+ * This also acts as a type guard.
40
+ * @param {ClaspOptions} options - The Clasp options to check.
41
+ * @throws {Error} If essential script configurations are missing.
42
+ */
14
43
  export function assertScriptConfigured(options) {
15
44
  var _a;
16
45
  if (!((_a = options.project) === null || _a === void 0 ? void 0 : _a.scriptId) ||
@@ -25,6 +54,12 @@ export function assertScriptConfigured(options) {
25
54
  });
26
55
  }
27
56
  }
57
+ /**
58
+ * Asserts that the provided ClaspOptions include a GCP project ID, in addition to base script configurations.
59
+ * Throws an error if `projectId` is missing. This also acts as a type guard.
60
+ * @param {ClaspOptions} options - The Clasp options to check.
61
+ * @throws {Error} If `options.project.projectId` is not set.
62
+ */
28
63
  export function assertGcpProjectConfigured(options) {
29
64
  var _a;
30
65
  assertScriptConfigured(options);
@@ -74,6 +109,11 @@ export async function fetchWithPages(fn, options) {
74
109
  partialResults: pageToken !== undefined,
75
110
  };
76
111
  }
112
+ /**
113
+ * Checks if an error object is a GaxiosError with detailed error information.
114
+ * @param {unknown} error - The error object to check.
115
+ * @returns {boolean} True if the error is a GaxiosError with details, false otherwise.
116
+ */
77
117
  function isDetailedError(error) {
78
118
  if (!error) {
79
119
  return false;
@@ -93,13 +133,20 @@ const ERROR_CODES = {
93
133
  403: 'NOT_AUTHORIZED',
94
134
  404: 'NOT_FOUND',
95
135
  };
136
+ /**
137
+ * Standardized error handler for Google API errors (GaxiosError).
138
+ * It extracts a meaningful message and error code, then re-throws a new error.
139
+ * @param {unknown} error - The error received from a Google API call.
140
+ * @throws {Error} A new error with a structured cause including the original error,
141
+ * a clasp-specific error code, and the message.
142
+ */
96
143
  export function handleApiError(error) {
97
144
  var _a;
98
145
  debug('Handling API error: %O', error);
99
146
  if (!(error instanceof GaxiosError)) {
100
147
  throw new Error('Unexpected error', {
101
148
  cause: {
102
- code: 'UNEPECTED_ERROR',
149
+ code: 'UNEXPECTED_ERROR',
103
150
  message: new String(error),
104
151
  error: error,
105
152
  },
@@ -119,6 +166,15 @@ export function handleApiError(error) {
119
166
  },
120
167
  });
121
168
  }
169
+ /**
170
+ * Ensures that a value is an array of strings.
171
+ * If the input is a single string, it's wrapped in an array.
172
+ * If it's already an array of strings, it's returned as is.
173
+ * If it's an array containing non-string elements, those elements are filtered out.
174
+ * If the input is neither a string nor an array, an empty array is returned.
175
+ * @param {string | string[]} value - The value to process.
176
+ * @returns {string[]} An array of strings.
177
+ */
122
178
  export function ensureStringArray(value) {
123
179
  if (typeof value === 'string') {
124
180
  return [value];