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