@geastack/cli 0.1.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/LICENSE +674 -0
- package/README.md +161 -0
- package/bin/create-geastack.mjs +15 -0
- package/bin/gea.mjs +15 -0
- package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +133 -0
- package/docs/NPX-COMMANDS.md +144 -0
- package/docs/SETUP.md +276 -0
- package/docs/SPEC.md +217 -0
- package/examples/catalog.json +668 -0
- package/package.json +41 -0
- package/src/args.mjs +78 -0
- package/src/board-catalog.mjs +224 -0
- package/src/context.mjs +110 -0
- package/src/create-geastack.mjs +430 -0
- package/src/errors.mjs +20 -0
- package/src/fs-utils.mjs +47 -0
- package/src/gea.mjs +444 -0
- package/src/manifest.mjs +171 -0
- package/src/prompts.mjs +60 -0
- package/src/run.mjs +33 -0
- package/src/serial-devices.mjs +143 -0
- package/src/setup-wizard.mjs +633 -0
- package/src/starter-catalog.mjs +191 -0
- package/src/toolchain.mjs +15 -0
- package/starters/bundled/counter/index.tsx +25 -0
- package/starters/bundled/counter/package.json +33 -0
- package/starters/bundled/counter/store.ts +7 -0
- package/starters/bundled/counter/styles.css +39 -0
- package/starters/catalog.json +22 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { flag, option, optionList, parseArgs } from './args.mjs'
|
|
5
|
+
import { createContext } from './context.mjs'
|
|
6
|
+
import { ExitCode, fail } from './errors.mjs'
|
|
7
|
+
import { exists, readJson, writeJson } from './fs-utils.mjs'
|
|
8
|
+
import { canPrompt, choose, createPrompt } from './prompts.mjs'
|
|
9
|
+
import { runExternal } from './run.mjs'
|
|
10
|
+
import {
|
|
11
|
+
copyStarterFiles,
|
|
12
|
+
discoverBundledStarters,
|
|
13
|
+
discoverGithubExamples,
|
|
14
|
+
fetchGithubExample,
|
|
15
|
+
formatStarterChoice
|
|
16
|
+
} from './starter-catalog.mjs'
|
|
17
|
+
|
|
18
|
+
export async function runCreateGeastack(argv, io = {}) {
|
|
19
|
+
const parsed = parseArgs(argv)
|
|
20
|
+
const stdout = io.stdout || console.log
|
|
21
|
+
const env = io.env || process.env
|
|
22
|
+
const cwd = io.cwd || process.cwd()
|
|
23
|
+
|
|
24
|
+
if (flag(parsed, 'help') || parsed.positionals[0] === 'help') {
|
|
25
|
+
stdout(usage())
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rawName = parsed.positionals[0]
|
|
30
|
+
if (!rawName) fail('create-geastack requires an app name.', ExitCode.usage)
|
|
31
|
+
|
|
32
|
+
const appId = slugify(option(parsed, 'id', rawName))
|
|
33
|
+
if (!appId) fail(`Could not derive a valid app id from '${rawName}'.`, ExitCode.usage)
|
|
34
|
+
|
|
35
|
+
const targetDir = path.resolve(cwd, option(parsed, 'dir', rawName))
|
|
36
|
+
ensureWritableTarget(targetDir, flag(parsed, 'force'))
|
|
37
|
+
|
|
38
|
+
const ctx = createContext(parsed, env, cwd)
|
|
39
|
+
const displayName = option(parsed, 'name', titleFromId(appId))
|
|
40
|
+
const coreDependency = option(parsed, 'core-dependency') || '^0.1.2'
|
|
41
|
+
const cliDependency = option(parsed, 'cli-dependency') || '^0.1.0'
|
|
42
|
+
const starter = await resolveStarter(ctx, parsed, io)
|
|
43
|
+
|
|
44
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
45
|
+
if (starter.kind === 'empty') {
|
|
46
|
+
fs.writeFileSync(path.join(targetDir, 'index.tsx'), indexTsx(displayName))
|
|
47
|
+
fs.writeFileSync(path.join(targetDir, 'styles.css'), stylesCss())
|
|
48
|
+
} else if (starter.kind === 'bundled') {
|
|
49
|
+
copyStarterFiles(starter.starter.root, targetDir)
|
|
50
|
+
} else if (starter.kind === 'example') {
|
|
51
|
+
const repo = option(parsed, 'examples-repo') || env.GEA_EXAMPLES_REPO || starter.example.repo
|
|
52
|
+
const ref = option(parsed, 'examples-ref') || env.GEA_EXAMPLES_REF || starter.example.ref
|
|
53
|
+
fetchGithubExample(starter.example, targetDir, {
|
|
54
|
+
repo,
|
|
55
|
+
ref,
|
|
56
|
+
env,
|
|
57
|
+
cwd,
|
|
58
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
59
|
+
stdout
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourcePackage = readPackageJson(targetDir)
|
|
64
|
+
const sourceManifest = sourcePackage.gea || starter.starter?.manifest || starter.example?.manifest || {}
|
|
65
|
+
const targets = optionList(parsed, 'targets').length === 0
|
|
66
|
+
? targetManifestFromObject(sourceManifest.targets)
|
|
67
|
+
: targetManifest(optionList(parsed, 'targets'))
|
|
68
|
+
writeJson(path.join(targetDir, 'package.json'), packageJson({
|
|
69
|
+
appId,
|
|
70
|
+
displayName,
|
|
71
|
+
targets,
|
|
72
|
+
coreDependency,
|
|
73
|
+
cliDependency,
|
|
74
|
+
sourcePackage,
|
|
75
|
+
sourceManifest
|
|
76
|
+
}))
|
|
77
|
+
ensureFile(path.join(targetDir, 'index.html'), () => indexHtml(displayName))
|
|
78
|
+
fs.mkdirSync(path.join(targetDir, '.gea'), { recursive: true })
|
|
79
|
+
writeJson(path.join(targetDir, '.gea', 'boards.json'), {})
|
|
80
|
+
ensureJson(path.join(targetDir, 'tsconfig.json'), tsconfigJson)
|
|
81
|
+
ensureFile(path.join(targetDir, 'vite.config.ts'), viteConfigTs)
|
|
82
|
+
fs.writeFileSync(path.join(targetDir, 'README.md'), readme({ appId, displayName, starter }))
|
|
83
|
+
|
|
84
|
+
const installDependencies = shouldInstallDependencies(parsed, io)
|
|
85
|
+
if (installDependencies) {
|
|
86
|
+
stdout('Installing npm dependencies...')
|
|
87
|
+
runExternal('npm', ['install'], {
|
|
88
|
+
cwd: targetDir,
|
|
89
|
+
env,
|
|
90
|
+
dryRun: flag(parsed, 'dry-run'),
|
|
91
|
+
failureCode: ExitCode.missingDependency,
|
|
92
|
+
stdout
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
stdout(`Created ${displayName} at ${targetDir}`)
|
|
97
|
+
if (starter.kind === 'bundled') stdout(`Starter: ${starter.starter.name}`)
|
|
98
|
+
else if (starter.kind === 'example') stdout(`Example: fetched ${starter.example.name}`)
|
|
99
|
+
else stdout('Starter: empty app')
|
|
100
|
+
if (installDependencies) stdout(`Next: cd ${targetDir} && npx gea setup`)
|
|
101
|
+
else stdout(`Next: cd ${targetDir} && npm install && npx gea setup`)
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function resolveStarter(ctx, parsed, io) {
|
|
106
|
+
const interactive = canPrompt(io) && !flag(parsed, 'yes')
|
|
107
|
+
const bundled = discoverBundledStarters(ctx)
|
|
108
|
+
const examples = discoverGithubExamples(ctx, io.env || process.env)
|
|
109
|
+
const requested = normalizeStarter(option(parsed, 'starter') || option(parsed, 'template') || '')
|
|
110
|
+
const prompt = interactive ? createPrompt(io) : null
|
|
111
|
+
try {
|
|
112
|
+
const mode = requested || await chooseStarterMode({ interactive, prompt, bundled, examples })
|
|
113
|
+
|
|
114
|
+
if (mode === 'empty') return { kind: 'empty' }
|
|
115
|
+
if (mode === 'bundled' || mode === 'counter') {
|
|
116
|
+
const requestedBundled = mode === 'counter' ? 'counter' : option(parsed, 'bundled') || option(parsed, 'starter-id') || ''
|
|
117
|
+
const starter = requestedBundled
|
|
118
|
+
? bundled.find((candidate) => candidate.id === requestedBundled)
|
|
119
|
+
: bundled[0]
|
|
120
|
+
if (!starter) {
|
|
121
|
+
const available = bundled.map((candidate) => candidate.id).join(', ')
|
|
122
|
+
fail(`Unknown bundled starter '${requestedBundled}'. Available starters: ${available}`, ExitCode.usage)
|
|
123
|
+
}
|
|
124
|
+
return { kind: 'bundled', starter }
|
|
125
|
+
}
|
|
126
|
+
if (mode !== 'example') {
|
|
127
|
+
fail(`Unknown starter '${mode}'. Expected counter, empty, or example.`, ExitCode.usage)
|
|
128
|
+
}
|
|
129
|
+
if (examples.length === 0) {
|
|
130
|
+
fail('No GitHub examples are available. Use --starter counter or --starter empty.', ExitCode.usage)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const requestedExample = option(parsed, 'example') || option(parsed, 'from-example') || ''
|
|
134
|
+
const example = requestedExample
|
|
135
|
+
? examples.find((candidate) => candidate.id === requestedExample)
|
|
136
|
+
: await chooseExample({ interactive, prompt, examples })
|
|
137
|
+
if (!example) {
|
|
138
|
+
const available = examples.map((candidate) => candidate.id).join(', ')
|
|
139
|
+
fail(`Unknown starter example '${requestedExample}'. Available examples: ${available}`, ExitCode.usage)
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'example', example }
|
|
142
|
+
} finally {
|
|
143
|
+
if (prompt) await prompt.close()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function chooseStarterMode({ interactive, prompt, bundled, examples }) {
|
|
148
|
+
if (!interactive) return bundled.length > 0 ? 'counter' : 'empty'
|
|
149
|
+
return choose(prompt, {
|
|
150
|
+
message: 'Starter app',
|
|
151
|
+
choices: [
|
|
152
|
+
...(bundled.length > 0 ? [{ value: 'counter', label: 'Counter starter - bundled minimal JSX app' }] : []),
|
|
153
|
+
{ value: 'empty', label: 'Empty app - minimal blank Gea app' },
|
|
154
|
+
...(examples.length > 0 ? [{ value: 'example', label: 'Rich example - fetch from GitHub examples repo' }] : [])
|
|
155
|
+
],
|
|
156
|
+
defaultValue: bundled.length > 0 ? 'counter' : 'empty'
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function chooseExample({ interactive, prompt, examples }) {
|
|
161
|
+
if (!interactive) {
|
|
162
|
+
fail('Choosing --starter example in a non-interactive shell also requires --example <id>.', ExitCode.usage)
|
|
163
|
+
}
|
|
164
|
+
const id = await choose(prompt, {
|
|
165
|
+
message: 'Example to copy',
|
|
166
|
+
choices: examples.map((example) => ({ value: example.id, label: formatStarterChoice(example) })),
|
|
167
|
+
defaultValue: examples[0].id
|
|
168
|
+
})
|
|
169
|
+
return examples.find((example) => example.id === id) || null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ensureWritableTarget(targetDir, force) {
|
|
173
|
+
if (!exists(targetDir)) return
|
|
174
|
+
const entries = fs.readdirSync(targetDir).filter((entry) => entry !== '.DS_Store')
|
|
175
|
+
if (entries.length > 0 && !force) {
|
|
176
|
+
fail(`Target directory is not empty: ${targetDir}. Pass --force to write into it.`, ExitCode.usage)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function targetManifest(requestedTargets) {
|
|
181
|
+
const enabled = requestedTargets.length > 0 ? new Set(requestedTargets) : new Set(['web', 'esp32', 'rp2350', 'geaos'])
|
|
182
|
+
return {
|
|
183
|
+
web: enabled.has('web'),
|
|
184
|
+
esp32: enabled.has('esp32'),
|
|
185
|
+
rp2350: enabled.has('rp2350'),
|
|
186
|
+
geaos: enabled.has('geaos'),
|
|
187
|
+
macos: enabled.has('macos'),
|
|
188
|
+
ios: enabled.has('ios'),
|
|
189
|
+
android: enabled.has('android')
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function targetManifestFromObject(targets = {}) {
|
|
194
|
+
if (!targets || typeof targets !== 'object') return targetManifest([])
|
|
195
|
+
return {
|
|
196
|
+
web: targets.web === true,
|
|
197
|
+
esp32: targets.esp32 === true,
|
|
198
|
+
rp2350: targets.rp2350 === true,
|
|
199
|
+
geaos: targets.geaos === true,
|
|
200
|
+
macos: targets.macos === true,
|
|
201
|
+
ios: targets.ios === true,
|
|
202
|
+
android: targets.android === true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function packageJson({ appId, displayName, targets, coreDependency, cliDependency, sourcePackage = {}, sourceManifest = {} }) {
|
|
207
|
+
return {
|
|
208
|
+
name: `gea-${appId}`,
|
|
209
|
+
version: '0.1.0',
|
|
210
|
+
private: true,
|
|
211
|
+
type: 'module',
|
|
212
|
+
scripts: {
|
|
213
|
+
...(sourcePackage.scripts || {}),
|
|
214
|
+
dev: 'gea dev',
|
|
215
|
+
build: 'gea build --target web',
|
|
216
|
+
check: 'tsc --noEmit'
|
|
217
|
+
},
|
|
218
|
+
dependencies: {
|
|
219
|
+
...(sourcePackage.dependencies || {}),
|
|
220
|
+
'@geajs/core': sourcePackage.dependencies?.['@geajs/core'] || '^1.3.0',
|
|
221
|
+
'@geastack/core': coreDependency
|
|
222
|
+
},
|
|
223
|
+
devDependencies: {
|
|
224
|
+
...(sourcePackage.devDependencies || {}),
|
|
225
|
+
'@geastack/cli': cliDependency,
|
|
226
|
+
typescript: sourcePackage.devDependencies?.typescript || 'latest',
|
|
227
|
+
vite: sourcePackage.devDependencies?.vite || 'latest'
|
|
228
|
+
},
|
|
229
|
+
gea: {
|
|
230
|
+
...sourceManifest,
|
|
231
|
+
id: appId,
|
|
232
|
+
name: displayName,
|
|
233
|
+
entry: sourceManifest.entry || 'index.tsx',
|
|
234
|
+
runtime: sourceManifest.runtime || 'gea',
|
|
235
|
+
targets
|
|
236
|
+
},
|
|
237
|
+
license: 'MIT'
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function indexTsx(displayName) {
|
|
242
|
+
return `import { mount } from '@geastack/core'
|
|
243
|
+
import './styles.css'
|
|
244
|
+
|
|
245
|
+
function App() {
|
|
246
|
+
return (
|
|
247
|
+
<body class="app">
|
|
248
|
+
<main class="panel">
|
|
249
|
+
<p class="eyebrow">GeaStack</p>
|
|
250
|
+
<h1>${escapeText(displayName)}</h1>
|
|
251
|
+
<p class="copy">One TypeScript app, ready for simulator and native targets.</p>
|
|
252
|
+
</main>
|
|
253
|
+
</body>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
mount(App)
|
|
258
|
+
`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function stylesCss() {
|
|
262
|
+
return `.app {
|
|
263
|
+
width: 100vw;
|
|
264
|
+
height: 100vh;
|
|
265
|
+
margin: 0;
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
background: #101418;
|
|
270
|
+
color: #f8fafc;
|
|
271
|
+
font-family: Inter, system-ui, sans-serif;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.panel {
|
|
275
|
+
width: min(320px, calc(100vw - 32px));
|
|
276
|
+
padding: 24px;
|
|
277
|
+
border: 1px solid #2dd4bf;
|
|
278
|
+
background: #182026;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.eyebrow {
|
|
282
|
+
margin: 0 0 8px;
|
|
283
|
+
color: #2dd4bf;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
text-transform: uppercase;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
h1 {
|
|
289
|
+
margin: 0;
|
|
290
|
+
font-size: 28px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.copy {
|
|
294
|
+
margin: 12px 0 0;
|
|
295
|
+
color: #cbd5e1;
|
|
296
|
+
}
|
|
297
|
+
`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function indexHtml(displayName) {
|
|
301
|
+
return `<!doctype html>
|
|
302
|
+
<html lang="en">
|
|
303
|
+
<head>
|
|
304
|
+
<meta charset="UTF-8" />
|
|
305
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
306
|
+
<title>${escapeText(displayName)}</title>
|
|
307
|
+
</head>
|
|
308
|
+
<body>
|
|
309
|
+
<div id="app"></div>
|
|
310
|
+
<script type="module" src="/index.tsx"></script>
|
|
311
|
+
</body>
|
|
312
|
+
</html>
|
|
313
|
+
`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function tsconfigJson() {
|
|
317
|
+
return {
|
|
318
|
+
compilerOptions: {
|
|
319
|
+
target: 'ES2022',
|
|
320
|
+
module: 'ESNext',
|
|
321
|
+
moduleResolution: 'Bundler',
|
|
322
|
+
lib: ['ES2022', 'DOM'],
|
|
323
|
+
strict: true,
|
|
324
|
+
noEmit: true,
|
|
325
|
+
skipLibCheck: true,
|
|
326
|
+
jsx: 'preserve',
|
|
327
|
+
jsxImportSource: '@geajs/core',
|
|
328
|
+
allowImportingTsExtensions: true
|
|
329
|
+
},
|
|
330
|
+
include: ['**/*.tsx', '**/*.ts', '**/*.d.ts'],
|
|
331
|
+
exclude: ['vite.config.ts', 'dist']
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function viteConfigTs() {
|
|
336
|
+
return `import { defineConfig } from 'vite'
|
|
337
|
+
|
|
338
|
+
export default defineConfig({
|
|
339
|
+
root: __dirname,
|
|
340
|
+
build: {
|
|
341
|
+
outDir: 'dist',
|
|
342
|
+
emptyOutDir: true
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function readme({ appId, displayName, starter }) {
|
|
349
|
+
const starterLine = starter.kind === 'example'
|
|
350
|
+
? `Started from GitHub example: \`${starter.example.name}\` (\`${starter.example.id}\`).`
|
|
351
|
+
: starter.kind === 'bundled'
|
|
352
|
+
? `Started from bundled starter: \`${starter.starter.name}\` (\`${starter.starter.id}\`).`
|
|
353
|
+
: 'Started from an empty app.'
|
|
354
|
+
return `# ${displayName}
|
|
355
|
+
|
|
356
|
+
GeaStack app id: \`${appId}\`.
|
|
357
|
+
|
|
358
|
+
${starterLine}
|
|
359
|
+
|
|
360
|
+
\`\`\`sh
|
|
361
|
+
npx gea setup
|
|
362
|
+
npx gea dev
|
|
363
|
+
npx gea build --target web
|
|
364
|
+
\`\`\`
|
|
365
|
+
`
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function shouldInstallDependencies(parsed, io) {
|
|
369
|
+
const explicit = option(parsed, 'install')
|
|
370
|
+
if (explicit !== undefined) return explicit === true || explicit === 'true'
|
|
371
|
+
return canPrompt(io)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function slugify(value) {
|
|
375
|
+
return String(value)
|
|
376
|
+
.trim()
|
|
377
|
+
.toLowerCase()
|
|
378
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
379
|
+
.replace(/^-+|-+$/g, '')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function titleFromId(id) {
|
|
383
|
+
return id.split('-').filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(' ')
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function escapeText(value) {
|
|
387
|
+
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeStarter(value) {
|
|
391
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
392
|
+
if (!normalized) return ''
|
|
393
|
+
if (['empty', 'blank', 'minimal'].includes(normalized)) return 'empty'
|
|
394
|
+
if (['counter', 'starter', 'bundled', 'hello', 'hello-world'].includes(normalized)) return normalized === 'counter' ? 'counter' : 'bundled'
|
|
395
|
+
if (['example', 'examples', 'from-example', 'from_example'].includes(normalized)) return 'example'
|
|
396
|
+
return normalized
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function readPackageJson(targetDir) {
|
|
400
|
+
const packagePath = path.join(targetDir, 'package.json')
|
|
401
|
+
if (!exists(packagePath)) return {}
|
|
402
|
+
return readJson(packagePath)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function ensureFile(filePath, create) {
|
|
406
|
+
if (!exists(filePath)) fs.writeFileSync(filePath, create())
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function ensureJson(filePath, create) {
|
|
410
|
+
if (!exists(filePath)) writeJson(filePath, create())
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function usage() {
|
|
414
|
+
return `Usage:
|
|
415
|
+
create-geastack <name> [--dir <path>] [--id <app-id>] [--name <display-name>]
|
|
416
|
+
|
|
417
|
+
Options:
|
|
418
|
+
--starter counter|empty|example
|
|
419
|
+
--example <example-id>
|
|
420
|
+
--examples-repo <git-url-or-local-path>
|
|
421
|
+
--examples-ref <git-ref>
|
|
422
|
+
--targets web,esp32,rp2350,geaos,macos,ios,android
|
|
423
|
+
--core-dependency <specifier>
|
|
424
|
+
--cli-dependency <specifier>
|
|
425
|
+
--install / --no-install
|
|
426
|
+
--dry-run
|
|
427
|
+
--yes
|
|
428
|
+
--force
|
|
429
|
+
`
|
|
430
|
+
}
|
package/src/errors.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const ExitCode = Object.freeze({
|
|
2
|
+
generic: 1,
|
|
3
|
+
usage: 2,
|
|
4
|
+
missingDependency: 3,
|
|
5
|
+
targetUnavailable: 4,
|
|
6
|
+
buildFailed: 5,
|
|
7
|
+
deployFailed: 6
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export class CliError extends Error {
|
|
11
|
+
constructor(message, exitCode = ExitCode.generic) {
|
|
12
|
+
super(message)
|
|
13
|
+
this.name = 'CliError'
|
|
14
|
+
this.exitCode = exitCode
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function fail(message, exitCode = ExitCode.generic) {
|
|
19
|
+
throw new CliError(`ERROR: ${message}`, exitCode)
|
|
20
|
+
}
|
package/src/fs-utils.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function exists(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
fs.accessSync(filePath)
|
|
7
|
+
return true
|
|
8
|
+
} catch {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isDirectory(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(filePath).isDirectory()
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readJson(filePath) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeJson(filePath, value) {
|
|
26
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function listDirectories(root) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isDirectory())
|
|
33
|
+
.map((entry) => path.join(root, entry.name))
|
|
34
|
+
} catch {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findUp(startDir, predicate) {
|
|
40
|
+
let current = path.resolve(startDir)
|
|
41
|
+
while (true) {
|
|
42
|
+
if (predicate(current)) return current
|
|
43
|
+
const parent = path.dirname(current)
|
|
44
|
+
if (parent === current) return ''
|
|
45
|
+
current = parent
|
|
46
|
+
}
|
|
47
|
+
}
|