@mcpher/gas-fakes 2.3.11 → 2.3.14
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 +14 -14
- package/gas-fakes.js +1 -0
- package/gf_agent/scripts/builder.js +97 -41
- package/package.json +1 -1
- package/src/cli/lib-manager.js +14 -4
- package/src/cli/setup.js +97 -6
- package/src/services/chartsapp/fakechartsapp.js +6 -1
- package/src/services/driveapp/driveiterators.js +53 -8
- package/src/services/driveapp/fakedriveapp.js +49 -15
- package/src/services/driveapp/fakedrivefile.js +105 -0
- package/src/services/driveapp/fakedrivefolder.js +8 -0
- package/src/services/driveapp/fakedrivemeta.js +68 -16
- package/src/services/driveapp/fakefolderapp.js +19 -6
- package/src/services/enums/chartsenums.js +53 -20
- package/src/services/enums/driveenums.js +1 -0
- package/src/services/slidesapp/fakeautofit.js +23 -15
- package/src/services/slidesapp/fakecolorscheme.js +160 -0
- package/src/services/slidesapp/fakelayout.js +11 -1
- package/src/services/slidesapp/fakemaster.js +10 -0
- package/src/services/slidesapp/fakepresentation.js +27 -0
- package/src/services/slidesapp/fakeslide.js +9 -0
- package/src/services/slidesapp/faketextrange.js +6 -11
- package/src/services/spreadsheetapp/chartenummapping.js +15 -0
- package/src/services/spreadsheetapp/fakebooleancondition.js +119 -0
- package/src/services/spreadsheetapp/fakecellimage.js +42 -0
- package/src/services/spreadsheetapp/fakecellimagebuilder.js +59 -0
- package/src/services/spreadsheetapp/fakeconditionalformatrule.js +55 -0
- package/src/services/spreadsheetapp/fakeconditionalformatrulebuilder.js +330 -0
- package/src/services/spreadsheetapp/fakedevelopermetadata.js +32 -4
- package/src/services/spreadsheetapp/fakedevelopermetadatalocation.js +27 -5
- package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +155 -21
- package/src/services/spreadsheetapp/fakegradientcondition.js +71 -0
- package/src/services/spreadsheetapp/fakesheet.js +63 -0
- package/src/services/spreadsheetapp/fakesheetrange.js +12 -1
- package/src/services/spreadsheetapp/fakespreadsheet.js +30 -11
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +21 -3
- package/src/services/urlfetchapp/app.js +33 -1
- package/src/support/fileiterators.js +3 -1
- package/src/support/filesharers.js +7 -3
- package/src/support/peeker.js +8 -2
- package/src/support/sheetutils.js +1 -0
- package/src/support/sxdrive.js +26 -15
- package/src/support/syncit.js +4 -6
- package/src/support/workersync/synchronizer.js +24 -4
- package/src/support/workersync/worker.js +13 -2
- package/summarize_advanced.js +0 -69
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Now you can run apps script code directly from your console - for example
|
|
|
35
35
|
gas-fakes -s "const files=DriveApp.getRootFolder().searchFiles('title contains \"Untitled\"');while (files.hasNext()) {console.log(files.next().getName())};"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
For details see [gas fakes cli](gas-fakes-cli.md)
|
|
38
|
+
For details see [gas fakes cli](notes/gas-fakes-cli.md)
|
|
39
39
|
|
|
40
40
|
### Configuration
|
|
41
41
|
|
|
@@ -168,7 +168,7 @@ There are a couple of syntactical differences between Node and Apps Script. Not
|
|
|
168
168
|
// this required on Node but not on Apps Script
|
|
169
169
|
if (ScriptApp.isFake) testFakes()
|
|
170
170
|
````
|
|
171
|
-
For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](pull-test-push.md)
|
|
171
|
+
For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](notes/pull-test-push.md)
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
## Help
|
|
@@ -188,11 +188,11 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
188
188
|
- [readme](README.md)
|
|
189
189
|
- [Natural Language Automation with Gemini Skills & MCP Server](gemini-skills-mcp.md) - new skills-based agent approach.
|
|
190
190
|
- [gf_agent documentation](../gf_agent/README.md) - instructions for the Gemini CLI automation agent and MCP server.
|
|
191
|
-
- [gas fakes cli](gas-fakes-cli.md)
|
|
191
|
+
- [gas fakes cli](notes/gas-fakes-cli.md)
|
|
192
192
|
- [github actions using adc](https://github.com/brucemcpherson/gas-fakes-actions-adc)
|
|
193
193
|
- [github actions using dwd and wif](https://github.com/brucemcpherson/gas-fakes-actions-dwd)
|
|
194
|
-
- [ksuite as a back end](ksuite_poc.md)
|
|
195
|
-
- [msgraph as a back end](msgraph.md)
|
|
194
|
+
- [ksuite as a back end](notes/ksuite_poc.md)
|
|
195
|
+
- [msgraph as a back end](notes/msgraph.md)
|
|
196
196
|
- [resurrecting scriptDb repo](https://github.com/brucemcpherson/scriptdb-redux)
|
|
197
197
|
- [Resurrecting ScriptDb – nosql database for Apps Script](https://ramblings.mcpher.com/resurrecting-scriptdb-nosql-database-for-apps-script/)
|
|
198
198
|
- [gas-fakes in serverless containers](https://docs.google.com/presentation/d/1JlXF9T--DD4ERHopyP3WyAMhjRCxxHblgCP5ynxaJ3k/edit?usp=sharing)
|
|
@@ -203,7 +203,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
203
203
|
- [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
204
204
|
- [running gas-fakes on Azure ACA](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
205
205
|
- [running gas-fakes on Github actions](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
206
|
-
- [jdbc notes](jdbc-notes.md)
|
|
206
|
+
- [jdbc notes](notes/jdbc-notes.md)
|
|
207
207
|
- [Yes – you can run native apps script code on Azure ACA as well!](https://ramblings.mcpher.com/yes-you-can-run-native-apps-script-code-on-azure-aca-as-well/)
|
|
208
208
|
- [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
|
|
209
209
|
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
@@ -213,16 +213,16 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
213
213
|
- [Turning async into synch on Node using workers](https://ramblings.mcpher.com/turning-async-into-synch-on-node-using-workers/)
|
|
214
214
|
- [All about Apps Script Enums and how to fake them](https://ramblings.mcpher.com/all-about-apps-script-enums-and-how-to-fake-them/)
|
|
215
215
|
- [colaborators](collaborators.md) - additional information for collaborators
|
|
216
|
-
- [oddities](oddities.md) - a collection of oddities uncovered during this project
|
|
217
|
-
- [named colors](named-colors.md)
|
|
218
|
-
- [sandbox](sandbox.md)
|
|
219
|
-
- [senstive scopes](workspace_scopes.md)
|
|
220
|
-
- [using apps script libraries with gas-fakes](libraries.md)
|
|
216
|
+
- [oddities](notes/oddities.md) - a collection of oddities uncovered during this project
|
|
217
|
+
- [named colors](notes/named-colors.md)
|
|
218
|
+
- [sandbox](notes/sandbox.md)
|
|
219
|
+
- [senstive scopes](notes/workspace_scopes.md)
|
|
220
|
+
- [using apps script libraries with gas-fakes](notes/libraries.md)
|
|
221
221
|
- [how libhandler works](libhandler.md)
|
|
222
222
|
- [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
|
|
223
|
-
- [named range identity](named-range-identity.md)
|
|
224
|
-
- [Workspace scopes with local authentication](workspace_scopes.md)
|
|
225
|
-
- [push test pull](pull-test-push.md)
|
|
223
|
+
- [named range identity](notes/named-range-identity.md)
|
|
224
|
+
- [Workspace scopes with local authentication](notes/workspace_scopes.md)
|
|
225
|
+
- [push test pull](notes/pull-test-push.md)
|
|
226
226
|
- [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
|
|
227
227
|
- [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
|
|
228
228
|
- [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
|
package/gas-fakes.js
CHANGED
|
@@ -1,62 +1,118 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// The script is at gf_agent/scripts/builder.js, so the root is one level up
|
|
9
|
+
const GF_AGENT_DIR = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
// Standardize paths relative to the gf_agent directory
|
|
12
|
+
const SKILLS_DIR = path.join(GF_AGENT_DIR, 'skills');
|
|
13
|
+
const INDEX_FILE = path.join(GF_AGENT_DIR, 'index.md');
|
|
14
|
+
const TEMPLATE_FILE = path.join(__dirname, 'SKILL.template.md');
|
|
15
|
+
const KNOWLEDGE_DIR = path.join(GF_AGENT_DIR, 'knowledge');
|
|
16
|
+
const SKILL_OUTPUT = path.join(GF_AGENT_DIR, 'SKILL.md');
|
|
17
|
+
|
|
18
|
+
// Use CWD for progress dir to allow user to provide it in a sparse clone/standalone env
|
|
19
|
+
const PROGRESS_DIR = path.resolve(process.cwd(), 'progress');
|
|
7
20
|
|
|
8
21
|
async function build() {
|
|
9
|
-
|
|
22
|
+
// Cleanup potential junk from previous runs where paths were relative to CWD
|
|
23
|
+
// (e.g. if run from within gf_agent/scripts, it might have created gf_agent/scripts/gf_agent)
|
|
24
|
+
try {
|
|
25
|
+
const junkDir = path.join(__dirname, 'gf_agent');
|
|
26
|
+
await fs.rm(junkDir, { recursive: true, force: true });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Ignore
|
|
29
|
+
}
|
|
10
30
|
|
|
11
|
-
|
|
12
|
-
const mdFiles = files.filter(f => f.endsWith('.md') || f.endsWith('.MD'));
|
|
31
|
+
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
|
13
32
|
|
|
14
33
|
let masterIndex = '# gf_agent Skills Index\n\nThis index lists all Google Apps Script services and classes supported by `gf_agent` via `gas-fakes`.\n\n';
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const className = match[1];
|
|
26
|
-
// Find the table for this class
|
|
27
|
-
const classSection = content.slice(match.index);
|
|
28
|
-
const tableEnd = classSection.indexOf('## Class:') > 0 ? classSection.indexOf('## Class:', 10) : classSection.length;
|
|
29
|
-
const tableContent = classSection.slice(0, tableEnd);
|
|
35
|
+
try {
|
|
36
|
+
// Check if progress directory exists before attempting to read
|
|
37
|
+
await fs.access(PROGRESS_DIR);
|
|
38
|
+
const files = await fs.readdir(PROGRESS_DIR);
|
|
39
|
+
const mdFiles = files.filter(f => f.endsWith('.md') || f.endsWith('.MD'));
|
|
40
|
+
|
|
41
|
+
for (const file of mdFiles) {
|
|
42
|
+
const content = await fs.readFile(path.join(PROGRESS_DIR, file), 'utf-8');
|
|
43
|
+
const serviceName = file.replace(/\.md$/i, '');
|
|
30
44
|
|
|
31
|
-
// Extract
|
|
32
|
-
const
|
|
33
|
-
const
|
|
45
|
+
// Extract classes
|
|
46
|
+
const classMatches = content.matchAll(/## Class: \[(.*?)\]/g);
|
|
47
|
+
const classes = [];
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
for (const match of classMatches) {
|
|
50
|
+
const className = match[1];
|
|
51
|
+
// Find the table for this class
|
|
52
|
+
const classSection = content.slice(match.index);
|
|
53
|
+
const tableEnd = classSection.indexOf('## Class:') > 0 ? classSection.indexOf('## Class:', 10) : classSection.length;
|
|
54
|
+
const tableContent = classSection.slice(0, tableEnd);
|
|
55
|
+
|
|
56
|
+
// Extract completed methods
|
|
57
|
+
const methodMatches = tableContent.matchAll(/\| \[(.*?)\]\(.*?\) \| .*? \| .*? \| .*? \| (completed) \|/g);
|
|
58
|
+
const methods = Array.from(methodMatches).map(m => m[1]);
|
|
59
|
+
|
|
60
|
+
if (methods.length > 0) {
|
|
61
|
+
classes.push({ name: className, methods });
|
|
62
|
+
}
|
|
37
63
|
}
|
|
38
|
-
}
|
|
39
64
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
if (classes.length > 0) {
|
|
66
|
+
const skillFile = `${serviceName.toLowerCase()}.md`;
|
|
67
|
+
let skillContent = `# Service: ${serviceName}\n\n`;
|
|
68
|
+
|
|
69
|
+
classes.forEach(c => {
|
|
70
|
+
skillContent += `## Class: ${c.name}\n\n`;
|
|
71
|
+
skillContent += `Supported Methods:\n`;
|
|
72
|
+
c.methods.forEach(m => {
|
|
73
|
+
skillContent += `- \`${m}\`\n`;
|
|
74
|
+
});
|
|
75
|
+
skillContent += '\n';
|
|
49
76
|
});
|
|
50
|
-
skillContent += '\n';
|
|
51
|
-
});
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
await fs.writeFile(path.join(SKILLS_DIR, skillFile), skillContent);
|
|
79
|
+
masterIndex += `- [${serviceName}](skills/${skillFile})\n`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await fs.writeFile(INDEX_FILE, masterIndex);
|
|
84
|
+
console.log(`Skills and Index generated from ${PROGRESS_DIR}`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.code === 'ENOENT') {
|
|
87
|
+
console.log(`Skipping skills index generation: ${PROGRESS_DIR} not found.`);
|
|
88
|
+
} else {
|
|
89
|
+
console.log(`Skipping skills index generation: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
console.log(`(This is expected in a sparse clone environment).`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Aggregate knowledge files into SKILL.md
|
|
95
|
+
let skillMarkdown = await fs.readFile(TEMPLATE_FILE, 'utf-8');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const knowledgeFiles = await fs.readdir(KNOWLEDGE_DIR);
|
|
99
|
+
// Sort files to ensure deterministic aggregation (e.g., 01-drive.md, 02-syntax.md)
|
|
100
|
+
knowledgeFiles.sort();
|
|
101
|
+
|
|
102
|
+
for (const kFile of knowledgeFiles) {
|
|
103
|
+
if (kFile.endsWith('.md')) {
|
|
104
|
+
const kContent = await fs.readFile(path.join(KNOWLEDGE_DIR, kFile), 'utf-8');
|
|
105
|
+
skillMarkdown += `\n${kContent}\n`;
|
|
106
|
+
}
|
|
55
107
|
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.log(`No knowledge directory found at ${KNOWLEDGE_DIR} or error reading it:`, err.message);
|
|
56
110
|
}
|
|
57
111
|
|
|
58
|
-
await fs.writeFile(
|
|
59
|
-
|
|
112
|
+
await fs.writeFile(SKILL_OUTPUT, skillMarkdown);
|
|
113
|
+
|
|
114
|
+
console.log(`Build complete! Skills, Index, and monolithic SKILL.md generated at ${SKILL_OUTPUT}`);
|
|
60
115
|
}
|
|
61
116
|
|
|
62
117
|
build().catch(console.error);
|
|
118
|
+
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"name": "@mcpher/gas-fakes",
|
|
42
42
|
"author": "bruce mcpherson",
|
|
43
|
-
"version": "2.3.
|
|
43
|
+
"version": "2.3.14",
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"main": "main.js",
|
|
46
46
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
package/src/cli/lib-manager.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { parse } from "acorn";
|
|
3
|
-
import { Auth } from "../support/auth.js"; // Relative to gas-fakes-cli directory
|
|
3
|
+
import { Auth, _identities } from "../support/auth.js"; // Relative to gas-fakes-cli directory
|
|
4
4
|
import { checkForGcloudCli, spawnCommand } from "./utils.js";
|
|
5
5
|
|
|
6
6
|
async function getAccessToken(pattern) {
|
|
@@ -8,12 +8,22 @@ async function getAccessToken(pattern) {
|
|
|
8
8
|
// Authorization pattern 1
|
|
9
9
|
// We use cloud-platform as it provides sufficient access for Drive API fetching
|
|
10
10
|
// while avoiding the 'well-known client ID' block that targets Workspace scopes like drive.readonly.
|
|
11
|
-
const
|
|
11
|
+
const client = await Auth.setAuth(
|
|
12
12
|
["https://www.googleapis.com/auth/cloud-platform"],
|
|
13
13
|
true
|
|
14
14
|
);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (client.cachedCredential !== undefined) {
|
|
16
|
+
client.cachedCredential = null;
|
|
17
|
+
}
|
|
18
|
+
const tokenResponse = await client.getAccessToken();
|
|
19
|
+
const token = tokenResponse.token || tokenResponse;
|
|
20
|
+
|
|
21
|
+
// CRITICAL: We MUST clear the global identities map so that this temporary
|
|
22
|
+
// library-fetching auth does not trick the downstream executor.js and syncit.js
|
|
23
|
+
// into thinking the full environment (and the background worker) has been initialized.
|
|
24
|
+
_identities.clear();
|
|
25
|
+
|
|
26
|
+
return token;
|
|
17
27
|
} else {
|
|
18
28
|
// Authorization pattern 2
|
|
19
29
|
await checkForGcloudCli();
|
package/src/cli/setup.js
CHANGED
|
@@ -622,11 +622,102 @@ export async function initializeConfiguration(options = {}) {
|
|
|
622
622
|
|
|
623
623
|
if (skillResponse.installSkills) {
|
|
624
624
|
console.log("Installing Gemini CLI skills and MCP server...");
|
|
625
|
+
let manualSkillCmd;
|
|
625
626
|
try {
|
|
626
|
-
// 1. Install the agent skill
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
627
|
+
// 1. Install or link the agent skill
|
|
628
|
+
let skillCmd;
|
|
629
|
+
const gfAgentSubdir = path.resolve(process.cwd(), "gf_agent", "SKILL.md");
|
|
630
|
+
const gfAgentCurrent = path.resolve(process.cwd(), "SKILL.md");
|
|
631
|
+
|
|
632
|
+
if (fs.existsSync(gfAgentSubdir)) {
|
|
633
|
+
console.log("Detected local gas-fakes repository. Linking local skill for development...");
|
|
634
|
+
skillCmd = "gemini skills link ./gf_agent --consent";
|
|
635
|
+
manualSkillCmd = "1. gemini skills link ./gf_agent";
|
|
636
|
+
execSync(skillCmd, { stdio: ["ignore", "pipe", "ignore"] });
|
|
637
|
+
console.log("Skill linked successfully.");
|
|
638
|
+
} else if (fs.existsSync(gfAgentCurrent) && fs.existsSync(path.resolve(process.cwd(), "index.md"))) {
|
|
639
|
+
console.log("Detected local gf_agent directory. Linking local skill for development...");
|
|
640
|
+
skillCmd = "gemini skills link . --consent";
|
|
641
|
+
manualSkillCmd = "1. gemini skills link .";
|
|
642
|
+
execSync(skillCmd, { stdio: ["ignore", "pipe", "ignore"] });
|
|
643
|
+
console.log("Skill linked successfully.");
|
|
644
|
+
} else {
|
|
645
|
+
// Not a local clone, check if already installed to avoid overwriting
|
|
646
|
+
let isAlreadyInstalled = false;
|
|
647
|
+
try {
|
|
648
|
+
const existingSkills = execSync("gemini skills list", {
|
|
649
|
+
encoding: "utf8",
|
|
650
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
651
|
+
});
|
|
652
|
+
if (existingSkills.includes("gf_agent")) {
|
|
653
|
+
isAlreadyInstalled = true;
|
|
654
|
+
}
|
|
655
|
+
} catch (err) {
|
|
656
|
+
// Ignore errors checking skills list
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (isAlreadyInstalled) {
|
|
660
|
+
console.log("gf_agent skill is already installed. Skipping remote installation.");
|
|
661
|
+
manualSkillCmd = "1. gemini skills update gf_agent (if needed)";
|
|
662
|
+
} else {
|
|
663
|
+
const installChoice = await prompts({
|
|
664
|
+
type: "select",
|
|
665
|
+
name: "method",
|
|
666
|
+
message: "How would you like to install the gf_agent skill?",
|
|
667
|
+
choices: [
|
|
668
|
+
{
|
|
669
|
+
title: "Global (Standard)",
|
|
670
|
+
value: "global",
|
|
671
|
+
description: "Recommended for most users. Installs a read-only copy globally.",
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
title: "Local Standalone (Contributor)",
|
|
675
|
+
value: "local",
|
|
676
|
+
description: "Installs a local sparse-clone for skill development and linking.",
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
initial: 0,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
if (installChoice.method === "local") {
|
|
683
|
+
const standaloneDir = "gf_agent_standalone";
|
|
684
|
+
const fullStandalonePath = path.resolve(process.cwd(), standaloneDir);
|
|
685
|
+
console.log(`Setting up local standalone skill environment in "./${standaloneDir}"...`);
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
if (!fs.existsSync(fullStandalonePath)) {
|
|
689
|
+
fs.mkdirSync(fullStandalonePath, { recursive: true });
|
|
690
|
+
execSync("git init", { cwd: fullStandalonePath, stdio: "ignore" });
|
|
691
|
+
execSync("git remote add origin https://github.com/brucemcpherson/gas-fakes.git", {
|
|
692
|
+
cwd: fullStandalonePath,
|
|
693
|
+
stdio: "ignore",
|
|
694
|
+
});
|
|
695
|
+
execSync("git config core.sparseCheckout true", { cwd: fullStandalonePath, stdio: "ignore" });
|
|
696
|
+
|
|
697
|
+
const sparsePath = path.join(fullStandalonePath, ".git", "info", "sparse-checkout");
|
|
698
|
+
fs.writeFileSync(sparsePath, "gf_agent/*\n");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
execSync("git pull origin main", { cwd: fullStandalonePath, stdio: "ignore" });
|
|
702
|
+
|
|
703
|
+
skillCmd = "gemini skills link ./gf_agent --consent";
|
|
704
|
+
execSync(skillCmd, { cwd: fullStandalonePath, stdio: ["ignore", "pipe", "ignore"] });
|
|
705
|
+
console.log("Skill linked successfully.");
|
|
706
|
+
|
|
707
|
+
manualSkillCmd = `1. cd ${standaloneDir} && gemini skills link ./gf_agent`;
|
|
708
|
+
} catch (gitErr) {
|
|
709
|
+
console.error(`Error during local setup: ${gitErr.message}`);
|
|
710
|
+
throw gitErr;
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent --consent";
|
|
714
|
+
manualSkillCmd = "1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
|
|
715
|
+
console.log(`Installing global skill from remote...`);
|
|
716
|
+
execSync(skillCmd, { stdio: ["ignore", "pipe", "ignore"] });
|
|
717
|
+
console.log("Skill installed successfully.");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
630
721
|
|
|
631
722
|
// 2. Add the MCP server
|
|
632
723
|
const mcpCmd = "gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp";
|
|
@@ -635,11 +726,11 @@ export async function initializeConfiguration(options = {}) {
|
|
|
635
726
|
|
|
636
727
|
console.log("\x1b[1;32mInstallation complete!\x1b[0m");
|
|
637
728
|
console.log("\nYou can now use natural language to automate tasks:");
|
|
638
|
-
console.log(" \x1b[1;33m
|
|
729
|
+
console.log(" \x1b[1;33m\“Create a sheet called ‘Todays drive files' and add any files on Drive modified today to it\"\x1b[0m");
|
|
639
730
|
} catch (err) {
|
|
640
731
|
console.error(`\x1b[1;31mError during Gemini installation: ${err.message}\x1b[0m`);
|
|
641
732
|
console.log("You may need to install them manually:");
|
|
642
|
-
console.log(
|
|
733
|
+
console.log(manualSkillCmd);
|
|
643
734
|
console.log("2. gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp");
|
|
644
735
|
}
|
|
645
736
|
} else {
|
|
@@ -19,7 +19,12 @@ export const newFakeChartsApp = (...args) => {
|
|
|
19
19
|
export class FakeChartsApp {
|
|
20
20
|
constructor() {
|
|
21
21
|
const enumProps = [
|
|
22
|
-
"ChartType", //
|
|
22
|
+
"ChartType", // ChartType An enumeration of the possible chart types.
|
|
23
|
+
"ChartHiddenDimensionStrategy",
|
|
24
|
+
"ChartMergeStrategy",
|
|
25
|
+
"CurveStyle",
|
|
26
|
+
"PointStyle",
|
|
27
|
+
"Position"
|
|
23
28
|
];
|
|
24
29
|
|
|
25
30
|
// import all known enums as props of chartsapp
|
|
@@ -14,14 +14,33 @@ export const getFilesIterator = ({
|
|
|
14
14
|
qob,
|
|
15
15
|
parentId = null,
|
|
16
16
|
folderTypes,
|
|
17
|
-
fileTypes
|
|
17
|
+
fileTypes,
|
|
18
|
+
token
|
|
18
19
|
}) => {
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
// parentId can be null to search everywhere
|
|
22
23
|
if (!is.null(parentId)) assert.nonEmptyString(parentId)
|
|
23
|
-
assert.boolean(folderTypes)
|
|
24
|
-
assert.boolean(fileTypes)
|
|
24
|
+
assert.boolean(folderTypes || !!token)
|
|
25
|
+
assert.boolean(fileTypes || !!token)
|
|
26
|
+
|
|
27
|
+
let state = {
|
|
28
|
+
qob,
|
|
29
|
+
parentId,
|
|
30
|
+
folderTypes,
|
|
31
|
+
fileTypes,
|
|
32
|
+
pageToken: null,
|
|
33
|
+
tank: []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (token) {
|
|
37
|
+
const saved = JSON.parse(Buffer.from(token, 'base64').toString())
|
|
38
|
+
state = { ...state, ...saved }
|
|
39
|
+
qob = state.qob
|
|
40
|
+
parentId = state.parentId
|
|
41
|
+
folderTypes = state.folderTypes
|
|
42
|
+
fileTypes = state.fileTypes
|
|
43
|
+
}
|
|
25
44
|
|
|
26
45
|
// DriveApp doesnt give option to specify these so this will be fixed
|
|
27
46
|
const fields = `files(${minFields}),nextPageToken`
|
|
@@ -49,9 +68,9 @@ export const getFilesIterator = ({
|
|
|
49
68
|
*/
|
|
50
69
|
function* filesink() {
|
|
51
70
|
// the result tank
|
|
52
|
-
let tank = []
|
|
71
|
+
let tank = [...state.tank]
|
|
53
72
|
// the next page token
|
|
54
|
-
let pageToken =
|
|
73
|
+
let pageToken = state.pageToken
|
|
55
74
|
|
|
56
75
|
do {
|
|
57
76
|
// if nothing in the tank, fill it up
|
|
@@ -63,10 +82,14 @@ export const getFilesIterator = ({
|
|
|
63
82
|
// format the results into the folder or file object
|
|
64
83
|
assert.array(data.files)
|
|
65
84
|
assert.function(DriveApp.__settleClass)
|
|
66
|
-
|
|
85
|
+
|
|
86
|
+
// we store raw data in state for continuation
|
|
87
|
+
state.tank = data.files
|
|
88
|
+
tank = [...state.tank]
|
|
67
89
|
|
|
68
90
|
// the presence of a nextPageToken is the signal that there's more to come
|
|
69
91
|
pageToken = data.nextPageToken
|
|
92
|
+
state.pageToken = pageToken
|
|
70
93
|
|
|
71
94
|
// if we still have nothing in the tank but there's a page token, keep going
|
|
72
95
|
if (!tank.length && !pageToken) break;
|
|
@@ -74,7 +97,12 @@ export const getFilesIterator = ({
|
|
|
74
97
|
|
|
75
98
|
// if we've got anything in the tank send back the oldest one
|
|
76
99
|
if (tank.length) {
|
|
77
|
-
|
|
100
|
+
const raw = tank.splice(0, 1)[0]
|
|
101
|
+
state.tank.splice(0, 1)
|
|
102
|
+
yield {
|
|
103
|
+
__fakeResolved: DriveApp.__settleClass(raw),
|
|
104
|
+
__fakeRaw: raw
|
|
105
|
+
}
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
// if there's still anything left in the tank,
|
|
@@ -85,9 +113,26 @@ export const getFilesIterator = ({
|
|
|
85
113
|
// create the iterator
|
|
86
114
|
const fileit = filesink()
|
|
87
115
|
|
|
116
|
+
const continuationHandler = (peeked) => {
|
|
117
|
+
// if we've peeked, we need to make sure the peeked item is included in the tank
|
|
118
|
+
// because GAS continuation tokens should represent the state AFTER the last next() call.
|
|
119
|
+
// Peeker already called generator.next(), so the item to be returned by the NEXT next() call
|
|
120
|
+
// is in peeked.value.
|
|
121
|
+
const tank = [...state.tank]
|
|
122
|
+
if (peeked && !peeked.done) {
|
|
123
|
+
// Put the pre-fetched item back at the start of the tank for the token
|
|
124
|
+
tank.unshift(peeked.value.__fakeRaw || peeked.value)
|
|
125
|
+
}
|
|
126
|
+
const tokenState = {
|
|
127
|
+
...state,
|
|
128
|
+
tank
|
|
129
|
+
}
|
|
130
|
+
return Buffer.from(JSON.stringify(tokenState)).toString('base64')
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
// a regular iterator doesnt support the same methods
|
|
89
134
|
// as Apps Script so we'll fake that too
|
|
90
|
-
return newPeeker(fileit)
|
|
135
|
+
return newPeeker(fileit, continuationHandler)
|
|
91
136
|
|
|
92
137
|
}
|
|
93
138
|
|
|
@@ -5,6 +5,8 @@ import { notYetImplemented, isFolder } from '../../support/helpers.js'
|
|
|
5
5
|
import { Proxies } from '../../support/proxies.js'
|
|
6
6
|
import { Utils } from '../../support/utils.js'
|
|
7
7
|
import { Access, Permission } from '../enums/driveenums.js'
|
|
8
|
+
import { getFilesIterator } from './driveiterators.js'
|
|
9
|
+
import { Syncit } from '../../support/syncit.js'
|
|
8
10
|
const { is } = Utils
|
|
9
11
|
|
|
10
12
|
|
|
@@ -19,6 +21,7 @@ export class FakeDriveApp {
|
|
|
19
21
|
this.__rootFolder = null
|
|
20
22
|
this.folderApp = newFakeFolderApp()
|
|
21
23
|
this.__settleClass = (file) => isFolder(file) ? newFakeDriveFolder(file) : newFakeDriveFile(file)
|
|
24
|
+
this.__enforceSingleParent = true
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
|
|
@@ -133,37 +136,68 @@ export class FakeDriveApp {
|
|
|
133
136
|
return this.getRootFolder().createFolder(name)
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
createShortcut(targetId, resourceKey) {
|
|
140
|
+
return this.getRootFolder().createShortcut(targetId, resourceKey)
|
|
141
|
+
}
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
return
|
|
143
|
+
createShortcutForTargetIdAndResourceKey(targetId, resourceKey) {
|
|
144
|
+
return this.getRootFolder().createShortcutForTargetIdAndResourceKey(targetId, resourceKey)
|
|
140
145
|
}
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
|
|
147
|
+
getFolderByIdAndResourceKey(id, resourceKey) {
|
|
148
|
+
return this.getFolderById(id)
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
return
|
|
151
|
+
getFileByIdAndResourceKey(id, resourceKey) {
|
|
152
|
+
return this.getFileById(id)
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
|
|
155
|
+
continueFileIterator(token) {
|
|
156
|
+
return getFilesIterator({ token })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
continueFolderIterator(token) {
|
|
160
|
+
return getFilesIterator({ token })
|
|
150
161
|
}
|
|
162
|
+
|
|
151
163
|
getTrashedFiles() {
|
|
152
|
-
return
|
|
164
|
+
return getFilesIterator({
|
|
165
|
+
folderTypes: false,
|
|
166
|
+
fileTypes: true,
|
|
167
|
+
qob: ['trashed = true']
|
|
168
|
+
})
|
|
153
169
|
}
|
|
170
|
+
|
|
154
171
|
getTrashedFolders() {
|
|
155
|
-
return
|
|
172
|
+
return getFilesIterator({
|
|
173
|
+
folderTypes: true,
|
|
174
|
+
fileTypes: false,
|
|
175
|
+
qob: ['trashed = true']
|
|
176
|
+
})
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
getStorageLimit() {
|
|
159
|
-
|
|
180
|
+
const { data } = Syncit.fxDrive({
|
|
181
|
+
prop: 'about',
|
|
182
|
+
method: 'get',
|
|
183
|
+
params: { fields: 'storageQuota' }
|
|
184
|
+
})
|
|
185
|
+
return parseInt(data.storageQuota.limit, 10)
|
|
160
186
|
}
|
|
187
|
+
|
|
161
188
|
getStorageUsed() {
|
|
162
|
-
|
|
189
|
+
const { data } = Syncit.fxDrive({
|
|
190
|
+
prop: 'about',
|
|
191
|
+
method: 'get',
|
|
192
|
+
params: { fields: 'storageQuota' }
|
|
193
|
+
})
|
|
194
|
+
return parseInt(data.storageQuota.usage, 10)
|
|
163
195
|
}
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
|
|
197
|
+
enforceSingleParent(enabled) {
|
|
198
|
+
this.__enforceSingleParent = enabled
|
|
166
199
|
}
|
|
200
|
+
|
|
167
201
|
get Access() {
|
|
168
202
|
return Access
|
|
169
203
|
}
|