@gannochenko/staticstripes 0.0.12 → 0.0.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.
Files changed (111) hide show
  1. package/Makefile +20 -0
  2. package/dist/asset-manager.d.ts +1 -0
  3. package/dist/asset-manager.d.ts.map +1 -1
  4. package/dist/asset-manager.js +3 -0
  5. package/dist/asset-manager.js.map +1 -1
  6. package/dist/cli/ai-generation-strategy-factory.d.ts +23 -0
  7. package/dist/cli/ai-generation-strategy-factory.d.ts.map +1 -0
  8. package/dist/cli/ai-generation-strategy-factory.js +44 -0
  9. package/dist/cli/ai-generation-strategy-factory.js.map +1 -0
  10. package/dist/cli/ai-generation-strategy.d.ts +33 -0
  11. package/dist/cli/ai-generation-strategy.d.ts.map +1 -0
  12. package/dist/cli/ai-generation-strategy.js +3 -0
  13. package/dist/cli/ai-generation-strategy.js.map +1 -0
  14. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +38 -0
  15. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -0
  16. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +174 -0
  17. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -0
  18. package/dist/cli/auth-strategy-factory.d.ts +31 -0
  19. package/dist/cli/auth-strategy-factory.d.ts.map +1 -0
  20. package/dist/cli/auth-strategy-factory.js +61 -0
  21. package/dist/cli/auth-strategy-factory.js.map +1 -0
  22. package/dist/cli/auth-strategy.d.ts +31 -0
  23. package/dist/cli/auth-strategy.d.ts.map +1 -0
  24. package/dist/cli/auth-strategy.js +3 -0
  25. package/dist/cli/auth-strategy.js.map +1 -0
  26. package/dist/cli/commands/auth.d.ts +6 -0
  27. package/dist/cli/commands/auth.d.ts.map +1 -0
  28. package/dist/cli/commands/auth.js +103 -0
  29. package/dist/cli/commands/auth.js.map +1 -0
  30. package/dist/cli/commands/generate.d.ts.map +1 -1
  31. package/dist/cli/commands/generate.js +69 -2
  32. package/dist/cli/commands/generate.js.map +1 -1
  33. package/dist/cli/instagram/instagram-auth-strategy.d.ts +31 -0
  34. package/dist/cli/instagram/instagram-auth-strategy.d.ts.map +1 -0
  35. package/dist/cli/instagram/instagram-auth-strategy.js +505 -0
  36. package/dist/cli/instagram/instagram-auth-strategy.js.map +1 -0
  37. package/dist/cli/instagram/instagram-upload-strategy.d.ts +45 -0
  38. package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -0
  39. package/dist/cli/instagram/instagram-upload-strategy.js +303 -0
  40. package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -0
  41. package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
  42. package/dist/cli/s3/s3-upload-strategy.js +7 -3
  43. package/dist/cli/s3/s3-upload-strategy.js.map +1 -1
  44. package/dist/cli/upload-strategy-factory.d.ts +1 -1
  45. package/dist/cli/upload-strategy-factory.d.ts.map +1 -1
  46. package/dist/cli/upload-strategy-factory.js +5 -5
  47. package/dist/cli/upload-strategy-factory.js.map +1 -1
  48. package/dist/cli/youtube/youtube-auth-strategy.d.ts +11 -0
  49. package/dist/cli/youtube/youtube-auth-strategy.d.ts.map +1 -0
  50. package/dist/cli/youtube/youtube-auth-strategy.js +320 -0
  51. package/dist/cli/youtube/youtube-auth-strategy.js.map +1 -0
  52. package/dist/cli/youtube/youtube-upload-strategy.d.ts +10 -3
  53. package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -1
  54. package/dist/cli/youtube/youtube-upload-strategy.js +96 -16
  55. package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -1
  56. package/dist/cli.js +2 -3
  57. package/dist/cli.js.map +1 -1
  58. package/dist/html-project-parser.d.ts +40 -1
  59. package/dist/html-project-parser.d.ts.map +1 -1
  60. package/dist/html-project-parser.js +343 -9
  61. package/dist/html-project-parser.js.map +1 -1
  62. package/dist/lib/file.d.ts +2 -0
  63. package/dist/lib/file.d.ts.map +1 -0
  64. package/dist/lib/file.js +13 -0
  65. package/dist/lib/file.js.map +1 -0
  66. package/dist/lib/net.d.ts +19 -0
  67. package/dist/lib/net.d.ts.map +1 -0
  68. package/dist/lib/net.js +101 -0
  69. package/dist/lib/net.js.map +1 -0
  70. package/dist/project.d.ts +5 -2
  71. package/dist/project.d.ts.map +1 -1
  72. package/dist/project.js +9 -1
  73. package/dist/project.js.map +1 -1
  74. package/dist/type.d.ts +17 -0
  75. package/dist/type.d.ts.map +1 -1
  76. package/package.json +1 -1
  77. package/src/asset-manager.ts +4 -0
  78. package/src/cli/ai-generation-strategy-factory.ts +48 -0
  79. package/src/cli/ai-generation-strategy.ts +35 -0
  80. package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +266 -0
  81. package/src/cli/auth-strategy-factory.ts +67 -0
  82. package/src/cli/auth-strategy.ts +37 -0
  83. package/src/cli/commands/auth.ts +120 -0
  84. package/src/cli/commands/generate.ts +55 -2
  85. package/src/cli/instagram/instagram-auth-strategy.ts +569 -0
  86. package/src/cli/instagram/instagram-upload-strategy.ts +398 -0
  87. package/src/cli/s3/s3-upload-strategy.ts +7 -3
  88. package/src/cli/upload-strategy-factory.ts +6 -9
  89. package/src/cli/youtube/youtube-auth-strategy.ts +323 -0
  90. package/src/cli/youtube/youtube-upload-strategy.ts +147 -16
  91. package/src/cli.ts +2 -4
  92. package/src/html-project-parser.ts +429 -8
  93. package/src/lib/file.ts +11 -0
  94. package/src/lib/net.ts +120 -0
  95. package/src/project.ts +10 -0
  96. package/src/type.ts +19 -0
  97. package/dist/cli/youtube/auth-commands.d.ts +0 -3
  98. package/dist/cli/youtube/auth-commands.d.ts.map +0 -1
  99. package/dist/cli/youtube/auth-commands.js +0 -273
  100. package/dist/cli/youtube/auth-commands.js.map +0 -1
  101. package/dist/cli/youtube/cli.d.ts +0 -7
  102. package/dist/cli/youtube/cli.d.ts.map +0 -1
  103. package/dist/cli/youtube/cli.js +0 -13
  104. package/dist/cli/youtube/cli.js.map +0 -1
  105. package/dist/cli/youtube/upload-handler.d.ts +0 -12
  106. package/dist/cli/youtube/upload-handler.d.ts.map +0 -1
  107. package/dist/cli/youtube/upload-handler.js +0 -66
  108. package/dist/cli/youtube/upload-handler.js.map +0 -1
  109. package/src/cli/youtube/auth-commands.ts +0 -312
  110. package/src/cli/youtube/cli.ts +0 -11
  111. package/src/cli/youtube/upload-handler.ts +0 -101
@@ -0,0 +1,323 @@
1
+ import { AuthStrategy, AuthOptions } from '../auth-strategy';
2
+ import { YouTubeUploader } from '../../youtube-uploader.js';
3
+ import open from 'open';
4
+ import http from 'http';
5
+ import { parse as parseUrl } from 'url';
6
+ import * as readline from 'readline';
7
+ import { writeFileSync } from 'fs';
8
+ import { resolve } from 'path';
9
+
10
+ /**
11
+ * YouTube authentication strategy
12
+ * Uses OAuth 2.0 flow with local callback server
13
+ */
14
+ export class YouTubeAuthStrategy implements AuthStrategy {
15
+ getTag(): string {
16
+ return 'youtube';
17
+ }
18
+
19
+ async execute(
20
+ uploadName: string,
21
+ projectPath: string,
22
+ _options?: AuthOptions,
23
+ ): Promise<void> {
24
+ console.log(`šŸ” YouTube Authentication Setup\n`);
25
+
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout,
29
+ });
30
+
31
+ const question = (prompt: string): Promise<string> => {
32
+ return new Promise((resolve) => {
33
+ rl.question(prompt, (answer) => {
34
+ resolve(answer);
35
+ });
36
+ });
37
+ };
38
+
39
+ try {
40
+ console.log('━'.repeat(60));
41
+ console.log('STEP 1: Enter YouTube API Credentials');
42
+ console.log('━'.repeat(60));
43
+ console.log('');
44
+ console.log('šŸ’” Run `staticstripes auth-help youtube` for setup instructions\n');
45
+
46
+ const clientId = await question('Enter your OAuth Client ID: ');
47
+ if (!clientId || clientId.trim().length < 10) {
48
+ throw new Error('Invalid Client ID');
49
+ }
50
+
51
+ const clientSecret = await question('Enter your OAuth Client Secret: ');
52
+ if (!clientSecret || clientSecret.trim().length < 10) {
53
+ throw new Error('Invalid Client Secret');
54
+ }
55
+
56
+ console.log('\n━'.repeat(60));
57
+ console.log('STEP 2: Authorize with Google');
58
+ console.log('━'.repeat(60));
59
+ console.log('');
60
+
61
+ rl.close();
62
+
63
+ // Create uploader instance
64
+ const uploader = new YouTubeUploader(
65
+ clientId.trim(),
66
+ clientSecret.trim(),
67
+ );
68
+
69
+ // Get authorization URL
70
+ const authUrl = uploader.getAuthUrl();
71
+
72
+ console.log('🌐 Starting local server on http://localhost:3000...\n');
73
+
74
+ // Create a promise that resolves when we get the OAuth callback
75
+ const authPromise = new Promise<string>((resolve, reject) => {
76
+ // Track all connections to force-close them
77
+ const connections = new Set<any>();
78
+
79
+ const server = http.createServer((req, res) => {
80
+ const url = parseUrl(req.url || '', true);
81
+
82
+ if (url.pathname === '/oauth2callback') {
83
+ const code = url.query.code as string;
84
+ const error = url.query.error as string;
85
+
86
+ const closeServer = () => {
87
+ // Destroy all connections
88
+ connections.forEach((socket) => {
89
+ socket.destroy();
90
+ });
91
+ connections.clear();
92
+ server.close();
93
+ };
94
+
95
+ if (error) {
96
+ res.writeHead(200, { 'Content-Type': 'text/html' });
97
+ res.end(`
98
+ <html>
99
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
100
+ <h1>āŒ Authorization Failed</h1>
101
+ <p>Error: ${error}</p>
102
+ <p>You can close this window.</p>
103
+ </body>
104
+ </html>
105
+ `);
106
+ res.on('finish', closeServer);
107
+ reject(new Error(`Authorization failed: ${error}`));
108
+ return;
109
+ }
110
+
111
+ if (code) {
112
+ res.writeHead(200, { 'Content-Type': 'text/html' });
113
+ res.end(`
114
+ <html>
115
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
116
+ <h1>Authorization Successful!</h1>
117
+ <p>You can close this window and return to the terminal.</p>
118
+ </body>
119
+ </html>
120
+ `);
121
+ res.on('finish', closeServer);
122
+ resolve(code);
123
+ } else {
124
+ res.writeHead(400, { 'Content-Type': 'text/html' });
125
+ res.end(`
126
+ <html>
127
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
128
+ <h1>āŒ No Authorization Code</h1>
129
+ <p>No code was received from Google.</p>
130
+ <p>You can close this window.</p>
131
+ </body>
132
+ </html>
133
+ `);
134
+ res.on('finish', closeServer);
135
+ reject(new Error('No authorization code received'));
136
+ }
137
+ } else {
138
+ res.writeHead(404);
139
+ res.end('Not found');
140
+ }
141
+ });
142
+
143
+ // Track connections
144
+ server.on('connection', (socket) => {
145
+ connections.add(socket);
146
+ socket.on('close', () => {
147
+ connections.delete(socket);
148
+ });
149
+ });
150
+
151
+ server.listen(3000, () => {
152
+ console.log('āœ… Server started successfully\n');
153
+ });
154
+
155
+ // Set timeout to avoid hanging forever
156
+ setTimeout(
157
+ () => {
158
+ connections.forEach((socket) => {
159
+ socket.destroy();
160
+ });
161
+ connections.clear();
162
+ server.close();
163
+ reject(new Error('Authentication timeout (5 minutes)'));
164
+ },
165
+ 5 * 60 * 1000,
166
+ );
167
+ });
168
+
169
+ console.log('🌐 Opening browser for authorization...\n');
170
+
171
+ // Open browser automatically
172
+ try {
173
+ await open(authUrl);
174
+ console.log('āœ… Browser opened successfully\n');
175
+ } catch (err) {
176
+ console.log('āš ļø Could not open browser automatically');
177
+ console.log('🌐 Please visit this URL to authorize:\n');
178
+ console.log(authUrl);
179
+ console.log();
180
+ }
181
+
182
+ console.log('ā³ Waiting for authorization...\n');
183
+
184
+ // Wait for the OAuth callback
185
+ const code = await authPromise;
186
+
187
+ console.log('šŸ”‘ Authorization code received\n');
188
+ console.log('šŸ’¾ Saving authentication tokens...\n');
189
+
190
+ // Complete authentication - saves OAuth tokens to .auth file
191
+ await uploader.authenticate(code, uploadName, projectPath);
192
+
193
+ // Now add clientId and clientSecret to the saved file
194
+ const authDir = resolve(projectPath, '.auth');
195
+ const credentialsPath = resolve(authDir, `${uploadName}.json`);
196
+
197
+ // Read the tokens that were just saved
198
+ const { readFileSync } = await import('fs');
199
+ const savedTokens = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
200
+
201
+ // Add clientId and clientSecret
202
+ const fullCredentials = {
203
+ clientId: clientId.trim(),
204
+ clientSecret: clientSecret.trim(),
205
+ ...savedTokens,
206
+ };
207
+
208
+ // Save back with all credentials
209
+ writeFileSync(
210
+ credentialsPath,
211
+ JSON.stringify(fullCredentials, null, 2),
212
+ 'utf-8',
213
+ );
214
+
215
+ console.log(`āœ… Authentication complete for ${uploadName}!\n`);
216
+ console.log(`šŸ“ Credentials saved to: ${credentialsPath}\n`);
217
+ } catch (error) {
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ getSetupInstructions(): string {
223
+ return `
224
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
225
+ YouTube Authentication Setup
226
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
227
+
228
+ Interactive OAuth 2.0 flow - no environment variables needed!
229
+
230
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
231
+ STEP 1: Go to Google Cloud Console
232
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
233
+ URL: https://console.cloud.google.com/
234
+
235
+ 1. Create or select a project
236
+
237
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
238
+ STEP 2: Enable YouTube Data API v3
239
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
240
+ 1. Go to "APIs & Services" > "Library"
241
+ 2. Search for "YouTube Data API v3"
242
+ 3. Click "Enable"
243
+
244
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
245
+ STEP 3: Configure OAuth Consent Screen
246
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
247
+ 1. Go to "APIs & Services" > "OAuth consent screen"
248
+ 2. Choose "External" user type
249
+ 3. Fill in:
250
+ • App name: "My YouTube Uploader"
251
+ • User support email: your.email@example.com
252
+ • Developer contact email: your.email@example.com
253
+ 4. Click "Save and Continue"
254
+ 5. Add scope: https://www.googleapis.com/auth/youtube.upload
255
+ 6. Click "Save and Continue"
256
+ 7. Add your email as a test user
257
+ 8. Click "Save and Continue"
258
+
259
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
260
+ STEP 4: Create OAuth 2.0 Credentials
261
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
262
+ 1. Go to "APIs & Services" > "Credentials"
263
+ 2. Click "Create Credentials" > "OAuth client ID"
264
+ 3. Choose "Web application"
265
+ 4. Name: "YouTube Uploader"
266
+ 5. Add redirect URI: http://localhost:3000/oauth2callback
267
+ (Make sure it's exactly this - no trailing slash!)
268
+ 6. Click "Create"
269
+ 7. Copy your Client ID (looks like: xxx.apps.googleusercontent.com)
270
+ 8. Copy your Client Secret
271
+
272
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
273
+ STEP 5: Publish Your OAuth App (IMPORTANT!)
274
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275
+ 1. Go to "APIs & Services" > "OAuth consent screen"
276
+ 2. Click "PUBLISH APP" button
277
+ 3. This makes refresh tokens permanent (otherwise they expire in 7 days)
278
+ 4. Note: For personal use, you don't need Google verification
279
+
280
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
281
+ STEP 6: Run Authentication Wizard
282
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
283
+ Run:
284
+ staticstripes auth --upload-name YOUR_UPLOAD_NAME
285
+
286
+ The wizard will:
287
+ 1. Ask you to enter your OAuth Client ID
288
+ 2. Ask you to enter your OAuth Client Secret
289
+ 3. Start local server on port 3000
290
+ 4. Open browser automatically for Google authorization
291
+ 5. Automatically exchange authorization code for tokens
292
+ 6. Save ALL credentials to .auth/YOUR_UPLOAD_NAME.json
293
+
294
+ Done! Interactive and secure - no environment variables needed!
295
+
296
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
297
+ TROUBLESHOOTING
298
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
299
+ āŒ "redirect_uri_mismatch"
300
+ → Make sure redirect URI is exactly: http://localhost:3000/oauth2callback
301
+ → No trailing slash, no typos!
302
+
303
+ āŒ "Invalid client" error
304
+ → Double-check your Client ID and Client Secret
305
+ → Make sure you copied them correctly
306
+
307
+ āŒ Tokens expire after 7 days
308
+ → Publish your OAuth app (Step 5)
309
+ → This makes refresh tokens last indefinitely
310
+
311
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
312
+ REFERENCE LINKS
313
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
314
+ • Google Cloud Console:
315
+ https://console.cloud.google.com/
316
+
317
+ • YouTube Data API docs:
318
+ https://developers.google.com/youtube/v3
319
+
320
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
321
+ `;
322
+ }
323
+ }
@@ -1,30 +1,28 @@
1
1
  import { UploadStrategy } from '../upload-strategy';
2
2
  import { Project } from '../../project';
3
3
  import { YouTubeUpload } from '../../type';
4
- import { handleYouTubeUpload } from './upload-handler';
4
+ import { resolve } from 'path';
5
+ import { YouTubeUploader } from '../../youtube-uploader';
6
+ import ejs from 'ejs';
7
+ import { readFileSync, existsSync } from 'fs';
8
+
9
+ export interface YouTubeUploadOptions {
10
+ uploadName: string;
11
+ projectPath: string;
12
+ clientId: string;
13
+ clientSecret: string;
14
+ }
5
15
 
6
16
  /**
7
17
  * YouTube upload strategy implementation
8
18
  */
9
19
  export class YouTubeUploadStrategy implements UploadStrategy {
10
- constructor(
11
- private clientId: string,
12
- private clientSecret: string,
13
- ) {}
14
-
15
20
  getTag(): string {
16
21
  return 'youtube';
17
22
  }
18
23
 
19
24
  validate(): void {
20
- if (!this.clientId || !this.clientSecret) {
21
- const error = new Error(
22
- 'āŒ Error: STATICSTRIPES_GOOGLE_CLIENT_ID and STATICSTRIPES_GOOGLE_CLIENT_SECRET environment variables are not set\n\n' +
23
- 'šŸ’” Run: staticstripes auth --help\n' +
24
- ' for complete setup instructions',
25
- );
26
- throw error;
27
- }
25
+ // Validation now happens in execute() when we read credentials
28
26
  }
29
27
 
30
28
  async execute(
@@ -32,12 +30,145 @@ export class YouTubeUploadStrategy implements UploadStrategy {
32
30
  upload: YouTubeUpload,
33
31
  projectPath: string,
34
32
  ): Promise<void> {
33
+ // Read credentials from .auth file
34
+ const authDir = resolve(projectPath, '.auth');
35
+ const credentialsPath = resolve(authDir, `${upload.name}.json`);
36
+
37
+ if (!existsSync(credentialsPath)) {
38
+ throw new Error(
39
+ `āŒ Error: YouTube credentials not found\n\n` +
40
+ `Expected location: ${credentialsPath}\n\n` +
41
+ `šŸ’” Run authentication wizard:\n` +
42
+ ` staticstripes auth --upload-name ${upload.name}\n\n` +
43
+ `šŸ“– Or view setup instructions:\n` +
44
+ ` staticstripes auth-help youtube\n`,
45
+ );
46
+ }
47
+
48
+ let credentials: { clientId?: string; clientSecret?: string };
49
+ try {
50
+ const credentialsJson = readFileSync(credentialsPath, 'utf-8');
51
+ credentials = JSON.parse(credentialsJson);
52
+
53
+ if (!credentials.clientId || !credentials.clientSecret) {
54
+ throw new Error('Missing clientId or clientSecret');
55
+ }
56
+ } catch (error) {
57
+ throw new Error(
58
+ `āŒ Error: Failed to parse YouTube credentials from ${credentialsPath}\n` +
59
+ `Ensure the file contains clientId and clientSecret.\n` +
60
+ `Error: ${error instanceof Error ? error.message : String(error)}`,
61
+ );
62
+ }
63
+
35
64
  // Delegate to existing handler
36
65
  await handleYouTubeUpload(project, {
37
66
  uploadName: upload.name,
38
67
  projectPath,
39
- clientId: this.clientId,
40
- clientSecret: this.clientSecret,
68
+ clientId: credentials.clientId,
69
+ clientSecret: credentials.clientSecret,
41
70
  });
42
71
  }
43
72
  }
73
+
74
+ /**
75
+ * Handles YouTube video upload process
76
+ */
77
+ export async function handleYouTubeUpload(
78
+ project: Project,
79
+ options: YouTubeUploadOptions,
80
+ ): Promise<void> {
81
+ // Get upload configuration
82
+ const upload = project.getYouTubeUpload(options.uploadName);
83
+ if (!upload) {
84
+ const availableUploads = Array.from(project.getYouTubeUploads().keys());
85
+ throw new Error(
86
+ `Upload "${options.uploadName}" not found in project.html\n` +
87
+ (availableUploads.length > 0
88
+ ? `Available uploads: ${availableUploads.join(', ')}`
89
+ : 'No uploads defined in project.html'),
90
+ );
91
+ }
92
+
93
+ // Get the output file
94
+ const output = project.getOutput(upload.outputName);
95
+ if (!output) {
96
+ throw new Error(`Output "${upload.outputName}" not found`);
97
+ }
98
+
99
+ console.log(`šŸ“¹ Video file: ${output.path}`);
100
+ console.log(`šŸŽ¬ Upload config: ${options.uploadName}\n`);
101
+
102
+ // Create uploader and load tokens
103
+ const uploader = new YouTubeUploader(options.clientId, options.clientSecret);
104
+ const hasTokens = uploader.loadTokens(
105
+ options.uploadName,
106
+ options.projectPath,
107
+ );
108
+
109
+ if (!hasTokens) {
110
+ throw new Error(
111
+ `Not authenticated. Please run: staticstripes auth --upload-name ${options.uploadName}`,
112
+ );
113
+ }
114
+
115
+ // Determine title (use upload-specific title or fall back to project title)
116
+ const title = upload.title || project.getTitle();
117
+ console.log(`šŸ“ Title: ${title}\n`);
118
+
119
+ // Build the project to populate fragment times (needed for timecodes)
120
+ console.log('šŸ”Ø Building project to calculate timecodes...');
121
+ await project.build(upload.outputName);
122
+
123
+ // Get timecodes and process description with EJS
124
+ const timecodes = project.getTimecodes();
125
+
126
+ // Format tags (space-separated, no hashtags for YouTube)
127
+ const formattedTags = upload.tags.join(' ');
128
+
129
+ // Convert ${variable} syntax to <%= variable %> for EJS compatibility
130
+ const ejsDescription = upload.description.replace(
131
+ /\$\{(\w+)\}/g,
132
+ '<%= $1 %>',
133
+ );
134
+
135
+ const processedDescription = ejs.render(ejsDescription, {
136
+ title,
137
+ tags: formattedTags,
138
+ timecodes: timecodes.join('\n'),
139
+ });
140
+
141
+ // Create a processed upload object with rendered description
142
+ const processedUpload = {
143
+ ...upload,
144
+ description: processedDescription,
145
+ };
146
+
147
+ // Upload video
148
+ const videoId = await uploader.uploadVideo(
149
+ output.path,
150
+ processedUpload,
151
+ title,
152
+ );
153
+
154
+ // Handle thumbnail if specified
155
+ if (upload.thumbnailTimecode !== undefined) {
156
+ console.log(
157
+ `\nšŸ–¼ļø Extracting thumbnail at ${upload.thumbnailTimecode}ms...`,
158
+ );
159
+ const thumbnailPath = resolve(
160
+ options.projectPath,
161
+ '.cache',
162
+ 'thumbnail.png',
163
+ );
164
+ await uploader.extractThumbnail(
165
+ output.path,
166
+ upload.thumbnailTimecode,
167
+ thumbnailPath,
168
+ );
169
+ await uploader.uploadThumbnail(videoId, thumbnailPath);
170
+ }
171
+
172
+ // TODO: Update project.html with video ID
173
+ console.log('\nāœ… Upload complete!');
174
+ }
package/src/cli.ts CHANGED
@@ -7,7 +7,7 @@ import { registerGenerateCommand } from './cli/commands/generate.js';
7
7
  import { registerBootstrapCommand } from './cli/commands/bootstrap.js';
8
8
  import { registerAddAssetsCommand } from './cli/commands/add-assets.js';
9
9
  import { registerUploadCommand } from './cli/commands/upload.js';
10
- import { registerYouTubeCommands } from './cli/youtube/cli.js';
10
+ import { registerAuthCommand } from './cli/commands/auth.js';
11
11
 
12
12
  // Read version from package.json
13
13
  // In built code, this file is at dist/cli.js, package.json is at ../package.json
@@ -64,8 +64,6 @@ registerGenerateCommand(program, () => isDebugMode, handleError);
64
64
  registerBootstrapCommand(program, handleError);
65
65
  registerAddAssetsCommand(program, handleError);
66
66
  registerUploadCommand(program, handleError);
67
-
68
- // Register provider-specific commands (auth, etc.)
69
- registerYouTubeCommands(program);
67
+ registerAuthCommand(program, handleError);
70
68
 
71
69
  program.parse(process.argv);