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/README.md +77 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +580 -141
- package/package.json +19 -21
- package/templates/gitignore +24 -0
- package/templates/scripts/build.ts +103 -0
- package/templates/scripts/test.ts +475 -0
- package/{template → templates}/src/types.ts +24 -15
- package/{template → templates}/tsconfig.json +2 -3
- package/template/manifest.yaml.ejs +0 -6
- package/template/package.json.ejs +0 -14
- package/template/src/index.ts +0 -9
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
});
|