@ciderjs/gasbombe 0.1.0 → 0.2.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/dist/index.d.cts CHANGED
@@ -1,10 +1,16 @@
1
+ type PackageManager = 'npm' | 'pnpm' | 'yarn';
2
+ type TemplateType = 'vanilla-ts' | 'react-tsx';
3
+ type ClaspOption = 'create' | 'list' | 'input' | 'skip';
1
4
  interface ProjectOptions {
2
5
  projectName: string;
3
- packageManager: "npm" | "pnpm" | "yarn";
4
- templateType: "vanilla-ts" | "react-tsx";
6
+ packageManager: PackageManager;
7
+ templateType: TemplateType;
8
+ clasp: ClaspOption;
9
+ claspProjectId?: string | undefined;
10
+ install: boolean;
5
11
  }
6
12
 
7
- declare function runCommand(command: string, args: string[], cwd: string): Promise<void>;
8
- declare function generateProject({ projectName, packageManager, templateType, }: ProjectOptions): Promise<void>;
13
+ declare function runCommand(command: string, args: string[], cwd: string, capture?: boolean): Promise<string>;
14
+ declare function generateProject({ projectName, packageManager, templateType, clasp, claspProjectId, install, }: ProjectOptions): Promise<void>;
9
15
 
10
16
  export { generateProject, runCommand };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,16 @@
1
+ type PackageManager = 'npm' | 'pnpm' | 'yarn';
2
+ type TemplateType = 'vanilla-ts' | 'react-tsx';
3
+ type ClaspOption = 'create' | 'list' | 'input' | 'skip';
1
4
  interface ProjectOptions {
2
5
  projectName: string;
3
- packageManager: "npm" | "pnpm" | "yarn";
4
- templateType: "vanilla-ts" | "react-tsx";
6
+ packageManager: PackageManager;
7
+ templateType: TemplateType;
8
+ clasp: ClaspOption;
9
+ claspProjectId?: string | undefined;
10
+ install: boolean;
5
11
  }
6
12
 
7
- declare function runCommand(command: string, args: string[], cwd: string): Promise<void>;
8
- declare function generateProject({ projectName, packageManager, templateType, }: ProjectOptions): Promise<void>;
13
+ declare function runCommand(command: string, args: string[], cwd: string, capture?: boolean): Promise<string>;
14
+ declare function generateProject({ projectName, packageManager, templateType, clasp, claspProjectId, install, }: ProjectOptions): Promise<void>;
9
15
 
10
16
  export { generateProject, runCommand };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,16 @@
1
+ type PackageManager = 'npm' | 'pnpm' | 'yarn';
2
+ type TemplateType = 'vanilla-ts' | 'react-tsx';
3
+ type ClaspOption = 'create' | 'list' | 'input' | 'skip';
1
4
  interface ProjectOptions {
2
5
  projectName: string;
3
- packageManager: "npm" | "pnpm" | "yarn";
4
- templateType: "vanilla-ts" | "react-tsx";
6
+ packageManager: PackageManager;
7
+ templateType: TemplateType;
8
+ clasp: ClaspOption;
9
+ claspProjectId?: string | undefined;
10
+ install: boolean;
5
11
  }
6
12
 
7
- declare function runCommand(command: string, args: string[], cwd: string): Promise<void>;
8
- declare function generateProject({ projectName, packageManager, templateType, }: ProjectOptions): Promise<void>;
13
+ declare function runCommand(command: string, args: string[], cwd: string, capture?: boolean): Promise<string>;
14
+ declare function generateProject({ projectName, packageManager, templateType, clasp, claspProjectId, install, }: ProjectOptions): Promise<void>;
9
15
 
10
16
  export { generateProject, runCommand };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { select } from '@inquirer/prompts';
4
5
  import { consola } from 'consola';
5
6
  import ejs from 'ejs';
6
7
  import { glob } from 'glob';
@@ -14,18 +15,30 @@ import __cjs_mod__ from 'module';
14
15
  const __filename = __cjs_url__.fileURLToPath(import.meta.url);
15
16
  const __dirname = __cjs_path__.dirname(__filename);
16
17
  const require = __cjs_mod__.createRequire(import.meta.url);
17
- async function runCommand(command, args, cwd) {
18
+ async function runCommand(command, args, cwd, capture = false) {
18
19
  return new Promise((resolve, reject) => {
19
20
  const child = spawn(command, args, {
20
21
  cwd,
21
- stdio: "inherit",
22
+ stdio: capture ? "pipe" : "inherit",
22
23
  shell: true
23
24
  });
25
+ let stdout = "";
26
+ let stderr = "";
27
+ if (capture) {
28
+ child.stdout?.on("data", (data) => {
29
+ stdout += data.toString();
30
+ });
31
+ child.stderr?.on("data", (data) => {
32
+ stderr += data.toString();
33
+ });
34
+ }
24
35
  child.on("close", (code) => {
25
36
  if (code === 0) {
26
- resolve();
37
+ resolve(stdout.trim());
27
38
  } else {
28
- reject(new Error(`Command failed with exit code ${code}`));
39
+ const errorMsg = `Command failed with exit code ${code}${stderr ? `:
40
+ ${stderr}` : ""}`;
41
+ reject(new Error(errorMsg));
29
42
  }
30
43
  });
31
44
  child.on("error", (err) => {
@@ -33,10 +46,132 @@ async function runCommand(command, args, cwd) {
33
46
  });
34
47
  });
35
48
  }
49
+ async function handleClaspSetup(claspOption, projectName, outputDir, claspProjectId, packageManager = "npm") {
50
+ if (claspOption === "skip") {
51
+ return;
52
+ }
53
+ const npxLikeCommand = packageManager === "npm" ? "npx" : packageManager === "pnpm" ? "pnpx" : "yarn";
54
+ consola.start(
55
+ `Setting up .clasp.json with \`${npxLikeCommand} @google/clasp\`...`
56
+ );
57
+ if (claspOption === "create" || claspOption === "list") {
58
+ try {
59
+ await runCommand(npxLikeCommand, ["@google/clasp", "status"], outputDir);
60
+ } catch {
61
+ consola.error(
62
+ `It seems you are not logged in to clasp. Please run \`${npxLikeCommand} @google/clasp login\` and try again.`
63
+ );
64
+ return;
65
+ }
66
+ }
67
+ let scriptId;
68
+ let existingClaspJson = null;
69
+ switch (claspOption) {
70
+ case "create":
71
+ try {
72
+ const claspJsonPath = path.join(outputDir, ".clasp.json");
73
+ try {
74
+ const existingContent = await fs.readFile(claspJsonPath, "utf-8");
75
+ existingClaspJson = JSON.parse(existingContent);
76
+ await fs.unlink(claspJsonPath);
77
+ } catch {
78
+ }
79
+ const result = await runCommand(
80
+ npxLikeCommand,
81
+ [
82
+ "@google/clasp",
83
+ "create",
84
+ "--title",
85
+ `"${projectName}"`,
86
+ "--type",
87
+ "standalone"
88
+ ],
89
+ outputDir,
90
+ true
91
+ );
92
+ const match = result.match(/Created new script: .+\/d\/(.+)\/edit/);
93
+ if (match?.[1]) {
94
+ scriptId = match[1];
95
+ consola.info(`Created new Apps Script project with ID: ${scriptId}`);
96
+ } else {
97
+ throw new Error("Could not parse scriptId from clasp output.");
98
+ }
99
+ } catch (e) {
100
+ consola.error("Failed to create new Apps Script project.", e);
101
+ return;
102
+ }
103
+ break;
104
+ case "list":
105
+ try {
106
+ const listOutput = await runCommand(
107
+ npxLikeCommand,
108
+ ["@google/clasp", "list"],
109
+ outputDir,
110
+ true
111
+ );
112
+ const projects = listOutput.split("\n").slice(1).map((line) => {
113
+ const parts = line.split(" - ");
114
+ if (parts.length >= 2) {
115
+ const name = parts[0]?.trim();
116
+ const url = parts[1]?.trim();
117
+ if (!name || !url) return null;
118
+ const match = url.match(/\/d\/(.+)\/edit/);
119
+ const scriptId2 = match?.[1];
120
+ if (scriptId2) {
121
+ return { name, value: scriptId2 };
122
+ }
123
+ }
124
+ return null;
125
+ }).filter((p) => p !== null);
126
+ if (projects.length === 0) {
127
+ consola.warn(
128
+ "No existing Apps Script projects found. Please create one first."
129
+ );
130
+ return;
131
+ }
132
+ scriptId = await select({
133
+ message: "Choose an existing Apps Script project:",
134
+ choices: projects
135
+ });
136
+ } catch (e) {
137
+ consola.error("Failed to list Apps Script projects.", e);
138
+ return;
139
+ }
140
+ break;
141
+ case "input":
142
+ scriptId = claspProjectId;
143
+ break;
144
+ }
145
+ if (scriptId) {
146
+ const claspJsonPath = path.join(outputDir, ".clasp.json");
147
+ let claspJson = {
148
+ scriptId
149
+ };
150
+ let successMessage = `.clasp.json created successfully with scriptId: ${scriptId}`;
151
+ if (existingClaspJson) {
152
+ claspJson = { ...existingClaspJson, scriptId };
153
+ successMessage = `.clasp.json updated successfully with scriptId: ${scriptId}`;
154
+ } else {
155
+ try {
156
+ const existingContent = await fs.readFile(claspJsonPath, "utf-8");
157
+ const currentClaspJson = JSON.parse(existingContent);
158
+ claspJson = { ...currentClaspJson, scriptId };
159
+ successMessage = `.clasp.json updated successfully with scriptId: ${scriptId}`;
160
+ } catch {
161
+ }
162
+ }
163
+ const claspJsonContent = JSON.stringify(claspJson, null, 2);
164
+ await fs.writeFile(claspJsonPath, claspJsonContent, { encoding: "utf-8" });
165
+ consola.success(successMessage);
166
+ }
167
+ }
36
168
  async function generateProject({
37
169
  projectName,
38
170
  packageManager,
39
- templateType
171
+ templateType,
172
+ clasp,
173
+ claspProjectId,
174
+ install
40
175
  }) {
41
176
  const outputDir = path.resolve(process.cwd(), projectName);
42
177
  const templateBaseDir = path.resolve(__dirname, "..", "dist", "templates");
@@ -45,11 +180,28 @@ async function generateProject({
45
180
  consola.start(
46
181
  `Creating a new Project for GoogleAppsScript in ${outputDir}...`
47
182
  );
48
- try {
49
- await fs.access(outputDir);
50
- consola.error(`Directory ${projectName} already exists.`);
51
- process.exit(1);
52
- } catch {
183
+ if (projectName === ".") {
184
+ try {
185
+ const files = await fs.readdir(outputDir);
186
+ const relevantFiles = files.filter((file) => !file.startsWith("."));
187
+ if (relevantFiles.length > 0) {
188
+ const proceed = await confirm(
189
+ "Current directory is not empty. Proceed anyway?"
190
+ );
191
+ if (!proceed) {
192
+ consola.warn("Operation cancelled.");
193
+ process.exit(0);
194
+ }
195
+ }
196
+ } catch {
197
+ }
198
+ } else {
199
+ try {
200
+ await fs.access(outputDir);
201
+ consola.error(`Directory ${projectName} already exists.`);
202
+ process.exit(1);
203
+ } catch {
204
+ }
53
205
  }
54
206
  await fs.mkdir(outputDir, { recursive: true });
55
207
  consola.info(`Generating project files from template '${templateType}'...`);
@@ -59,30 +211,36 @@ async function generateProject({
59
211
  const files = await glob("./**/*", {
60
212
  cwd: dir,
61
213
  nodir: true,
62
- dot: true,
63
- dotRelative: true,
64
- follow: true,
65
- windowsPathsNoEscape: true
214
+ dot: true
66
215
  });
67
216
  for (const file of files) {
68
- const templatePath = path.join(dir, file);
69
- const outputPath = path.join(outputDir, file.replace(".ejs", ""));
217
+ const relativePath = path.relative(dir, path.resolve(dir, file));
218
+ const templatePath = path.join(dir, relativePath);
219
+ const outputPath = path.join(outputDir, relativePath.replace(".ejs", ""));
70
220
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
71
221
  const templateContent = await fs.readFile(templatePath, {
72
222
  encoding: "utf-8"
73
223
  });
74
224
  const renderedContent = ejs.render(templateContent, ejsData);
75
- await fs.writeFile(outputPath, renderedContent);
225
+ await fs.writeFile(outputPath, renderedContent, { encoding: "utf-8" });
76
226
  }
77
227
  }
78
- consola.start(`Installing dependencies with ${packageManager}...`);
79
- try {
80
- await runCommand(packageManager, ["install"], outputDir);
81
- consola.success(`Dependencies installed successfully.`);
82
- } catch (e) {
83
- consola.fail("Failed to install dependencies.");
84
- consola.error(e);
85
- process.exit(1);
228
+ await handleClaspSetup(
229
+ clasp,
230
+ projectName,
231
+ outputDir,
232
+ claspProjectId,
233
+ packageManager
234
+ );
235
+ if (install) {
236
+ consola.start(`Installing dependencies with ${packageManager}...`);
237
+ try {
238
+ await runCommand(packageManager, ["install"], outputDir);
239
+ consola.success(`Dependencies installed successfully.`);
240
+ } catch (e) {
241
+ consola.fail("Failed to install dependencies. Please do it manually.");
242
+ consola.error(e);
243
+ }
86
244
  }
87
245
  consola.start(`Initializing Git repository...`);
88
246
  try {
@@ -94,15 +252,28 @@ async function generateProject({
94
252
  outputDir
95
253
  );
96
254
  consola.success(`Git repository initialized successfully.`);
97
- } catch {
255
+ } catch (e) {
98
256
  consola.fail("Failed to initialize Git repository. Please do it manually.");
257
+ consola.error(e);
99
258
  }
100
259
  consola.success(`Project '${projectName}' created successfully!`);
101
- consola.log(`
260
+ const messages = [];
261
+ projectName !== "." && messages.push(` cd ${projectName}`);
262
+ !install && messages.push(` ${packageManager} install`);
263
+ templateType !== "vanilla-ts" && messages.push(` ${packageManager} dev`);
264
+ if (messages.length > 0) {
265
+ consola.log(`
102
266
  To get started, run:
103
267
  `);
104
- projectName !== "." && consola.log(` cd ${projectName}`);
105
- templateType !== "vanilla-ts" ? consola.log(` ${packageManager} dev`) : consola.log(` ...and write your GAS code!`);
268
+ for (const message of messages) {
269
+ consola.log(message);
270
+ }
271
+ consola.log("");
272
+ consola.log(`...and write your GAS code!`);
273
+ } else {
274
+ consola.log(`
275
+ To get started, write your GAS code in \`src/\`!`);
276
+ }
106
277
  }
107
278
 
108
279
  export { generateProject, runCommand };
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title><% projectName %></title>
7
+ <title><%= projectName %></title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "<% projectName %>",
2
+ "name": "<%= projectName %>",
3
3
  "private": true,
4
4
  "version": "0.0.0",
5
5
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "<% projectName %>",
2
+ "name": "<%= projectName %>",
3
3
  "private": true,
4
4
  "version": "0.0.0",
5
5
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciderjs/gasbombe",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A TypeScript Project Generator for GoogleAppsScript, available as CLI",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -15,12 +15,12 @@
15
15
  "module": "dist/index.mjs",
16
16
  "types": "dist/index.d.ts",
17
17
  "scripts": {
18
+ "dev": "jiti src/cli",
18
19
  "tsc": "tsgo --noEmit src",
19
20
  "check": "biome check --write",
20
21
  "generate": "jiti scripts/copyTemplates",
21
- "prebuild": "pnpm run tsc",
22
- "build": "unbuild",
23
- "postbuild": "pnpm run generate",
22
+ "test": "vitest run --coverage",
23
+ "build": "pnpm run tsc && pnpm run test --silent && unbuild && pnpm run generate",
24
24
  "postinstall": "pnpm run generate"
25
25
  },
26
26
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "typescript",
30
30
  "react"
31
31
  ],
32
- "author": "luthpg",
32
+ "author": "ciderjs/luthpg",
33
33
  "license": "MIT",
34
34
  "repository": {
35
35
  "type": "git",
@@ -51,8 +51,10 @@
51
51
  "@types/ejs": "^3.1.5",
52
52
  "@types/node": "^24.5.2",
53
53
  "@typescript/native-preview": "7.0.0-dev.20250925.1",
54
+ "@vitest/coverage-v8": "3.2.4",
54
55
  "jiti": "^2.6.0",
55
56
  "typescript": "^5.9.2",
56
- "unbuild": "^3.6.1"
57
+ "unbuild": "^3.6.1",
58
+ "vitest": "^3.2.4"
57
59
  }
58
60
  }