create-glanceway-source 1.0.0 → 1.2.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/dist/index.js CHANGED
@@ -1,161 +1,600 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/index.ts
4
2
  import * as fs from "fs";
5
3
  import * as path from "path";
6
- import * as readline from "readline";
7
4
  import { fileURLToPath } from "url";
8
- var __filename = fileURLToPath(import.meta.url);
9
- var __dirname = path.dirname(__filename);
10
- var CATEGORIES = [
11
- "Developer",
12
- "News",
13
- "Social",
14
- "Finance",
15
- "Entertainment",
16
- "Productivity",
17
- "Other"
5
+ import prompts from "prompts";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const CATEGORIES = [
9
+ "Developer",
10
+ "News",
11
+ "Social",
12
+ "Finance",
13
+ "Entertainment",
14
+ "Productivity",
15
+ "Other",
18
16
  ];
19
- async function prompt(question, defaultValue) {
20
- const rl = readline.createInterface({
21
- input: process.stdin,
22
- output: process.stdout
23
- });
24
- return new Promise((resolve2) => {
25
- const displayQuestion = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
26
- rl.question(displayQuestion, (answer) => {
27
- rl.close();
28
- resolve2(answer.trim() || defaultValue || "");
29
- });
30
- });
31
- }
32
- async function confirm(question) {
33
- const answer = await prompt(`${question} (y/N)`);
34
- return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
35
- }
36
- async function selectCategory() {
37
- console.log("\nSelect a category:");
38
- CATEGORIES.forEach((cat, index2) => {
39
- console.log(` ${index2 + 1}. ${cat}`);
40
- });
41
- const selection = await prompt("Enter number", "1");
42
- const index = parseInt(selection, 10) - 1;
43
- if (index >= 0 && index < CATEGORIES.length) {
44
- return CATEGORIES[index];
45
- }
46
- return "Other";
47
- }
48
- function toKebabCase(str) {
49
- return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
50
- }
51
- async function getConfig(projectName) {
52
- const name = toKebabCase(projectName);
53
- const version = await prompt("Version", "1.0.0");
54
- const displayName = await prompt("Display name", projectName);
55
- const description = await prompt("Description", "A Glanceway information source");
56
- const author = await prompt("Author name");
57
- if (!author) {
58
- console.error("\u274C Error: Author name is required.");
59
- process.exit(1);
60
- }
61
- const authorUrl = await prompt("Author URL (optional)");
62
- const category = await selectCategory();
63
- return {
64
- name,
65
- version,
66
- displayName,
67
- description,
68
- author,
69
- authorUrl,
70
- category
71
- };
17
+ function parseArgs() {
18
+ const argv = process.argv.slice(2);
19
+ const result = {};
20
+ const flags = {};
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const arg = argv[i];
23
+ if (arg === "--help" || arg === "-h") {
24
+ printHelp();
25
+ process.exit(0);
26
+ }
27
+ else if (arg.startsWith("--")) {
28
+ const key = arg.slice(2);
29
+ const next = argv[i + 1];
30
+ if (next && !next.startsWith("--")) {
31
+ flags[key] = next;
32
+ i++;
33
+ }
34
+ }
35
+ else if (!result.projectDir) {
36
+ result.projectDir = arg;
37
+ }
38
+ }
39
+ result.name = flags["name"];
40
+ result.description = flags["description"];
41
+ result.author = flags["author"];
42
+ result.authorUrl = flags["author-url"];
43
+ result.category = flags["category"];
44
+ return result;
45
+ }
46
+ function printHelp() {
47
+ console.log(`
48
+ Usage: create-glanceway-source [project-directory] [options]
49
+
50
+ Options:
51
+ --name Source display name
52
+ --description Source description
53
+ --author Author name
54
+ --author-url Author URL
55
+ --category Category (${CATEGORIES.join(", ")})
56
+ -h, --help Show this help message
57
+
58
+ Examples:
59
+ npm create glanceway-source my-source
60
+ create-glanceway-source my-source --name "My Source" --author myname
61
+ `);
62
+ }
63
+ // ─── Validation ────────────────────────────────────────────────────────────
64
+ function validateAuthor(value) {
65
+ if (!value)
66
+ return "Author is required";
67
+ if (!/^[a-zA-Z0-9-]+$/.test(value)) {
68
+ return "Author can only contain letters, numbers, and hyphens";
69
+ }
70
+ return true;
71
+ }
72
+ // ─── Template Generators ───────────────────────────────────────────────────
73
+ function toDisplayName(dirName) {
74
+ return dirName
75
+ .split("-")
76
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
77
+ .join(" ");
78
+ }
79
+ function validateProjectDir(value) {
80
+ if (!value)
81
+ return "Project directory is required";
82
+ if (!/^[a-z][a-z0-9-]*$/.test(value)) {
83
+ return "Must be lowercase, start with a letter, and use only letters, numbers, and hyphens";
84
+ }
85
+ return true;
72
86
  }
73
- function renderTemplate(content, config) {
74
- const authorBlock = config.authorUrl ? `author: ${config.author}
75
- author_url: ${config.authorUrl}` : `author: ${config.author}`;
76
- return content.replace(/<%=\s*name\s*%>/g, config.name).replace(/<%=\s*version\s*%>/g, config.version).replace(/<%=\s*displayName\s*%>/g, config.displayName).replace(/<%=\s*description\s*%>/g, config.description).replace(/<%-\s*authorBlock\s*%>/g, authorBlock).replace(/<%=\s*category\s*%>/g, config.category).replace(/<%=\s*author\s*%>/g, config.author);
77
- }
78
- function copyTemplate(templateDir, targetDir, config) {
79
- const entries = fs.readdirSync(templateDir, { withFileTypes: true });
80
- for (const entry of entries) {
81
- const srcPath = path.join(templateDir, entry.name);
82
- let targetName = entry.name;
83
- if (targetName.endsWith(".ejs")) {
84
- targetName = targetName.slice(0, -4);
87
+ function generateManifest(options) {
88
+ let manifest = `version: 1.0.0
89
+ name: ${options.name}
90
+ description: ${options.description}
91
+ author: ${options.author}
92
+ `;
93
+ if (options.authorUrl) {
94
+ manifest += `author_url: ${options.authorUrl}\n`;
85
95
  }
86
- const targetPath = path.join(targetDir, targetName);
87
- if (entry.isDirectory()) {
88
- fs.mkdirSync(targetPath, { recursive: true });
89
- copyTemplate(srcPath, targetPath, config);
90
- } else {
91
- let content = fs.readFileSync(srcPath, "utf-8");
92
- content = renderTemplate(content, config);
93
- fs.writeFileSync(targetPath, content);
96
+ manifest += `category: ${options.category}
97
+ tags: []
98
+
99
+ # Optional: Add configuration fields
100
+ # config:
101
+ # - key: API_TOKEN
102
+ # name: API Token
103
+ # type: secret # string, number, boolean, secret, select, or list
104
+ # required: true
105
+ # description: Your API token
106
+ # - key: SORT
107
+ # name: Sort Order
108
+ # type: select # use select with options for fixed value sets
109
+ # required: false
110
+ # default: hot
111
+ # options:
112
+ # - hot
113
+ # - new
114
+ # - top
115
+ `;
116
+ return manifest;
117
+ }
118
+ function generatePackageJson(projectDir) {
119
+ return (JSON.stringify({
120
+ name: projectDir,
121
+ version: "1.0.0",
122
+ private: true,
123
+ type: "module",
124
+ scripts: {
125
+ build: "npx tsx scripts/build.ts",
126
+ test: "npx tsx scripts/test.ts",
127
+ },
128
+ devDependencies: {
129
+ "@types/archiver": "^7.0.0",
130
+ "@types/node": "^22.0.0",
131
+ archiver: "^7.0.1",
132
+ esbuild: "^0.27.2",
133
+ tsx: "^4.7.0",
134
+ typescript: "^5.3.3",
135
+ yaml: "^2.3.4",
136
+ },
137
+ }, null, 2) + "\n");
138
+ }
139
+ function generateIndexTs() {
140
+ return `import type { GlancewayAPI, SourceMethods } from "./types";
141
+
142
+ export default async (api: GlancewayAPI): Promise<SourceMethods> => {
143
+ async function fetchData() {
144
+ // Example: Fetch data from an API
145
+ const response = await api.fetch("https://api.example.com/items");
146
+
147
+ if (response.ok && response.json) {
148
+ const items = (response.json as any[]).map((item: any) => ({
149
+ id: item.id,
150
+ title: item.title,
151
+ url: item.url,
152
+ }));
153
+ api.emit(items);
94
154
  }
95
155
  }
156
+
157
+ // Start phase: initial fetch
158
+ await fetchData();
159
+
160
+ return {
161
+ refresh: fetchData,
162
+ };
163
+ };
164
+ `;
96
165
  }
97
- function removeDir(dir) {
98
- if (fs.existsSync(dir)) {
99
- fs.rmSync(dir, { recursive: true, force: true });
166
+ function generateClaudeMd(options) {
167
+ return `# CLAUDE.md
168
+
169
+ ## Project Overview
170
+
171
+ Glanceway source: "${options.name}" by ${options.author}. This is a standalone JavaScript source for [Glanceway](https://glanceway.app), a macOS menu bar app that displays information items.
172
+
173
+ ## Commands
174
+
175
+ \`\`\`bash
176
+ npm install # Install dependencies
177
+ npm run build # Build source into dist/ (compile + package)
178
+ npm run test # Test source (mock API execution + validation)
179
+ \`\`\`
180
+
181
+ Provide config values for testing:
182
+ \`\`\`bash
183
+ npm run test -- --config API_TOKEN=xxx --config USERNAME=yyy
184
+ \`\`\`
185
+
186
+ There is no test framework. Build the source to verify it compiles. There is no linter or formatter configured.
187
+
188
+ ## Project Structure
189
+
190
+ - \`src/index.ts\` — Source implementation (main logic)
191
+ - \`src/types.ts\` — GlancewayAPI type definitions (do not modify)
192
+ - \`manifest.yaml\` — Source metadata and config schema
193
+ - \`scripts/build.ts\` — Build script (esbuild compile + package)
194
+ - \`scripts/test.ts\` — Test script (mock API + validation)
195
+
196
+ ## Source Development Constraints
197
+
198
+ **NO external imports.** Sources cannot use \`import\` or \`require\` for external packages. The only allowed import is the type import:
199
+
200
+ \`\`\`typescript
201
+ import type { GlancewayAPI, SourceMethods } from "./types";
202
+ \`\`\`
203
+
204
+ All functionality is provided through the \`api\` parameter. Use \`export default\` for the default export. Use \`GlancewayAPI<Config>\` generic when config fields are defined:
205
+
206
+ \`\`\`typescript
207
+ export default async (api: GlancewayAPI<Config>): Promise<SourceMethods> => {
208
+ async function fetchData() {
209
+ /* fetch, transform, emit */
100
210
  }
211
+
212
+ // Start phase: initial fetch
213
+ await fetchData();
214
+
215
+ return {
216
+ refresh: fetchData,
217
+ stop() {
218
+ /* optional cleanup */
219
+ },
220
+ };
221
+ };
222
+ \`\`\`
223
+
224
+ ## API Reference
225
+
226
+ All methods are available on the \`api: GlancewayAPI\` parameter.
227
+
228
+ ### api.emit(items: InfoItem[])
229
+
230
+ Send items to Glanceway for display.
231
+
232
+ \`\`\`typescript
233
+ interface InfoItem {
234
+ id: string; // Unique identifier
235
+ title: string; // Main display text
236
+ subtitle?: string; // Secondary text below title
237
+ url?: string; // Link opened on click
238
+ timestamp?: Date | string | number; // ISO string, Unix timestamp, or Date
101
239
  }
102
- async function main() {
103
- const args = process.argv.slice(2);
104
- if (args[0] === "--help" || args[0] === "-h") {
105
- console.log(`
106
- Usage: create-glanceway-source [project-name]
240
+ \`\`\`
107
241
 
108
- Creates a new Glanceway source project with TypeScript support.
242
+ ### api.fetch<T>(url: string, options?: FetchOptions): Promise<FetchResponse<T>>
243
+
244
+ Make HTTP requests. Supports generics for typed JSON responses.
245
+
246
+ \`\`\`typescript
247
+ interface FetchOptions {
248
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; // default: GET
249
+ headers?: Record<string, string>;
250
+ body?: string;
251
+ timeout?: number; // milliseconds, default: 30000
252
+ }
253
+
254
+ interface FetchResponse<T> {
255
+ ok: boolean; // true if status 200-299
256
+ status: number;
257
+ headers: Record<string, string>;
258
+ text: string; // raw response body
259
+ json?: T; // parsed JSON (if valid)
260
+ }
261
+ \`\`\`
109
262
 
110
263
  Example:
111
- npm create glanceway-source
112
- npm create glanceway-source my-source
113
- `);
114
- process.exit(0);
115
- }
116
- console.log("\n\u{1F4E6} Creating a new Glanceway source\n");
117
- let projectName = args[0];
118
- if (!projectName) {
119
- projectName = await prompt("Project name", "my-source");
264
+
265
+ \`\`\`typescript
266
+ const response = await api.fetch<{
267
+ items: Array<{ id: string; name: string }>;
268
+ }>("https://api.example.com/data", {
269
+ headers: { Authorization: \\\`Bearer \\\${token}\\\` },
270
+ });
271
+ if (response.ok && response.json) {
272
+ // response.json is typed
273
+ }
274
+ \`\`\`
275
+
276
+ ### api.config.get(key: string): unknown
277
+
278
+ Get a user-configured value by key (defined in \`manifest.yaml\` config section). Returns \`string\` for most types, \`string[]\` for \`list\` type.
279
+
280
+ ### api.config.getAll(): Record<string, unknown>
281
+
282
+ Get all user-configured values as a key-value map.
283
+
284
+ ### api.storage.get(key: string): string | undefined
285
+
286
+ Get a persisted value. Data survives between refreshes and app restarts.
287
+
288
+ ### api.storage.set(key: string, value: string): void
289
+
290
+ Store a value persistently.
291
+
292
+ ### api.log(level, message)
293
+
294
+ Log messages for debugging. Levels: \`"info"\`, \`"error"\`, \`"warn"\`, \`"debug"\`.
295
+
296
+ ### api.appVersion
297
+
298
+ Current Glanceway app version string (e.g., \`"1.2.0"\`).
299
+
300
+ ### api.websocket.connect(url, callbacks): Promise<WebSocketConnection>
301
+
302
+ Create a WebSocket connection for real-time data.
303
+
304
+ \`\`\`typescript
305
+ interface WebSocketCallbacks {
306
+ onConnect?: (ws: WebSocketConnection) => void;
307
+ onMessage?: (data: string) => void;
308
+ onError?: (error: string) => void;
309
+ onClose?: (code: number) => void;
310
+ }
311
+
312
+ interface WebSocketConnection {
313
+ send(message: string): Promise<void>;
314
+ close(): void;
315
+ }
316
+ \`\`\`
317
+
318
+ ## manifest.yaml Full Schema
319
+
320
+ \`\`\`yaml
321
+ version: 1.0.0 # Required: semantic version
322
+ name: Display Name # Required: shown in Glanceway
323
+ description: Brief desc # Required
324
+ author: authorname # Required
325
+ author_url: https://... # Optional
326
+ category: Developer # Required: Developer | News | Social | Finance | Entertainment | Productivity | Other
327
+ tags: # Optional
328
+ - tag1
329
+ min_app_version: 1.2.0 # Optional: minimum Glanceway app version required
330
+ config: # Optional: user-configurable values
331
+ - key: API_TOKEN
332
+ name: API Token
333
+ type: secret # string, number, boolean, secret, select, or list
334
+ required: true
335
+ description: Description shown to user
336
+ - key: TAGS
337
+ name: Tags
338
+ type: list # list for string arrays (multiple values)
339
+ required: false
340
+ description: Tags to filter by
341
+ - key: SORT
342
+ name: Sort Order
343
+ type: select # select requires options list
344
+ required: false
345
+ default: hot
346
+ options:
347
+ - hot
348
+ - new
349
+ - top
350
+ \`\`\`
351
+
352
+ ## Source Lifecycle
353
+
354
+ JavaScript sources have two distinct phases:
355
+
356
+ 1. **Start phase**: When the source is first loaded, the default export function (outer closure) runs. The app does **NOT** call \`refresh()\` at this point. Sources should perform their initial data fetch here by \`await\`ing their fetch function before returning.
357
+ 2. **Refresh phase**: On each scheduled refresh interval, the app calls \`refresh()\`. This is the only time \`refresh()\` is invoked.
358
+
359
+ ### Standard Pattern
360
+
361
+ Extract the fetch logic into a named async function, call it in the outer closure for the start phase, and assign it as the \`refresh\` method:
362
+
363
+ \`\`\`typescript
364
+ export default async (api: GlancewayAPI<Config>): Promise<SourceMethods> => {
365
+ const token = api.config.get("API_TOKEN");
366
+
367
+ async function fetchData() {
368
+ const res = await api.fetch<Item[]>(url);
369
+ if (!res.ok || !res.json) {
370
+ throw new Error(\\\`Failed to fetch (HTTP \\\${res.status})\\\`);
371
+ }
372
+ api.emit(toItems(res.json));
120
373
  }
121
- if (!projectName) {
122
- console.error("\u274C Error: Project name is required.");
123
- process.exit(1);
374
+
375
+ // Start phase: initial fetch
376
+ await fetchData();
377
+
378
+ return {
379
+ refresh: fetchData,
380
+ };
381
+ };
382
+ \`\`\`
383
+
384
+ ## Source Design Guidelines
385
+
386
+ - Always make full use of the \`subtitle\` field. If the API response contains summary, description, brief, or any descriptive text, map it to \`subtitle\` so users get maximum information at a glance.
387
+ - **Maximize items per fetch.** The app does not paginate, so each fetch should retrieve as many items as the API allows without hurting performance. The hard upper limit is **500 items** — never exceed this.
388
+
389
+ ## Source Code Conventions
390
+
391
+ ### File Structure Order
392
+
393
+ \`\`\`typescript
394
+ // 1. Type import (always first)
395
+ import type { GlancewayAPI, SourceMethods } from "./types";
396
+
397
+ // 2. Type definitions (config, response types, data models)
398
+ type Config = {
399
+ API_TOKEN: string;
400
+ TAGS: string[];
401
+ };
402
+
403
+ // 3. Helper functions (pure utilities, no api dependency)
404
+ function stripHtml(html: string): string {
405
+ return html.replace(/<[^>]*>/g, "");
406
+ }
407
+
408
+ // 4. Default export (async, with Config generic)
409
+ export default async (api: GlancewayAPI<Config>): Promise<SourceMethods> => {
410
+ // 5. Config reading (in outer closure; script reloads on config change)
411
+ const token = api.config.get("API_TOKEN");
412
+
413
+ // 6. Fetch function (fetch, transform, emit)
414
+ async function fetchData() {
415
+ // ...
124
416
  }
125
- const targetDir = path.resolve(process.cwd(), projectName);
126
- if (fs.existsSync(targetDir)) {
127
- const overwrite = await confirm(`Directory "${projectName}" already exists. Overwrite?`);
128
- if (!overwrite) {
129
- console.log("Cancelled.");
130
- process.exit(0);
417
+
418
+ // 7. Start phase: initial fetch (awaited before returning)
419
+ await fetchData();
420
+
421
+ return {
422
+ refresh: fetchData,
423
+ };
424
+ };
425
+ \`\`\`
426
+
427
+ ### Config Typing
428
+
429
+ Use \`GlancewayAPI<Config>\` generic to define config field types. This gives \`api.config.get()\` type-safe keys and return values.
430
+
431
+ ### Config Reading
432
+
433
+ Read config **in the outer closure** (before \`return\`), not inside the fetch function. When config changes, Glanceway reloads the entire script, so the outer closure always has fresh values.
434
+
435
+ ### Response Type Annotations
436
+
437
+ Use \`api.fetch<T>()\` generics to type responses. For simple/one-off types, inline them at the call site. For complex or reused types, define named types at the top of the file.
438
+
439
+ ### Error Handling
440
+
441
+ Check \`res.ok && res.json\` before using response data. For the main/only request, throw on failure. For parallel sub-requests, skip failures silently.
442
+
443
+ ### Parallel Requests
444
+
445
+ Always use \`Promise.allSettled\` (never \`Promise.all\`) for parallel requests. Skip failed results instead of throwing.
446
+
447
+ ### Helper Functions
448
+
449
+ Define reusable mapping functions (e.g., \`toItems\`) **inside the fetch function** when they use closure variables. Define pure utility functions (e.g., \`stripHtml\`) **at the module top** before the export.
450
+
451
+ ## Importing into Glanceway
452
+
453
+ After building (\`npm run build\`), import into Glanceway:
454
+ 1. Open Glanceway
455
+ 2. Go to Sources
456
+ 3. Click "Import from file"
457
+ 4. Select \`dist/latest.gwsrc\`
458
+
459
+ ## Submitting to glanceway-sources
460
+
461
+ To share your source with the community, [open an issue](https://github.com/glanceway/glanceway-sources/issues) on glanceway-sources with a link to your project repository.
462
+ `;
463
+ }
464
+ // ─── File Operations ───────────────────────────────────────────────────────
465
+ function copyTemplateFile(templateName, destPath) {
466
+ // Templates are relative to the CLI dist directory
467
+ const templateDir = path.join(__dirname, "..", "templates");
468
+ const srcPath = path.join(templateDir, templateName);
469
+ fs.copyFileSync(srcPath, destPath);
470
+ }
471
+ // ─── Main ──────────────────────────────────────────────────────────────────
472
+ async function main() {
473
+ const args = parseArgs();
474
+ console.log("\nCreate a new Glanceway source\n");
475
+ // 1. Project directory
476
+ let projectDir = args.projectDir;
477
+ if (!projectDir) {
478
+ const response = await prompts({
479
+ type: "text",
480
+ name: "projectDir",
481
+ message: "Project directory:",
482
+ validate: (value) => validateProjectDir(value),
483
+ });
484
+ if (!response.projectDir) {
485
+ console.log("\nCancelled");
486
+ process.exit(1);
487
+ }
488
+ projectDir = response.projectDir;
131
489
  }
132
- removeDir(targetDir);
133
- }
134
- const config = await getConfig(projectName);
135
- console.log("\n\u{1F680} Creating project...\n");
136
- fs.mkdirSync(targetDir, { recursive: true });
137
- const templateDir = path.join(__dirname, "..", "template");
138
- if (!fs.existsSync(templateDir)) {
139
- console.error("\u274C Error: Template directory not found.");
490
+ else {
491
+ const validation = validateProjectDir(projectDir);
492
+ if (validation !== true) {
493
+ console.error(`Error: ${validation}`);
494
+ process.exit(1);
495
+ }
496
+ }
497
+ const targetDir = path.resolve(process.cwd(), projectDir);
498
+ if (fs.existsSync(targetDir)) {
499
+ console.error(`Error: Directory "${projectDir}" already exists.`);
500
+ process.exit(1);
501
+ }
502
+ // 2. Source metadata
503
+ const hasAllFlags = args.name && args.description && args.author && args.category;
504
+ let name;
505
+ let description;
506
+ let author;
507
+ let authorUrl;
508
+ let category;
509
+ if (hasAllFlags) {
510
+ name = args.name;
511
+ description = args.description;
512
+ author = args.author;
513
+ authorUrl = args.authorUrl;
514
+ category = args.category;
515
+ const authorCheck = validateAuthor(author);
516
+ if (authorCheck !== true) {
517
+ console.error(`Error: ${authorCheck}`);
518
+ process.exit(1);
519
+ }
520
+ if (!CATEGORIES.includes(category)) {
521
+ console.error(`Error: Category must be one of: ${CATEGORIES.join(", ")}`);
522
+ process.exit(1);
523
+ }
524
+ }
525
+ else {
526
+ const response = await prompts([
527
+ {
528
+ type: "text",
529
+ name: "name",
530
+ message: "Display name:",
531
+ initial: args.name || toDisplayName(projectDir),
532
+ },
533
+ {
534
+ type: "text",
535
+ name: "description",
536
+ message: "Description:",
537
+ initial: args.description,
538
+ validate: (value) => (value ? true : "Description is required"),
539
+ },
540
+ {
541
+ type: "text",
542
+ name: "author",
543
+ message: "Author:",
544
+ initial: args.author,
545
+ validate: (value) => validateAuthor(value),
546
+ },
547
+ {
548
+ type: "text",
549
+ name: "authorUrl",
550
+ message: "Author URL (optional):",
551
+ initial: args.authorUrl,
552
+ },
553
+ {
554
+ type: "select",
555
+ name: "category",
556
+ message: "Category:",
557
+ choices: CATEGORIES.map((c) => ({ title: c, value: c })),
558
+ initial: args.category ? CATEGORIES.indexOf(args.category) : 0,
559
+ },
560
+ ]);
561
+ if (!response.name || !response.description || !response.author) {
562
+ console.log("\nCancelled");
563
+ process.exit(1);
564
+ }
565
+ name = response.name;
566
+ description = response.description;
567
+ author = response.author;
568
+ authorUrl = response.authorUrl || undefined;
569
+ category = response.category;
570
+ }
571
+ // 3. Generate project
572
+ console.log(`\nScaffolding project in ${projectDir}/...\n`);
573
+ // Create directories
574
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
575
+ fs.mkdirSync(path.join(targetDir, "scripts"), { recursive: true });
576
+ // Copy static template files
577
+ copyTemplateFile("src/types.ts", path.join(targetDir, "src", "types.ts"));
578
+ copyTemplateFile("scripts/build.ts", path.join(targetDir, "scripts", "build.ts"));
579
+ copyTemplateFile("scripts/test.ts", path.join(targetDir, "scripts", "test.ts"));
580
+ copyTemplateFile("tsconfig.json", path.join(targetDir, "tsconfig.json"));
581
+ copyTemplateFile("gitignore", path.join(targetDir, ".gitignore"));
582
+ // Generate dynamic files
583
+ fs.writeFileSync(path.join(targetDir, "manifest.yaml"), generateManifest({ name, description, author, authorUrl, category }));
584
+ fs.writeFileSync(path.join(targetDir, "package.json"), generatePackageJson(projectDir));
585
+ fs.writeFileSync(path.join(targetDir, "src", "index.ts"), generateIndexTs());
586
+ fs.writeFileSync(path.join(targetDir, "CLAUDE.md"), generateClaudeMd({ projectDir: projectDir, name, author }));
587
+ // 4. Print next steps
588
+ console.log(`Done! Created ${projectDir}/\n`);
589
+ console.log("Next steps:\n");
590
+ console.log(` cd ${projectDir}`);
591
+ console.log(" npm install");
592
+ console.log(" # Edit src/index.ts to implement your source");
593
+ console.log(" npm run build");
594
+ console.log(" npm run test");
595
+ console.log("");
596
+ }
597
+ main().catch((err) => {
598
+ console.error(err);
140
599
  process.exit(1);
141
- }
142
- copyTemplate(templateDir, targetDir, config);
143
- console.log(`\u2705 Created project "${projectName}"
144
- `);
145
- console.log("Next steps:");
146
- console.log(` cd ${projectName}`);
147
- console.log(" npm install");
148
- console.log(" npm run dev # Watch mode");
149
- console.log(" npm run build # Build for production");
150
- console.log("\nOutput files:");
151
- console.log(" dist/index.js - Compiled source code");
152
- console.log(" dist/manifest.yaml - Source metadata");
153
- console.log("\nTo contribute to glanceway-sources:");
154
- console.log(" 1. Copy dist/ contents to sources/<your-username>/<source-name>/");
155
- console.log(" 2. Submit a PR to the glanceway-sources repository");
156
- console.log("");
157
- }
158
- main().catch((error) => {
159
- console.error("Error:", error);
160
- process.exit(1);
161
600
  });