@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/src/index.ts 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';
@@ -10,30 +11,201 @@ export async function runCommand(
10
11
  command: string,
11
12
  args: string[],
12
13
  cwd: string,
13
- ): Promise<void> {
14
+ capture = false,
15
+ ): Promise<string> {
14
16
  return new Promise((resolve, reject) => {
15
17
  const child = spawn(command, args, {
16
18
  cwd,
17
- stdio: 'inherit',
19
+ stdio: capture ? 'pipe' : 'inherit',
18
20
  shell: true,
19
21
  });
22
+
23
+ let stdout = '';
24
+ let stderr = '';
25
+
26
+ if (capture) {
27
+ child.stdout?.on('data', (data) => {
28
+ stdout += data.toString();
29
+ });
30
+ child.stderr?.on('data', (data) => {
31
+ stderr += data.toString();
32
+ });
33
+ }
34
+
20
35
  child.on('close', (code) => {
21
36
  if (code === 0) {
22
- resolve();
37
+ resolve(stdout.trim());
23
38
  } else {
24
- reject(new Error(`Command failed with exit code ${code}`));
39
+ const errorMsg = `Command failed with exit code ${code}${stderr ? `:\n${stderr}` : ''}`;
40
+ reject(new Error(errorMsg));
25
41
  }
26
42
  });
43
+
27
44
  child.on('error', (err) => {
28
45
  reject(err);
29
46
  });
30
47
  });
31
48
  }
32
49
 
50
+ async function handleClaspSetup(
51
+ claspOption: ProjectOptions['clasp'],
52
+ projectName: string,
53
+ outputDir: string,
54
+ claspProjectId: string | undefined,
55
+ packageManager: ProjectOptions['packageManager'] = 'npm',
56
+ ): Promise<void> {
57
+ if (claspOption === 'skip') {
58
+ return;
59
+ }
60
+
61
+ const npxLikeCommand =
62
+ packageManager === 'npm'
63
+ ? 'npx'
64
+ : packageManager === 'pnpm'
65
+ ? 'pnpx'
66
+ : 'yarn';
67
+
68
+ consola.start(
69
+ `Setting up .clasp.json with \`${npxLikeCommand} @google/clasp\`...`,
70
+ );
71
+
72
+ if (claspOption === 'create' || claspOption === 'list') {
73
+ try {
74
+ await runCommand(npxLikeCommand, ['@google/clasp', 'status'], outputDir);
75
+ } catch {
76
+ consola.error(
77
+ `It seems you are not logged in to clasp. Please run \`${npxLikeCommand} @google/clasp login\` and try again.`,
78
+ );
79
+ return;
80
+ }
81
+ }
82
+
83
+ let scriptId: string | undefined;
84
+ let existingClaspJson: { scriptId: string; [key: string]: string | string[] } | null = null;
85
+
86
+ switch (claspOption) {
87
+ case 'create':
88
+ try {
89
+ const claspJsonPath = path.join(outputDir, '.clasp.json');
90
+ try {
91
+ const existingContent = await fs.readFile(claspJsonPath, 'utf-8');
92
+ existingClaspJson = JSON.parse(existingContent);
93
+ await fs.unlink(claspJsonPath);
94
+ } catch {
95
+ // No existing .clasp.json, which is fine.
96
+ }
97
+
98
+ const result = await runCommand(
99
+ npxLikeCommand,
100
+ [
101
+ '@google/clasp',
102
+ 'create',
103
+ '--title',
104
+ `"${projectName}"`,
105
+ '--type',
106
+ 'standalone',
107
+ ],
108
+ outputDir,
109
+ true,
110
+ );
111
+ const match = result.match(/Created new script: .+\/d\/(.+)\/edit/);
112
+ if (match?.[1]) {
113
+ scriptId = match[1];
114
+ consola.info(`Created new Apps Script project with ID: ${scriptId}`);
115
+ } else {
116
+ throw new Error('Could not parse scriptId from clasp output.');
117
+ }
118
+ } catch (e) {
119
+ consola.error('Failed to create new Apps Script project.', e);
120
+ return;
121
+ }
122
+ break;
123
+
124
+ case 'list':
125
+ try {
126
+ const listOutput = await runCommand(
127
+ npxLikeCommand,
128
+ ['@google/clasp', 'list'],
129
+ outputDir,
130
+ true,
131
+ );
132
+ const projects = listOutput
133
+ .split('\n')
134
+ .slice(1) // Skip header
135
+ .map((line) => {
136
+ const parts = line.split(' - ');
137
+ if (parts.length >= 2) {
138
+ const name = parts[0]?.trim();
139
+ const url = parts[1]?.trim();
140
+ if (!name || !url) return null;
141
+
142
+ const match = url.match(/\/d\/(.+)\/edit/);
143
+ const scriptId = match?.[1];
144
+
145
+ if (scriptId) {
146
+ return { name, value: scriptId };
147
+ }
148
+ }
149
+ return null;
150
+ })
151
+ .filter((p): p is { name: string; value: string } => p !== null);
152
+
153
+ if (projects.length === 0) {
154
+ consola.warn(
155
+ 'No existing Apps Script projects found. Please create one first.',
156
+ );
157
+ return;
158
+ }
159
+
160
+ scriptId = await select({
161
+ message: 'Choose an existing Apps Script project:',
162
+ choices: projects,
163
+ });
164
+ } catch (e) {
165
+ consola.error('Failed to list Apps Script projects.', e);
166
+ return;
167
+ }
168
+ break;
169
+
170
+ case 'input':
171
+ scriptId = claspProjectId;
172
+ break;
173
+ }
174
+
175
+ if (scriptId) {
176
+ const claspJsonPath = path.join(outputDir, '.clasp.json');
177
+ let claspJson: { scriptId: string; [key: string]: string | string[] } = {
178
+ scriptId,
179
+ };
180
+ let successMessage = `.clasp.json created successfully with scriptId: ${scriptId}`;
181
+
182
+ if (existingClaspJson) {
183
+ claspJson = { ...existingClaspJson, scriptId };
184
+ successMessage = `.clasp.json updated successfully with scriptId: ${scriptId}`;
185
+ } else {
186
+ try {
187
+ const existingContent = await fs.readFile(claspJsonPath, 'utf-8');
188
+ const currentClaspJson = JSON.parse(existingContent);
189
+ claspJson = { ...currentClaspJson, scriptId };
190
+ successMessage = `.clasp.json updated successfully with scriptId: ${scriptId}`;
191
+ } catch {
192
+ // If file doesn't exist or is invalid, we'll just create a new one.
193
+ }
194
+ }
195
+
196
+ const claspJsonContent = JSON.stringify(claspJson, null, 2);
197
+ await fs.writeFile(claspJsonPath, claspJsonContent, { encoding: 'utf-8' });
198
+ consola.success(successMessage);
199
+ }
200
+ }
201
+
33
202
  export async function generateProject({
34
203
  projectName,
35
204
  packageManager,
36
205
  templateType,
206
+ clasp,
207
+ claspProjectId,
208
+ install,
37
209
  }: ProjectOptions): Promise<void> {
38
210
  const outputDir = path.resolve(process.cwd(), projectName);
39
211
  const templateBaseDir = path.resolve(__dirname, '..', 'dist', 'templates');
@@ -44,12 +216,32 @@ export async function generateProject({
44
216
  `Creating a new Project for GoogleAppsScript in ${outputDir}...`,
45
217
  );
46
218
 
47
- try {
48
- await fs.access(outputDir);
49
- consola.error(`Directory ${projectName} already exists.`);
50
- process.exit(1);
51
- } catch {
52
- // Directory dose not exits, which is what we want.
219
+ if (projectName === '.') {
220
+ try {
221
+ const files = await fs.readdir(outputDir);
222
+ const relevantFiles = files.filter((file) => !file.startsWith('.'));
223
+
224
+ if (relevantFiles.length > 0) {
225
+ const proceed = await confirm(
226
+ 'Current directory is not empty. Proceed anyway?',
227
+ );
228
+
229
+ if (!proceed) {
230
+ consola.warn('Operation cancelled.');
231
+ process.exit(0);
232
+ }
233
+ }
234
+ } catch {
235
+ // Directory does not exist, then mkdir will create it.
236
+ }
237
+ } else {
238
+ try {
239
+ await fs.access(outputDir);
240
+ consola.error(`Directory ${projectName} already exists.`);
241
+ process.exit(1);
242
+ } catch {
243
+ // Directory does not exist, which is what we want.
244
+ }
53
245
  }
54
246
 
55
247
  await fs.mkdir(outputDir, { recursive: true });
@@ -62,14 +254,12 @@ export async function generateProject({
62
254
  cwd: dir,
63
255
  nodir: true,
64
256
  dot: true,
65
- dotRelative: true,
66
- follow: true,
67
- windowsPathsNoEscape: true,
68
257
  });
69
258
 
70
259
  for (const file of files) {
71
- const templatePath = path.join(dir, file);
72
- const outputPath = path.join(outputDir, file.replace('.ejs', ''));
260
+ const relativePath = path.relative(dir, path.resolve(dir, file));
261
+ const templatePath = path.join(dir, relativePath);
262
+ const outputPath = path.join(outputDir, relativePath.replace('.ejs', ''));
73
263
 
74
264
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
75
265
 
@@ -77,18 +267,27 @@ export async function generateProject({
77
267
  encoding: 'utf-8',
78
268
  });
79
269
  const renderedContent = ejs.render(templateContent, ejsData);
80
- await fs.writeFile(outputPath, renderedContent);
270
+ await fs.writeFile(outputPath, renderedContent, { encoding: 'utf-8' });
81
271
  }
82
272
  }
83
273
 
84
- consola.start(`Installing dependencies with ${packageManager}...`);
85
- try {
86
- await runCommand(packageManager, ['install'], outputDir);
87
- consola.success(`Dependencies installed successfully.`);
88
- } catch (e) {
89
- consola.fail('Failed to install dependencies.');
90
- consola.error(e);
91
- process.exit(1);
274
+ await handleClaspSetup(
275
+ clasp,
276
+ projectName,
277
+ outputDir,
278
+ claspProjectId,
279
+ packageManager,
280
+ );
281
+
282
+ if (install) {
283
+ consola.start(`Installing dependencies with ${packageManager}...`);
284
+ try {
285
+ await runCommand(packageManager, ['install'], outputDir);
286
+ consola.success(`Dependencies installed successfully.`);
287
+ } catch (e) {
288
+ consola.fail('Failed to install dependencies. Please do it manually.');
289
+ consola.error(e);
290
+ }
92
291
  }
93
292
 
94
293
  consola.start(`Initializing Git repository...`);
@@ -101,14 +300,26 @@ export async function generateProject({
101
300
  outputDir,
102
301
  );
103
302
  consola.success(`Git repository initialized successfully.`);
104
- } catch {
303
+ } catch (e) {
105
304
  consola.fail('Failed to initialize Git repository. Please do it manually.');
305
+ consola.error(e);
106
306
  }
107
307
 
108
308
  consola.success(`Project '${projectName}' created successfully!`);
109
- consola.log(`\nTo get started, run:\n`);
110
- projectName !== '.' && consola.log(` cd ${projectName}`);
111
- templateType !== 'vanilla-ts'
112
- ? consola.log(` ${packageManager} dev`)
113
- : consola.log(` ...and write your GAS code!`);
309
+
310
+ const messages: string[] = [];
311
+ projectName !== '.' && messages.push(` cd ${projectName}`);
312
+ !install && messages.push(` ${packageManager} install`);
313
+ templateType !== 'vanilla-ts' && messages.push(` ${packageManager} dev`);
314
+
315
+ if (messages.length > 0) {
316
+ consola.log(`\nTo get started, run:\n`);
317
+ for (const message of messages) {
318
+ consola.log(message);
319
+ }
320
+ consola.log('');
321
+ consola.log(`...and write your GAS code!`);
322
+ } else {
323
+ consola.log(`\nTo get started, write your GAS code in \`src/\`!`);
324
+ }
114
325
  }