@geastack/cli 0.1.51 → 0.1.53
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/docs/SPEC.md +2 -2
- package/package.json +2 -3
- package/src/apps/app-index-writer.mjs +15 -0
- package/src/apps/apple-icons.mjs +147 -0
- package/src/apps/bundle-writer.mjs +156 -0
- package/src/apps/launcher-catalog.mjs +114 -0
- package/src/apps/openai-icons.mjs +356 -0
- package/src/apps/zip-writer.mjs +104 -0
- package/src/ble/ble-ota.swift +291 -0
- package/src/boards/config.mjs +32 -0
- package/src/boards/custom-target.mjs +309 -0
- package/src/boards/resolve.mjs +143 -0
- package/src/boards/targets.mjs +45 -0
- package/src/boards/usb.mjs +211 -0
- package/src/commands/apps.mjs +236 -0
- package/src/commands/board.mjs +402 -0
- package/src/commands/doctor.mjs +91 -0
- package/src/context.mjs +45 -52
- package/src/device/device.mjs +96 -0
- package/src/device/image.mjs +115 -0
- package/src/device/serial.mjs +430 -0
- package/src/device/wifi.mjs +190 -0
- package/src/esp32/build.mjs +321 -0
- package/src/esp32/capabilities.mjs +54 -0
- package/src/esp32/flash.mjs +181 -0
- package/src/esp32/idf-env.mjs +171 -0
- package/src/esp32/ota.mjs +80 -0
- package/src/esp32/partitions.mjs +59 -0
- package/src/esp32/sdkconfig.mjs +103 -0
- package/src/esp32/wifi-config.mjs +72 -0
- package/src/gea.mjs +86 -427
- package/src/geaos/adapter.mjs +119 -0
- package/src/heap-report.mjs +1276 -0
- package/src/manifest.mjs +135 -36
- package/src/rp2350/adapter.mjs +155 -0
- package/src/setup-wizard.mjs +6 -10
- package/src/taurus/adapter.mjs +52 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
export const ICON_SIZES = [32, 64, 128, 256, 512]
|
|
6
|
+
|
|
7
|
+
export const ICON_STYLE_SYSTEM_PROMPT = [
|
|
8
|
+
'GeaOS app icons must preserve a clean retro-synthwave identity: night-mode arcade OS energy, dark futuristic depth, neon cyan, magenta, violet, hot pink, electric blue, and amber accents.',
|
|
9
|
+
'The style is clean synthwave emblem design, not flat corporate glyphs and not messy AI neon art. Use a bold vector-like central symbol with restrained neon rim light, subtle glow, glossy glass or chrome accents only where they clarify the shape.',
|
|
10
|
+
'Each icon uses one large central glyph occupying about 75-85% of the canvas. The glyph must read as a single silhouette at 32x32 pixels, with at most one or two tiny synthwave accents such as a horizon line, small grid tick, glow edge, or motion streak.',
|
|
11
|
+
'Use hard 90-degree square canvas corners and fill the whole source image edge to edge. The background may be a simple dark gradient or two-tone synthwave field, but should not become a scene.',
|
|
12
|
+
'Use only a small palette per icon: one dark synthwave background, one high-contrast foreground color, and one or two neon accents. No beige corporate palette, no default teal set, no pastel mobile icon look, and no monochrome white pictogram on black.',
|
|
13
|
+
'Diversify the set with different dark background hues, neon accent colors, and symbol silhouettes. Adjacent icons should not share the same dominant color or silhouette family.',
|
|
14
|
+
'Proper app icon composition only: one simple symbol or one symbol plus one tiny accent. No poster scenes, no literal landscapes, no many-object compositions, no decorative clutter, no particles, no starfields, no cityscapes.',
|
|
15
|
+
'Do not put the icon inside an inner rounded square, squircle, app mask, tile, badge, device frame, or inset plate. The full canvas itself is the square icon.',
|
|
16
|
+
'No readable text anywhere: no words, app names, captions, dates, clock times, labels, digits, keypad numbers, UI numerals, or letter badges. Use ticks, dots, bars, abstract glyph shapes, and iconography instead.',
|
|
17
|
+
'Use a clean centered subject with strong silhouette, no UI chrome, no device mockup, and enough contrast to read clearly at 32x32 pixels.'
|
|
18
|
+
].join(' ')
|
|
19
|
+
|
|
20
|
+
const APP_ICON_PALETTES = {
|
|
21
|
+
'analog-clock': 'midnight teal to black background, ivory clock glyph, electric cyan and amber rim accents',
|
|
22
|
+
'app-launcher': 'deep indigo background, neon magenta and cyan app dots, amber center glow',
|
|
23
|
+
'bouncing-balls': 'electric blue background, hot pink and amber balls, cyan motion arc',
|
|
24
|
+
'bouncing-balls-jsx': 'violet background, cyan and coral balls, amber highlight',
|
|
25
|
+
'button-tetris': 'ultramarine background, cyan magenta and amber blocks, hot pink button',
|
|
26
|
+
'camera-showcase': 'black to jade background, ivory lens glyph, cyan aperture glow, coral capture accent',
|
|
27
|
+
'canvas-3d': 'black background, cyan wireframe cube, magenta and amber axis ticks',
|
|
28
|
+
'counter-jsx': 'deep purple background, ivory control glyphs, cyan magenta amber meter accents',
|
|
29
|
+
'css-3d-cube': 'cobalt to black background, cyan cube, magenta face, amber transform arc',
|
|
30
|
+
'css-animation-showcase': 'black to plum background, cyan keyframe squares, hot pink motion curve',
|
|
31
|
+
'dialer': 'deep indigo background, cyan keypad dots, magenta handset curve, amber call accent',
|
|
32
|
+
'direct-psram-balls': 'black to olive background, cyan memory chip, magenta amber data balls',
|
|
33
|
+
'gea-companion': 'black to coral background, cyan linked companion nodes, violet edge accent',
|
|
34
|
+
'hid-clicker': 'black to violet background, ivory remote glyph, cyan buttons, amber signal chevron',
|
|
35
|
+
'image-jsx': 'midnight teal background, ivory image frame, magenta mountain, amber sun',
|
|
36
|
+
'ios-device-showcase': 'ultramarine background, cyan nested surface glyphs, magenta and amber component accents',
|
|
37
|
+
'ios-metal-shader-demo': 'black to plum background, cyan shader prism, magenta facet, amber edge',
|
|
38
|
+
'ios-metal-showcase': 'cobalt background, cyan faceted metal mark, violet shadow, amber core',
|
|
39
|
+
'ios-metal-world-game': 'black to burgundy background, cyan horizon ring, magenta terrain facets, amber notch',
|
|
40
|
+
'ios-native-showcase': 'deep jade background, ivory native-control glyph, cyan toggle, hot pink accent',
|
|
41
|
+
'notes-jsx': 'plum background, ivory note glyph, cyan lines, hot pink folded corner',
|
|
42
|
+
'notes-native': 'midnight teal background, ivory document glyph, cyan lines, amber folded corner',
|
|
43
|
+
'sky-hop': 'black to violet background, ivory leap glyph, cyan edge glow, hot pink platform',
|
|
44
|
+
'sky-hop-jsx': 'black to burgundy background, ivory comet glyph, magenta trail, amber platform',
|
|
45
|
+
'static-card': 'black to plum background, cyan layout glyph, ivory bars, hot pink thumbnail block',
|
|
46
|
+
'stopwatch-jsx': 'black to orange background, ivory stopwatch glyph, cyan dial ticks, hot pink hand accent',
|
|
47
|
+
'tic-tac-toe': 'black to burgundy background, cyan board lines, hot pink and amber tokens',
|
|
48
|
+
'tilt-breakout': 'midnight teal background, cyan bricks, ivory paddle, hot pink ball accent',
|
|
49
|
+
'todo-jsx': 'black to jade background, ivory checklist marks, cyan rows, magenta accent',
|
|
50
|
+
'typography': 'plum background, ivory abstract type curves, cyan baseline, amber pen accent',
|
|
51
|
+
'virtual-list': 'black to indigo background, cyan list rows, hot pink highlighted viewport',
|
|
52
|
+
'watch': 'midnight teal background, ivory watch face, cyan ticks, amber hand accent',
|
|
53
|
+
'watch-analog': 'cobalt to black background, ivory clock face, cyan ticks, magenta hand accent',
|
|
54
|
+
'watch-date': 'black to burgundy background, ivory calendar glyph, cyan date blocks, hot pink empty date block'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const APP_ICON_DIRECTIONS = {
|
|
58
|
+
'analog-clock': [
|
|
59
|
+
'For Analog Clock, use a bold analog clock symbol: circular face, thick hands, simple tick marks.',
|
|
60
|
+
'Do not show readable numbers, text, or a flat clock glyph.'
|
|
61
|
+
],
|
|
62
|
+
'app-launcher': [
|
|
63
|
+
'For App Launcher, use a simple launcher hub symbol: five to seven app dots arranged around a central dot.',
|
|
64
|
+
'Do not show a phone home screen, orbital scene, or many tiny icons.'
|
|
65
|
+
],
|
|
66
|
+
'bouncing-balls': [
|
|
67
|
+
'For Balls Canvas, use three bold balls and one simple bounce arc.',
|
|
68
|
+
'Do not show flat colored dots inside a black rounded tile.'
|
|
69
|
+
],
|
|
70
|
+
'bouncing-balls-jsx': [
|
|
71
|
+
'For Balls JSX, use a simple cluster of three balls in varied sizes.',
|
|
72
|
+
'Do not show flat emoji-like circles, a simple molecule, or a black rounded tile.'
|
|
73
|
+
],
|
|
74
|
+
'button-tetris': [
|
|
75
|
+
'For Tetris, use three tetromino blocks and one small arcade button accent.',
|
|
76
|
+
'Do not show a flat Tetris board inside a rounded black app tile.'
|
|
77
|
+
],
|
|
78
|
+
'camera-showcase': [
|
|
79
|
+
'For Camera, use a bold camera lens symbol with one simple capture beam or aperture mark.',
|
|
80
|
+
'Do not show a flat white camera pictogram or rounded black camera icon.'
|
|
81
|
+
],
|
|
82
|
+
'canvas-3d': [
|
|
83
|
+
'For Canvas 3D, use one clean wireframe cube or mesh diamond with three short axis ticks.',
|
|
84
|
+
'Do not show a simple flat white cube outline on black.'
|
|
85
|
+
],
|
|
86
|
+
'counter-jsx': [
|
|
87
|
+
'For Counter, use a compact counter badge with plus/minus buttons and three abstract indicator bars.',
|
|
88
|
+
'Do not show readable digits, tally marks, hash marks, a calculator, or a flat white glyph.'
|
|
89
|
+
],
|
|
90
|
+
'css-3d-cube': [
|
|
91
|
+
'For CSS 3D Cube, use one solid isometric cube with simple face shading and a short transform arc.',
|
|
92
|
+
'Do not show a plain flat cube pictogram or a generic box logo.'
|
|
93
|
+
],
|
|
94
|
+
'css-animation-showcase': [
|
|
95
|
+
'For CSS Animations, use three keyframe squares connected by one smooth motion curve.',
|
|
96
|
+
'Do not show a static flat slider, plain CSS badge, or rounded black tile.'
|
|
97
|
+
],
|
|
98
|
+
'dialer': [
|
|
99
|
+
'For Dialer, use a simple keypad matrix of unlabeled dots with a small handset curve.',
|
|
100
|
+
'Do not show readable keypad numbers, a flat handset glyph, white keypad dots, or old phone clipart.'
|
|
101
|
+
],
|
|
102
|
+
'direct-psram-balls': [
|
|
103
|
+
'For Direct PSRAM Balls, use one memory chip and three small balls moving through it.',
|
|
104
|
+
'Do not show generic computer hardware without the animated-ball/performance idea.'
|
|
105
|
+
],
|
|
106
|
+
'hid-clicker': [
|
|
107
|
+
'For HID Clicker, use a compact presentation remote with two buttons and one small signal chevron.',
|
|
108
|
+
'Use plain round or triangular control buttons with no play icon, no app video symbol, and no screen UI.',
|
|
109
|
+
'Do not show a human hand, finger press, tap target, Wi-Fi logo, accessibility tap icon, generic touch gesture, or media player remote.'
|
|
110
|
+
],
|
|
111
|
+
'image-jsx': [
|
|
112
|
+
'For Image, use a simple image-frame symbol with mountain and sun shapes.',
|
|
113
|
+
'Do not show a flat green mountain photo icon or a rounded black image tile.'
|
|
114
|
+
],
|
|
115
|
+
'ios-device-showcase': [
|
|
116
|
+
'For iOS Device Showcase, use nested flat surface panels and component blocks.',
|
|
117
|
+
'Do not show a phone outline, tablet outline, device mockup, home button, notch, screen UI, or rounded inner app tile.'
|
|
118
|
+
],
|
|
119
|
+
'ios-metal-shader-demo': [
|
|
120
|
+
'For iOS Metal Shader Demo, use one faceted shader prism or angular lightning shard.',
|
|
121
|
+
'Do not show a rounded square tile, app mask, letter mark, device, gradient mesh, or detailed scene.'
|
|
122
|
+
],
|
|
123
|
+
'ios-metal-showcase': [
|
|
124
|
+
'For iOS Metal Showcase, use one faceted metal prism, diamond, or triangular core mark.',
|
|
125
|
+
'Do not show a rounded square tile, app mask, letter mark, device, gradient mesh, or detailed scene.'
|
|
126
|
+
],
|
|
127
|
+
'ios-metal-world-game': [
|
|
128
|
+
'For iOS Metal World Game, use a simplified abstract world mark: horizon arc, compass notch, or terrain facets inside a bold ring.',
|
|
129
|
+
'Do not show a helmet, character, gear, weapon, readable logo, map scene, or rounded inner app tile.'
|
|
130
|
+
],
|
|
131
|
+
'ios-native-showcase': [
|
|
132
|
+
'For iOS Native Showcase, use a clean native-controls symbol: one toggle, one segmented bar, and one simple control dot.',
|
|
133
|
+
'Do not show a bird, animal, mascot, phone mockup, app window, rounded inner app tile, or letter mark.'
|
|
134
|
+
],
|
|
135
|
+
'notes-jsx': [
|
|
136
|
+
'For Notes JSX, use one note page with two or three thick content lines and a folded corner.',
|
|
137
|
+
'Do not show readable writing, app UI chrome, a notebook spiral, or a rounded inner app tile.'
|
|
138
|
+
],
|
|
139
|
+
'notes-native': [
|
|
140
|
+
'For Notes Native, use one document page with two or three thick content lines and a folded corner.',
|
|
141
|
+
'Do not show readable writing, app UI chrome, a notebook spiral, or a rounded inner app tile.'
|
|
142
|
+
],
|
|
143
|
+
'sky-hop': [
|
|
144
|
+
'For Sky Hop, use a simple leaping comet or wing shape above one platform.',
|
|
145
|
+
'Do not show a blue daytime sky, white clouds, green grass, cute cartoon mascot, black stick-figure silhouette, side-scroller mobile game tile, or rounded sky-blue app icon.'
|
|
146
|
+
],
|
|
147
|
+
'sky-hop-jsx': [
|
|
148
|
+
'For Sky Hop JSX, use a simple leaping comet or wing shape above one platform.',
|
|
149
|
+
'Do not show a blue daytime sky, white clouds, green grass, cute cartoon mascot, black stick-figure silhouette, side-scroller mobile game tile, or rounded sky-blue app icon.'
|
|
150
|
+
],
|
|
151
|
+
'static-card': [
|
|
152
|
+
'For Static Card, use a single UI content-card symbol: one header bar, two body bars, one thumbnail block.',
|
|
153
|
+
'This is a software layout card, not a playing card.',
|
|
154
|
+
'Do not show playing cards, card suits, spades, hearts, clubs, diamonds, tarot cards, deck cards, or casino imagery.'
|
|
155
|
+
],
|
|
156
|
+
'stopwatch-jsx': [
|
|
157
|
+
'For Stopwatch, use a bold stopwatch face with tick marks and two hands.',
|
|
158
|
+
'Do not show readable numbers, text, or a flat white stopwatch pictogram on black.'
|
|
159
|
+
],
|
|
160
|
+
'tilt-breakout': [
|
|
161
|
+
'For Breakout, use one tilted paddle, one ball, and four or five simple bricks.',
|
|
162
|
+
'Do not show a flat brick wall and paddle inside a rounded black tile.'
|
|
163
|
+
],
|
|
164
|
+
'tic-tac-toe': [
|
|
165
|
+
'For Tic Tac Toe, use a bold 3x3 board with abstract cross and ring tokens.',
|
|
166
|
+
'Do not show a flat textbook grid, plain red/blue marks on black, or a simple classroom game diagram.'
|
|
167
|
+
],
|
|
168
|
+
'todo-jsx': [
|
|
169
|
+
'For Todos, use a checklist symbol with three check rows.',
|
|
170
|
+
'Do not show a flat blue clipboard app icon or generic checklist emoji.'
|
|
171
|
+
],
|
|
172
|
+
'typography': [
|
|
173
|
+
'For Typography, use abstract font curves, baseline ticks, and a pen-nib shape.',
|
|
174
|
+
'Letterlike shapes are allowed here, but do not render readable words, a single flat T icon, or plain white typography on black.'
|
|
175
|
+
],
|
|
176
|
+
'virtual-list': [
|
|
177
|
+
'For Virtual List, use a stack of list rows with one highlighted viewport row.',
|
|
178
|
+
'Do not show flat up/down arrows, accessibility glyphs, or a generic menu icon.'
|
|
179
|
+
],
|
|
180
|
+
'watch': [
|
|
181
|
+
'For Watch, use a bold watch-face symbol with ticks and hands, no full watch device.',
|
|
182
|
+
'Do not show readable clock time, dates, numbers, text, or a real device mockup.'
|
|
183
|
+
],
|
|
184
|
+
'watch-analog': [
|
|
185
|
+
'For Analog Watch, use an analog watch-face symbol with ticks and hands, no full watch device.',
|
|
186
|
+
'Do not show readable numbers, text, or a real device mockup.'
|
|
187
|
+
],
|
|
188
|
+
'watch-date': [
|
|
189
|
+
'For Date, use a calendar/date symbol with abstract blocks, ticks, and one small empty window.',
|
|
190
|
+
'Do not show readable dates, words, clock time, numbers, or text.'
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function appSpecificIconDirection(app) {
|
|
195
|
+
return (APP_ICON_DIRECTIONS[app.id] || []).join(' ')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function iconPrompt(app) {
|
|
199
|
+
const description = app.launcher.description ? ` Purpose: ${app.launcher.description}.` : ''
|
|
200
|
+
const appSpecific = appSpecificIconDirection(app)
|
|
201
|
+
const palette = APP_ICON_PALETTES[app.id]
|
|
202
|
+
return [
|
|
203
|
+
ICON_STYLE_SYSTEM_PROMPT,
|
|
204
|
+
`Create square launcher source art for a tiny wearable operating system app named "${app.name}".`,
|
|
205
|
+
description,
|
|
206
|
+
palette ? `Assigned synthwave palette for this exact app: ${palette}. Use these colors as the dominant palette; do not default to teal unless teal is explicitly named here.` : '',
|
|
207
|
+
appSpecific,
|
|
208
|
+
'Make the result a proper clean synthwave app icon: simple, bold, centered, iconic, luminous, and readable at tiny sizes.'
|
|
209
|
+
].filter(Boolean).join(' ')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function iconSheetConcept(app, index) {
|
|
213
|
+
const description = app.launcher.description ? ` ${app.launcher.description}.` : ''
|
|
214
|
+
const direction = appSpecificIconDirection(app)
|
|
215
|
+
const palette = APP_ICON_PALETTES[app.id] ? ` Palette: ${APP_ICON_PALETTES[app.id]}.` : ''
|
|
216
|
+
return `${index + 1}. ${app.name} (${app.id}):${description}${palette} ${direction}`.trim()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function iconStyleSheetPrompt(apps, { columns = 5 } = {}) {
|
|
220
|
+
const rows = Math.ceil(apps.length / columns)
|
|
221
|
+
return [
|
|
222
|
+
ICON_STYLE_SYSTEM_PROMPT,
|
|
223
|
+
'Create ONE style approval contact sheet, not final production assets.',
|
|
224
|
+
`The sheet is a clean ${columns} column by ${rows} row grid of square icon concepts with even gutters. Each cell shows one full square icon concept.`,
|
|
225
|
+
'Do not put text, numbers, labels, captions, or app names inside the image. The list below is only generation guidance.',
|
|
226
|
+
'Make the set cohesive, but deliberately vary dominant colors and silhouettes from cell to cell.',
|
|
227
|
+
'Every cell should be a proper clean synthwave app icon: one large readable luminous symbol, hard square corners, dark synthwave background, restrained glow, no clutter.',
|
|
228
|
+
'Prefer iconic reduction over literal depiction. If an app concept is complex, reduce it to the simplest recognizable geometric mark.',
|
|
229
|
+
'Generate concepts in this exact order:',
|
|
230
|
+
apps.map(iconSheetConcept).join('\n')
|
|
231
|
+
].join('\n')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function generateImagePng({ prompt, model, apiKey }) {
|
|
235
|
+
const response = await fetch('https://api.openai.com/v1/images/generations', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Bearer ${apiKey}`,
|
|
239
|
+
'Content-Type': 'application/json'
|
|
240
|
+
},
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
model,
|
|
243
|
+
prompt,
|
|
244
|
+
n: 1,
|
|
245
|
+
size: '1024x1024',
|
|
246
|
+
quality: 'high',
|
|
247
|
+
output_format: 'png'
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
const body = await response.json().catch(() => ({}))
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
const message = body?.error?.message || `${response.status} ${response.statusText}`
|
|
253
|
+
throw new Error(`OpenAI image generation failed: ${message}`)
|
|
254
|
+
}
|
|
255
|
+
const b64 = body?.data?.[0]?.b64_json
|
|
256
|
+
if (!b64) throw new Error('OpenAI image generation returned no b64_json')
|
|
257
|
+
return Buffer.from(b64, 'base64')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function generateSourcePng({ app, model, apiKey }) {
|
|
261
|
+
return generateImagePng({ prompt: iconPrompt(app), model, apiKey })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function resizeWithSharp(sourcePath, outputPath, size) {
|
|
265
|
+
const sharp = await import('sharp').then((mod) => mod.default || mod).catch(() => null)
|
|
266
|
+
if (!sharp) return false
|
|
267
|
+
await sharp(sourcePath).resize(size, size, { fit: 'cover' }).png().toFile(outputPath)
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resizeWithSips(sourcePath, outputPath, size) {
|
|
272
|
+
const result = spawnSync('sips', ['-s', 'format', 'png', '-z', String(size), String(size), sourcePath, '--out', outputPath], {
|
|
273
|
+
encoding: 'utf8'
|
|
274
|
+
})
|
|
275
|
+
return result.status === 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function writeResizedIconSet(sourcePath, iconDir) {
|
|
279
|
+
for (const size of ICON_SIZES) {
|
|
280
|
+
const outputPath = path.join(iconDir, `icon-${size}.png`)
|
|
281
|
+
const resized = await resizeWithSharp(sourcePath, outputPath, size)
|
|
282
|
+
if (!resized && !resizeWithSips(sourcePath, outputPath, size)) {
|
|
283
|
+
throw new Error('Icon resizing requires the optional sharp package or macOS sips.')
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function generateIconSet({ repoRoot, app, model, apiKey = process.env.OPENAI_API_KEY }) {
|
|
289
|
+
if (!apiKey) throw new Error('OPENAI_API_KEY is required to generate icons')
|
|
290
|
+
const iconDir = path.resolve(repoRoot, app.root, 'icons')
|
|
291
|
+
mkdirSync(iconDir, { recursive: true })
|
|
292
|
+
const sourcePath = path.join(iconDir, 'icon-source.png')
|
|
293
|
+
const source = await generateSourcePng({ app, model, apiKey })
|
|
294
|
+
writeFileSync(sourcePath, source)
|
|
295
|
+
await writeResizedIconSet(sourcePath, iconDir)
|
|
296
|
+
return ICON_SIZES.map((size) => path.join(iconDir, `icon-${size}.png`))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function generateIconStyleSheet({
|
|
300
|
+
apps,
|
|
301
|
+
model,
|
|
302
|
+
outputPath,
|
|
303
|
+
columns = 5,
|
|
304
|
+
apiKey = process.env.OPENAI_API_KEY
|
|
305
|
+
}) {
|
|
306
|
+
if (!apiKey) throw new Error('OPENAI_API_KEY is required to generate icon style sheets')
|
|
307
|
+
const prompt = iconStyleSheetPrompt(apps, { columns })
|
|
308
|
+
mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
309
|
+
const sheet = await generateImagePng({ prompt, model, apiKey })
|
|
310
|
+
writeFileSync(outputPath, sheet)
|
|
311
|
+
writeFileSync(outputPath.replace(/\.[^.]+$/, '.prompt.txt'), prompt)
|
|
312
|
+
return { outputPath, prompt }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function generateOneIconSet({ repoRoot, app, model }) {
|
|
316
|
+
console.error(`[gea] generating icons for ${app.id} with ${model}`)
|
|
317
|
+
const oldSource = path.resolve(repoRoot, app.root, 'icons', 'icon-source.png')
|
|
318
|
+
if (existsSync(oldSource)) rmSync(oldSource, { force: true })
|
|
319
|
+
await generateIconSet({ repoRoot, app, model })
|
|
320
|
+
console.error(`[gea] generated icons for ${app.id}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function generateIconSets({ repoRoot, apps, model, concurrency = 1 }) {
|
|
324
|
+
const queue = [...apps]
|
|
325
|
+
const failures = []
|
|
326
|
+
const workerCount = Math.max(1, Math.min(concurrency, queue.length || 1))
|
|
327
|
+
|
|
328
|
+
async function worker() {
|
|
329
|
+
while (queue.length > 0) {
|
|
330
|
+
const app = queue.shift()
|
|
331
|
+
try {
|
|
332
|
+
await generateOneIconSet({ repoRoot, app, model })
|
|
333
|
+
} catch (error) {
|
|
334
|
+
failures.push(error)
|
|
335
|
+
console.error(`[gea] failed icons for ${app.id}: ${error.message}`)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()))
|
|
341
|
+
if (failures.length > 0) {
|
|
342
|
+
throw new Error(`${failures.length} icon generation job(s) failed:\n${failures.map((error) => error.message).join('\n')}`)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function packageIconConfig() {
|
|
347
|
+
return Object.fromEntries(ICON_SIZES.map((size) => [String(size), `icons/icon-${size}.png`]))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function iconSourceExists(repoRoot, app) {
|
|
351
|
+
return existsSync(path.resolve(repoRoot, app.root, 'icons', 'icon-source.png'))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function iconBytes(repoRoot, app, size) {
|
|
355
|
+
return readFileSync(path.resolve(repoRoot, app.root, 'icons', `icon-${size}.png`))
|
|
356
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const CRC_TABLE = new Uint32Array(256)
|
|
5
|
+
for (let i = 0; i < CRC_TABLE.length; i += 1) {
|
|
6
|
+
let c = i
|
|
7
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
|
8
|
+
CRC_TABLE[i] = c >>> 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function crc32(buffer) {
|
|
12
|
+
let c = 0xffffffff
|
|
13
|
+
for (const byte of buffer) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8)
|
|
14
|
+
return (c ^ 0xffffffff) >>> 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function dosTimeDate(date = new Date()) {
|
|
18
|
+
const year = Math.max(1980, date.getFullYear())
|
|
19
|
+
const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2)
|
|
20
|
+
const day = (year - 1980) << 9 | ((date.getMonth() + 1) << 5) | date.getDate()
|
|
21
|
+
return { time, day }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function u16(value) {
|
|
25
|
+
const out = Buffer.allocUnsafe(2)
|
|
26
|
+
out.writeUInt16LE(value)
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function u32(value) {
|
|
31
|
+
const out = Buffer.allocUnsafe(4)
|
|
32
|
+
out.writeUInt32LE(value >>> 0)
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createZipBuffer(entries) {
|
|
37
|
+
const localParts = []
|
|
38
|
+
const centralParts = []
|
|
39
|
+
let offset = 0
|
|
40
|
+
const { time, day } = dosTimeDate()
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const name = Buffer.from(entry.name, 'utf8')
|
|
44
|
+
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data)
|
|
45
|
+
const crc = crc32(data)
|
|
46
|
+
const mode = entry.executable ? 0o100755 : 0o100644
|
|
47
|
+
const localHeader = Buffer.concat([
|
|
48
|
+
u32(0x04034b50),
|
|
49
|
+
u16(20),
|
|
50
|
+
u16(0),
|
|
51
|
+
u16(0),
|
|
52
|
+
u16(time),
|
|
53
|
+
u16(day),
|
|
54
|
+
u32(crc),
|
|
55
|
+
u32(data.length),
|
|
56
|
+
u32(data.length),
|
|
57
|
+
u16(name.length),
|
|
58
|
+
u16(0),
|
|
59
|
+
name
|
|
60
|
+
])
|
|
61
|
+
localParts.push(localHeader, data)
|
|
62
|
+
|
|
63
|
+
centralParts.push(Buffer.concat([
|
|
64
|
+
u32(0x02014b50),
|
|
65
|
+
u16(20),
|
|
66
|
+
u16(20),
|
|
67
|
+
u16(0),
|
|
68
|
+
u16(0),
|
|
69
|
+
u16(time),
|
|
70
|
+
u16(day),
|
|
71
|
+
u32(crc),
|
|
72
|
+
u32(data.length),
|
|
73
|
+
u32(data.length),
|
|
74
|
+
u16(name.length),
|
|
75
|
+
u16(0),
|
|
76
|
+
u16(0),
|
|
77
|
+
u16(0),
|
|
78
|
+
u16(0),
|
|
79
|
+
u32(mode << 16),
|
|
80
|
+
u32(offset),
|
|
81
|
+
name
|
|
82
|
+
]))
|
|
83
|
+
|
|
84
|
+
offset += localHeader.length + data.length
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const central = Buffer.concat(centralParts)
|
|
88
|
+
const eocd = Buffer.concat([
|
|
89
|
+
u32(0x06054b50),
|
|
90
|
+
u16(0),
|
|
91
|
+
u16(0),
|
|
92
|
+
u16(entries.length),
|
|
93
|
+
u16(entries.length),
|
|
94
|
+
u32(central.length),
|
|
95
|
+
u32(offset),
|
|
96
|
+
u16(0)
|
|
97
|
+
])
|
|
98
|
+
return Buffer.concat([...localParts, central, eocd])
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function writeZip(outputPath, entries) {
|
|
102
|
+
mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
103
|
+
writeFileSync(outputPath, createZipBuffer(entries))
|
|
104
|
+
}
|