@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.
- package/README.md +35 -2
- package/build/src/auth/auth.js +54 -10
- package/build/src/auth/auth_code_flow.js +51 -0
- package/build/src/auth/credential_store.js +13 -0
- package/build/src/auth/file_credential_store.js +62 -7
- package/build/src/auth/localhost_auth_code_flow.js +47 -5
- package/build/src/auth/serverless_auth_code_flow.js +39 -2
- package/build/src/commands/clone-script.js +37 -5
- package/build/src/commands/create-deployment.js +31 -6
- package/build/src/commands/create-script.js +65 -24
- package/build/src/commands/create-version.js +21 -1
- package/build/src/commands/delete-deployment.js +36 -5
- package/build/src/commands/delete-script.js +41 -0
- package/build/src/commands/disable-api.js +20 -1
- package/build/src/commands/enable-api.js +20 -1
- package/build/src/commands/list-apis.js +24 -1
- package/build/src/commands/list-deployments.js +35 -5
- package/build/src/commands/list-scripts.js +26 -2
- package/build/src/commands/list-versions.js +35 -7
- package/build/src/commands/login.js +36 -10
- package/build/src/commands/logout.js +23 -1
- package/build/src/commands/open-apis.js +20 -1
- package/build/src/commands/open-container.js +20 -1
- package/build/src/commands/open-credentials.js +20 -1
- package/build/src/commands/open-logs.js +20 -1
- package/build/src/commands/open-script.js +20 -1
- package/build/src/commands/open-webapp.js +20 -1
- package/build/src/commands/program.js +48 -7
- package/build/src/commands/pull.js +54 -13
- package/build/src/commands/push.js +49 -9
- package/build/src/commands/run-function.js +56 -13
- package/build/src/commands/setup-logs.js +20 -1
- package/build/src/commands/show-authorized-user.js +29 -2
- package/build/src/commands/show-file-status.js +17 -2
- package/build/src/commands/start-mcp.js +17 -1
- package/build/src/commands/tail-logs.js +20 -5
- package/build/src/commands/update-deployment.js +32 -6
- package/build/src/commands/utils.js +68 -0
- package/build/src/constants.js +15 -0
- package/build/src/core/apis.js +13 -3
- package/build/src/core/clasp.js +71 -12
- package/build/src/core/files.js +135 -32
- package/build/src/core/functions.js +36 -0
- package/build/src/core/logs.js +29 -0
- package/build/src/core/manifest.js +13 -0
- package/build/src/core/project.js +154 -7
- package/build/src/core/services.js +105 -16
- package/build/src/core/utils.js +57 -1
- package/build/src/experiments.js +23 -0
- package/build/src/index.js +2 -0
- package/build/src/intl.js +28 -0
- package/build/src/mcp/server.js +82 -6
- package/docs/run.md +10 -4
- 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
|

|
|
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
|

|
|
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
|
|
package/build/src/auth/auth.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
65
|
-
* @
|
|
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
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* @param {
|
|
95
|
-
* @
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
});
|