@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
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Clasp
2
2
 
3
+ Note: This is not an officially support Google product.
4
+
3
5
  ![build status](https://github.com/google/clasp/actions/workflows/ci.yaml/badge.svg)
4
6
  <a href="https://coveralls.io/github/google/clasp?branch=master"><img src="https://coveralls.io/repos/github/google/clasp/badge.svg?branch=master" alt="Coverage Status"></a>
5
7
  <a href="https://www.npmjs.com/package/@google/clasp"><img src="https://img.shields.io/npm/v/@google/clasp.svg" alt="npm Version"></a>
@@ -64,6 +66,18 @@ Then enable the Google Apps Script API: https://script.google.com/home/usersetti
64
66
 
65
67
  ![Enable Apps Script API](https://user-images.githubusercontent.com/744973/54870967-a9135780-4d6a-11e9-991c-9f57a508bdf0.gif)
66
68
 
69
+ ### Installing as a Gemini CLI Extension
70
+
71
+ You can install clasp as an Gemini CLI extensions using the following command:
72
+
73
+ ```sh
74
+ gemini extensions install https://github.com/google/clasp
75
+ ```
76
+
77
+ This makes clasp available as an MCP server in Gemini CLI.
78
+
79
+ Make sure to enable the Google Apps Script API (as explained above) and perform a `clasp login` (with your specific login parameters) before you use the extension.
80
+
67
81
  ## Commands
68
82
 
69
83
  The following command provide basic Apps Script project management.
@@ -78,6 +92,7 @@ clasp
78
92
  - [`clasp logout`](#logout)
79
93
  - [`clasp create-script [--title <title>] [--type <type>] [--rootDir <dir>] [--parentId <id>]`](#create)
80
94
  - [`clasp clone-script <scriptId | scriptURL> [versionNumber] [--rootDir <dir>]`](#clone)
95
+ - [`clasp delete-script [--force]`](#delete)
81
96
  - [`clasp pull [--versionNumber]`](#pull)
82
97
  - [`clasp push [--watch] [--force]`](#push)
83
98
  - [`clasp show-file-status [--json]`](#status)
@@ -313,6 +328,7 @@ a `.claspignore` file, set this option to true.
313
328
  - `--project <file>`: Reads project settings from a file other than `.clasp.json`. Intended to support multiple deployment targets.
314
329
  - `--auth <file>`: (**DEPRECATED**) Reads credentials from a file other than `.clasprc.json`. Use the `--user` option to maintain multiple authorized accounts.
315
330
  - `--ignore <file>`: Reads ignore patterns from a file other than `.claspignore`.
331
+ - `--json`: Show output in JSON format.
316
332
 
317
333
  ### Login
318
334
 
@@ -386,6 +402,19 @@ Clones the script project from script.google.com.
386
402
  - `clasp clone-script "https://script.google.com/d/15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC/edit"`
387
403
  - `clasp clone-script "15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC" --rootDir ./src`
388
404
 
405
+ ### Delete
406
+
407
+ Interactively deletes a script or a project and the `.clasp.json` file. Prompt the user for confirmation if the --force option is not specified.
408
+
409
+ #### Options
410
+
411
+ - `-f` `--force`: Bypass any confirmation messages. It’s not a good idea to do this unless you want to run clasp from a script.
412
+
413
+ #### Examples
414
+
415
+ - `clasp delete-script`
416
+ - `clasp delete-script -f`
417
+
389
418
  ### Pull
390
419
 
391
420
  Fetches a project from either a provided or saved script ID.
@@ -462,7 +491,8 @@ List deployments of a script.
462
491
 
463
492
  #### Examples
464
493
 
465
- - `clasp list-deployments`
494
+ - `clasp list-deployments`: List all deployments for the current project
495
+ - `clasp list-deployments [scriptId]`: List all deployments for a script ID
466
496
 
467
497
  ### Deploy
468
498
 
@@ -531,9 +561,12 @@ Creates an immutable version of the script.
531
561
 
532
562
  List versions of a script.
533
563
 
564
+ #### Options
565
+
534
566
  #### Examples
535
567
 
536
- - `clasp list-versions`
568
+ - `clasp list-versions`: List all versions for the current project
569
+ - `clasp list-versions [scriptId]`: List all versions for a script ID
537
570
 
538
571
  ### List
539
572
 
@@ -1,3 +1,18 @@
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 contains functions for initializing and managing authentication,
15
+ // including OAuth2 client creation, authorization flows, and credential storage.
1
16
  import { readFileSync } from 'fs';
2
17
  import os from 'os';
3
18
  import path from 'path';
@@ -8,6 +23,14 @@ import { FileCredentialStore } from './file_credential_store.js';
8
23
  import { LocalServerAuthorizationCodeFlow } from './localhost_auth_code_flow.js';
9
24
  import { ServerlessAuthorizationCodeFlow } from './serverless_auth_code_flow.js';
10
25
  const debug = Debug('clasp:auth');
26
+ /**
27
+ * Initializes authentication, loading credentials if available or preparing for a new auth flow.
28
+ * @param {InitOptions} options - Options for initializing authentication.
29
+ * @param {string} [options.authFilePath] - Path to the credentials file. Defaults to ~/.clasprc.json.
30
+ * @param {string} [options.userKey] - Identifier for the user credentials to load. Defaults to 'default'.
31
+ * @param {boolean} [options.useApplicationDefaultCredentials] - Whether to use Application Default Credentials.
32
+ * @returns {Promise<AuthInfo>} An AuthInfo object with the credential store and potentially loaded credentials.
33
+ */
11
34
  export async function initAuth(options) {
12
35
  var _a, _b, _c;
13
36
  const authFilePath = (_a = options.authFilePath) !== null && _a !== void 0 ? _a : path.join(os.homedir(), '.clasprc.json');
@@ -28,6 +51,12 @@ export async function initAuth(options) {
28
51
  user: (_c = options.userKey) !== null && _c !== void 0 ? _c : 'default',
29
52
  };
30
53
  }
54
+ /**
55
+ * Fetches user information (email, ID) using the provided OAuth2 client.
56
+ * @param {OAuth2Client} credentials - An authorized OAuth2 client.
57
+ * @returns {Promise<{email?: string | null; id?: string | null} | undefined>}
58
+ * User's email and ID, or undefined if an error occurs or no data is returned.
59
+ */
31
60
  export async function getUserInfo(credentials) {
32
61
  debug('Fetching user info');
33
62
  const api = google.oauth2('v2');
@@ -50,8 +79,9 @@ export async function getUserInfo(credentials) {
50
79
  /**
51
80
  * Creates an an unauthorized oauth2 client given the client secret file. If no path is provided,
52
81
  * teh default client is returned.
53
- * @param clientSecretPath
54
- * @returns
82
+ * @param {string} [clientSecretPath] - Optional path to a client secrets JSON file.
83
+ * If not provided, the default clasp OAuth client is used.
84
+ * @returns {OAuth2Client} An unauthorized OAuth2 client instance.
55
85
  */
56
86
  export function getUnauthorizedOuth2Client(clientSecretPath) {
57
87
  if (clientSecretPath) {
@@ -61,8 +91,11 @@ export function getUnauthorizedOuth2Client(clientSecretPath) {
61
91
  }
62
92
  /**
63
93
  * Create an authorized oauth2 client from saved credentials.
64
- * @param userKey
65
- * @returns
94
+ * @param {CredentialStore} store - The credential store to load from.
95
+ * @param {string} [userKey='default'] - The user key for the credentials.
96
+ * @returns {Promise<OAuth2Client | undefined>} An authorized OAuth2 client if credentials
97
+ * are found and valid, otherwise undefined. The client is configured to auto-refresh
98
+ * tokens and save them back to the store.
66
99
  */
67
100
  export async function getAuthorizedOAuth2Client(store, userKey) {
68
101
  if (!userKey) {
@@ -89,12 +122,10 @@ export async function getAuthorizedOAuth2Client(store, userKey) {
89
122
  return client;
90
123
  }
91
124
  /**
92
- * Requests authorization to manage Apps Script projects.
93
- * @param {boolean} useLocalhost Uses a local HTTP server if true. Manual entry o.w.
94
- * @param {ClaspCredentials?} creds An optional credentials object.
95
- * @param {string[]} [scopes=[]] List of OAuth scopes to authorize.
96
- * @param {number?} redirectPort Optional custom port for the local HTTP server during the authorization process.
97
- * If not specified, a random available port will be used.
125
+ * Initiates an OAuth 2.0 authorization flow to obtain user consent and credentials.
126
+ * It selects between a local server flow or a serverless (manual) flow based on options.
127
+ * @param {AuthorizationOptions} options - Configuration for the authorization flow.
128
+ * @returns {Promise<OAuth2Client>} The authorized OAuth2 client.
98
129
  */
99
130
  export async function authorize(options) {
100
131
  let flow;
@@ -111,6 +142,13 @@ export async function authorize(options) {
111
142
  debug('Auth complete');
112
143
  return client;
113
144
  }
145
+ /**
146
+ * Saves the obtained OAuth2 client credentials to the provided credential store.
147
+ * It also sets up an event listener on the client to save refreshed tokens.
148
+ * @param {CredentialStore} store - The credential store.
149
+ * @param {string} userKey - The user key for saving credentials.
150
+ * @param {OAuth2Client} oauth2Client - The OAuth2 client whose credentials are to be saved.
151
+ */
114
152
  async function saveOauthClientCredentials(store, userKey, oauth2Client) {
115
153
  var _a, _b;
116
154
  const savedCredentials = {
@@ -177,6 +215,12 @@ function createDefaultOAuthClient() {
177
215
  debug('Created built-in oauth client, id: %s', client._clientId);
178
216
  return client;
179
217
  }
218
+ /**
219
+ * Attempts to create an OAuth2Client using Google Application Default Credentials (ADC).
220
+ * This is typically used in server environments where credentials can be automatically discovered.
221
+ * @returns {Promise<OAuth2Client | undefined>} An OAuth2Client if ADC are available and valid,
222
+ * otherwise undefined.
223
+ */
180
224
  export async function createApplicationDefaultCredentials() {
181
225
  const defaultCreds = await new GoogleAuth({
182
226
  scopes: [
@@ -1,7 +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
+ /**
15
+ * Base class for managing the OAuth 2.0 Authorization Code Flow.
16
+ * It provides common logic for generating authorization URLs,
17
+ * handling user authorization, and exchanging codes for tokens.
18
+ * Specific implementations will override methods to define how the
19
+ * redirect URI is obtained and how the user is prompted for the code.
20
+ */
1
21
  export class AuthorizationCodeFlow {
2
22
  constructor(oauth2client) {
3
23
  this.oauth2Client = oauth2client;
4
24
  }
25
+ /**
26
+ * Initiates the authorization process.
27
+ * This method generates an authorization URL, prompts the user for authorization,
28
+ * exchanges the authorization code for tokens, and sets the credentials
29
+ * on the OAuth2 client.
30
+ * @param {string | string[]} scopes - The scope(s) for which authorization is requested.
31
+ * @returns {Promise<OAuth2Client>} The authorized OAuth2 client.
32
+ */
5
33
  async authorize(scopes) {
6
34
  const scope = Array.isArray(scopes) ? scopes.join(' ') : scopes;
7
35
  const redirectUri = await this.getRedirectUri();
@@ -18,13 +46,36 @@ export class AuthorizationCodeFlow {
18
46
  this.oauth2Client.setCredentials(tokens.tokens);
19
47
  return this.oauth2Client;
20
48
  }
49
+ /**
50
+ * Abstract method to get the redirect URI.
51
+ * Subclasses must implement this to provide the specific redirect URI
52
+ * for their authorization flow.
53
+ * @returns {Promise<string>} The redirect URI.
54
+ * @throws {Error} If not implemented by the subclass.
55
+ */
21
56
  async getRedirectUri() {
22
57
  throw new Error('Not implemented');
23
58
  }
59
+ /**
60
+ * Abstract method to prompt the user for the authorization code.
61
+ * Subclasses must implement this to define how the user is prompted
62
+ * (e.g., via a local server, manual input).
63
+ * @param {string} _authorizationUrl - The URL to which the user should be directed
64
+ * for authorization.
65
+ * @returns {Promise<string>} The authorization code obtained from the user.
66
+ * @throws {Error} If not implemented by the subclass.
67
+ */
24
68
  async promptAndReturnCode(_authorizationUrl) {
25
69
  throw new Error('Not implemented');
26
70
  }
27
71
  }
72
+ /**
73
+ * Parses an authorization response URL (typically from a redirect)
74
+ * to extract the authorization code or an error.
75
+ * @param {string} url - The full URL from the authorization server's redirect.
76
+ * @returns {{code: string | null; error: string | null}} An object containing
77
+ * the 'code' if successful, or an 'error' if the authorization failed.
78
+ */
28
79
  export function parseAuthResponseUrl(url) {
29
80
  const urlParts = new URL(url, 'http://localhost/').searchParams;
30
81
  const code = urlParts.get('code');
@@ -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 {};
@@ -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 implements the `CredentialStore` interface, providing a file-based
15
+ // mechanism for storing and managing user credentials. It handles different
16
+ // file formats for compatibility with older versions of clasp.
1
17
  import fs from 'fs';
2
18
  function hasLegacyLocalCredentials(store) {
3
19
  return store.token && store.oauth2ClientSettings;
@@ -5,10 +21,23 @@ function hasLegacyLocalCredentials(store) {
5
21
  function hasLegacyGlobalCredentials(store) {
6
22
  return !!store.access_token;
7
23
  }
24
+ /**
25
+ * Implements the `CredentialStore` interface using a local JSON file.
26
+ * This class handles saving, loading, and deleting OAuth 2.0 credentials
27
+ * for different users. It also supports migrating credentials from older
28
+ * clasp file formats.
29
+ */
8
30
  export class FileCredentialStore {
9
31
  constructor(filePath) {
10
32
  this.filePath = filePath;
11
33
  }
34
+ /**
35
+ * Saves credentials for a given user.
36
+ * If credentials are provided as undefined, it effectively removes the user's credentials.
37
+ * @param {string} user - The identifier for the user.
38
+ * @param {StoredCredential | undefined} credentials - The credentials to save, or undefined to clear.
39
+ * @returns {Promise<void>}
40
+ */
12
41
  async save(user, credentials) {
13
42
  const store = this.readFile();
14
43
  if (!store.tokens) {
@@ -17,6 +46,12 @@ export class FileCredentialStore {
17
46
  store.tokens[user] = credentials;
18
47
  this.writeFile(store);
19
48
  }
49
+ /**
50
+ * Deletes credentials for a specific user.
51
+ * If deleting the 'default' user, it also cleans up legacy credential formats.
52
+ * @param {string} user - The identifier for the user whose credentials are to be deleted.
53
+ * @returns {Promise<void>}
54
+ */
20
55
  async delete(user) {
21
56
  let store = this.readFile();
22
57
  if (!store.tokens) {
@@ -24,41 +59,61 @@ export class FileCredentialStore {
24
59
  }
25
60
  store.tokens[user] = undefined;
26
61
  if (user === 'default') {
27
- // Remove legacy keys if default user
62
+ // If the 'default' user's token is deleted, we also clean up any potential
63
+ // top-level V1 credential keys to ensure a clean state and prevent
64
+ // V1 credentials from being loaded unintentionally after a V3 'default' delete.
28
65
  store = {
29
- tokens: store.tokens,
66
+ tokens: store.tokens, // Keep other named tokens if they exist
30
67
  };
31
68
  }
32
69
  this.writeFile(store);
33
70
  }
71
+ /**
72
+ * Deletes all stored credentials by clearing the tokens map.
73
+ * @returns {Promise<void>}
74
+ */
34
75
  async deleteAll() {
35
76
  await this.writeFile({
36
77
  tokens: {},
37
78
  });
38
79
  }
80
+ /**
81
+ * Loads credentials for a given user.
82
+ * It supports loading credentials from the current format as well as
83
+ * attempting to load from legacy V1 local and global file formats
84
+ * if the user is 'default' and no V3 credentials are found.
85
+ * @param {string} user - The identifier for the user.
86
+ * @returns {Promise<StoredCredential | null>} The stored credentials if found, otherwise null.
87
+ */
39
88
  async load(user) {
40
89
  var _a, _b, _c;
41
90
  const store = this.readFile();
42
91
  const credentials = (_a = store.tokens) === null || _a === void 0 ? void 0 : _a[user];
43
92
  if (credentials) {
44
- return credentials;
93
+ return credentials; // Modern V3 token found for the user.
45
94
  }
95
+ // The following logic attempts to load legacy V1 credentials
96
+ // ONLY if the requested user is 'default' and no V3 'default' token was found.
46
97
  if (user !== 'default') {
47
- return null;
98
+ return null; // For non-default users, only V3 tokens are considered.
48
99
  }
100
+ // Check for V1 local file format (usually from older .clasprc.json in project root)
49
101
  if (hasLegacyLocalCredentials(store)) {
50
- // Support previous un
102
+ // Convert V1 local format to StoredCredential format.
51
103
  return {
52
104
  type: 'authorized_user',
53
- ...store.token,
105
+ ...store.token, // Spread V1 token properties
54
106
  client_id: (_b = store.oauth2ClientSettings) === null || _b === void 0 ? void 0 : _b.clientId,
55
107
  client_secret: (_c = store.oauth2ClientSettings) === null || _c === void 0 ? void 0 : _c.clientSecret,
56
108
  };
57
109
  }
110
+ // Check for V1 global file format (usually from older ~/.clasprc.json)
58
111
  if (hasLegacyGlobalCredentials(store)) {
112
+ // Convert V1 global format to StoredCredential format.
113
+ // Note: Default client_id and client_secret are used here as global V1 didn't store them.
59
114
  return {
60
115
  type: 'authorized_user',
61
- access_token: store.access_token,
116
+ access_token: store.access_token, // Map V1 fields
62
117
  refresh_token: store.refresh_token,
63
118
  expiry_date: store.exprity_date,
64
119
  token_type: store.token_type,
@@ -1,18 +1,48 @@
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 implements the `AuthorizationCodeFlow` for local development
15
+ // environments. It starts a local HTTP server to receive the authorization
16
+ // code after the user grants permission.
1
17
  import { createServer } from 'http';
2
18
  import open from 'open';
3
19
  import enableDestroy from 'server-destroy';
4
20
  import { intl } from '../intl.js';
5
21
  import { AuthorizationCodeFlow, parseAuthResponseUrl } from './auth_code_flow.js';
22
+ /**
23
+ * Implements the Authorization Code Flow by starting a local HTTP server
24
+ * to act as the redirect URI. This is suitable for CLI environments
25
+ * where a browser can be opened and a local server can receive the
26
+ * authorization code.
27
+ */
6
28
  export class LocalServerAuthorizationCodeFlow extends AuthorizationCodeFlow {
7
29
  constructor(oauth2client) {
8
30
  super(oauth2client);
9
31
  this.port = 0;
10
32
  }
33
+ /**
34
+ * Starts a local HTTP server and returns its address as the redirect URI.
35
+ * The server will listen on the configured port (or a random available port if 0).
36
+ * @returns {Promise<string>} The local redirect URI (e.g., "http://localhost:1234").
37
+ * @throws {Error} If the server cannot be started (e.g., port in use).
38
+ */
11
39
  async getRedirectUri() {
12
40
  this.server = await new Promise((resolve, reject) => {
13
41
  const s = createServer();
14
- enableDestroy(s);
42
+ enableDestroy(s); // Allows the server to be destroyed gracefully.
43
+ // Try to listen on the specified port (or a random one if port is 0).
15
44
  s.listen(this.port, () => resolve(s)).on('error', (err) => {
45
+ // Handle common server errors like port already in use.
16
46
  if (err.code === 'EADDRINUSE') {
17
47
  const msg = intl.formatMessage({ id: "smVcjx", defaultMessage: [{ type: 0, value: "Error: Port " }, { type: 1, value: "port" }, { type: 0, value: " is already in use. Please specify a different port with --redirect-port" }] }, {
18
48
  port: this.port,
@@ -32,6 +62,15 @@ export class LocalServerAuthorizationCodeFlow extends AuthorizationCodeFlow {
32
62
  const { port } = this.server.address();
33
63
  return `http://localhost:${port}`;
34
64
  }
65
+ /**
66
+ * Prompts the user to authorize by opening the provided authorization URL
67
+ * in their default web browser. It then waits for the local server (started by
68
+ * `getRedirectUri`) to receive the callback containing the authorization code.
69
+ * @param {string} authorizationUrl - The URL to open for user authorization.
70
+ * @returns {Promise<string>} The authorization code extracted from the redirect.
71
+ * @throws {Error} If the server is not started, the request URL is missing, or an error
72
+ * parameter is present in the redirect URL.
73
+ */
35
74
  async promptAndReturnCode(authorizationUrl) {
36
75
  return await new Promise((resolve, reject) => {
37
76
  if (!this.server) {
@@ -43,21 +82,24 @@ export class LocalServerAuthorizationCodeFlow extends AuthorizationCodeFlow {
43
82
  reject(new Error('Missing URL in request'));
44
83
  return;
45
84
  }
46
- const { code, error } = parseAuthResponseUrl(request.url);
85
+ const { code, error } = parseAuthResponseUrl(request.url); // Extract code or error from the redirect URL.
47
86
  if (code) {
48
- resolve(code);
87
+ resolve(code); // Successfully obtained the authorization code.
49
88
  }
50
89
  else {
51
- reject(error);
90
+ reject(error); // An error occurred during authorization.
52
91
  }
92
+ // Send a simple response to the browser.
53
93
  const msg = intl.formatMessage({ id: "ZT8LeG", defaultMessage: [{ type: 0, value: "Logged in! You may close this page." }] });
54
94
  response.end(msg);
55
95
  });
96
+ // Open the authorization URL in the user's default browser.
56
97
  void open(authorizationUrl);
98
+ // Log the authorization URL to the console as a fallback or for visibility.
57
99
  const msg = intl.formatMessage({ id: "NbCrKh", defaultMessage: [{ type: 0, value: "`\uD83D\uDD11 Authorize clasp by visiting this url: " }, { type: 1, value: "url" }] }, {
58
100
  url: authorizationUrl,
59
101
  });
60
102
  console.log(msg);
61
- }).finally(() => { var _a; return (_a = this.server) === null || _a === void 0 ? void 0 : _a.destroy(); });
103
+ }).finally(() => { var _a; return (_a = this.server) === null || _a === void 0 ? void 0 : _a.destroy(); }); // Ensure the server is destroyed after completion or error.
62
104
  }
63
105
  }
@@ -1,20 +1,57 @@
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
  import inquirer from 'inquirer';
2
15
  import { intl } from '../intl.js';
3
16
  import { AuthorizationCodeFlow, parseAuthResponseUrl } from './auth_code_flow.js';
17
+ /**
18
+ * Implements the Authorization Code Flow for environments where a local
19
+ * server cannot be started (e.g., serverless functions, some CI environments).
20
+ * It prompts the user to manually open the authorization URL in a browser
21
+ * on another device and then paste the resulting redirect URL (containing
22
+ * the authorization code) back into the CLI.
23
+ */
4
24
  export class ServerlessAuthorizationCodeFlow extends AuthorizationCodeFlow {
5
25
  constructor(oauth2client) {
6
26
  super(oauth2client);
7
27
  }
28
+ /**
29
+ * Returns a hardcoded redirect URI.
30
+ * This URI is typically configured in the OAuth client settings in GCP Console
31
+ * and is used by Google's authorization server to redirect the user after
32
+ * successful authorization.
33
+ * @returns {Promise<string>} The redirect URI.
34
+ */
8
35
  async getRedirectUri() {
9
36
  return 'http://localhost:8888';
10
37
  }
38
+ /**
39
+ * Prompts the user to manually open the authorization URL in a browser
40
+ * on another device and then paste the resulting redirect URL (which contains
41
+ * the authorization code) back into the CLI.
42
+ * @param {string} authorizationUrl - The URL to display to the user for authorization.
43
+ * @returns {Promise<string>} The authorization code extracted from the URL pasted by the user.
44
+ * @throws {Error} If the pasted URL contains an error or no code.
45
+ */
11
46
  async promptAndReturnCode(authorizationUrl) {
12
- const prompt = intl.formatMessage({ id: "Tx67iE", defaultMessage: [{ type: 0, value: "Authorize clasp by visiting the following URL on another device: " }, { type: 1, value: "url" }, { type: 0, value: " After authorization, copy the URL in the browser. Enter the URL from your browser after completing authorization" }] }, {
47
+ const urlMessage = intl.formatMessage({ id: "7EHKbR", defaultMessage: [{ type: 0, value: "\uD83D\uDD11 Authorize clasp by visiting this url: " }, { type: 1, value: "url" }] }, {
13
48
  url: authorizationUrl,
14
49
  });
50
+ console.log(urlMessage);
51
+ const promptMessage = intl.formatMessage({ id: "xADuBP", defaultMessage: [{ type: 0, value: "After authorizing, copy the URL from your browser and paste it here:" }] });
15
52
  const answer = await inquirer.prompt([
16
53
  {
17
- message: prompt,
54
+ message: promptMessage,
18
55
  name: 'url',
19
56
  type: 'input',
20
57
  },
@@ -1,3 +1,17 @@
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 'clone-script' command for the clasp CLI.
1
15
  import { Command } from 'commander';
2
16
  import inquirer from 'inquirer';
3
17
  import { intl } from '../intl.js';
@@ -9,23 +23,28 @@ export const command = new Command('clone-script')
9
23
  .option('--rootDir <rootDir>', 'Local root directory in which clasp will store your project files.')
10
24
  .action(async function (scriptId, versionNumber) {
11
25
  var _a;
12
- let clasp = this.opts().clasp;
26
+ const options = this.optsWithGlobals();
27
+ let clasp = options.clasp;
13
28
  if (clasp.project.exists()) {
14
29
  const msg = intl.formatMessage({ id: "kk5+4G", defaultMessage: [{ type: 0, value: "Project file already exists." }] });
15
30
  this.error(msg);
16
31
  }
17
- const rootDir = this.opts().rootDir;
32
+ const rootDir = options.rootDir;
18
33
  clasp.withContentDir(rootDir !== null && rootDir !== void 0 ? rootDir : '.');
34
+ // Determine the script ID to clone.
35
+ // Priority: 1. Directly provided scriptId argument. 2. Extracted from a URL. 3. Selected from a list in interactive mode.
19
36
  if (scriptId) {
37
+ // If a scriptId is provided, check if it's a full URL and extract the ID.
20
38
  const match = scriptId.match(/https:\/\/script\.google\.com\/d\/([^/]+)\/.*/);
21
39
  if (match) {
22
- scriptId = match[1];
40
+ scriptId = match[1]; // Use the extracted ID from the URL.
23
41
  }
24
42
  else {
25
- scriptId = scriptId.trim();
43
+ scriptId = scriptId.trim(); // Otherwise, use the provided ID as is (after trimming).
26
44
  }
27
45
  }
28
46
  else if (isInteractive()) {
47
+ // If no scriptId is provided and the session is interactive, prompt the user to choose.
29
48
  const projects = await clasp.project.listScripts();
30
49
  const choices = projects.results.map(file => ({
31
50
  name: `${file.name.padEnd(20)} - https://script.google.com/d/${file.id}/edit`,
@@ -49,12 +68,23 @@ export const command = new Command('clone-script')
49
68
  }
50
69
  try {
51
70
  const cloningScriptMsg = intl.formatMessage({ id: "UTMHnH", defaultMessage: [{ type: 0, value: "Cloning script..." }] });
71
+ // Perform the cloning operation:
72
+ // 1. Configure the clasp instance with the determined script ID.
73
+ // 2. Pull the files from the remote project (optionally a specific version).
74
+ // 3. Update the local .clasp.json project settings file.
52
75
  const files = await withSpinner(cloningScriptMsg, async () => {
53
76
  clasp = clasp.withScriptId(scriptId);
54
77
  const files = await clasp.files.pull(versionNumber);
78
+ // After successfully pulling files, update the local project settings (e.g., .clasp.json)
79
+ // to reflect the cloned scriptId and other relevant configurations.
55
80
  clasp.project.updateSettings();
56
81
  return files;
57
82
  });
83
+ if (options.json) {
84
+ console.log(JSON.stringify({ scriptId, files: files.map(f => f.localPath) }, null, 2));
85
+ return;
86
+ }
87
+ // Log the paths of the cloned files.
58
88
  files.forEach(f => console.log(`└─ ${f.localPath}`));
59
89
  const successMessage = intl.formatMessage({ id: "XABSyD", defaultMessage: [{ type: 0, value: "Cloned " }, { type: 6, value: "count", options: { "=0": { value: [{ type: 0, value: "no files." }] }, one: { value: [{ type: 0, value: "one file." }] }, other: { value: [{ type: 7 }, { type: 0, value: " files" }] } }, offset: 0, pluralType: "cardinal" }, { type: 0, value: "." }] }, {
60
90
  count: files.length,
@@ -62,10 +92,12 @@ export const command = new Command('clone-script')
62
92
  console.log(successMessage);
63
93
  }
64
94
  catch (error) {
95
+ // Handle specific error codes from the API, like an invalid script ID.
65
96
  if (((_a = error.cause) === null || _a === void 0 ? void 0 : _a.code) === 'INVALID_ARGUMENT') {
66
97
  const msg = intl.formatMessage({ id: "jRe7cT", defaultMessage: [{ type: 0, value: "Invalid script ID." }] });
67
- this.error(msg);
98
+ this.error(msg); // Output a user-friendly error and exit.
68
99
  }
100
+ // For other errors, rethrow to be caught by the global error handler.
69
101
  throw error;
70
102
  }
71
103
  });