@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.
@@ -0,0 +1,191 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
5
+
6
+ import { exists, readJson } from './fs-utils.mjs'
7
+
8
+ const defaultExamplesRepo = 'https://github.com/geastack/examples.git'
9
+ const defaultExamplesRef = 'main'
10
+
11
+ const excludedNames = new Set([
12
+ '.git',
13
+ '.gea',
14
+ 'dist',
15
+ 'build',
16
+ 'coverage',
17
+ 'test',
18
+ '__tests__',
19
+ 'node_modules',
20
+ '.turbo',
21
+ '.next',
22
+ 'vitest.config.ts',
23
+ 'vitest.config.js',
24
+ 'package-lock.json',
25
+ 'pnpm-lock.yaml',
26
+ 'yarn.lock'
27
+ ])
28
+
29
+ export function discoverBundledStarters(ctx) {
30
+ const startersRoot = path.join(ctx.cliPackageRoot, 'starters')
31
+ const catalogPath = path.join(startersRoot, 'catalog.json')
32
+ if (!exists(catalogPath)) return []
33
+ const catalog = readJson(catalogPath)
34
+ const entries = Array.isArray(catalog) ? catalog : catalog.starters || []
35
+ return entries.flatMap((entry) => bundledStarterFromCatalogEntry(entry, startersRoot))
36
+ .sort((a, b) => a.name.localeCompare(b.name))
37
+ }
38
+
39
+ export function discoverGithubExamples(ctx, env = process.env) {
40
+ const catalogPath = path.join(ctx.cliPackageRoot, 'examples', 'catalog.json')
41
+ if (!exists(catalogPath)) return []
42
+ const catalog = readJson(catalogPath)
43
+ const repo = env.GEA_EXAMPLES_REPO || defaultExamplesRepo
44
+ const ref = env.GEA_EXAMPLES_REF || defaultExamplesRef
45
+ const entries = Array.isArray(catalog) ? catalog : catalog.examples || []
46
+ return entries.flatMap((entry) => githubExampleFromCatalogEntry(entry, { repo, ref }))
47
+ .sort((a, b) => a.name.localeCompare(b.name))
48
+ }
49
+
50
+ export function copyStarterFiles(sourceDir, targetDir) {
51
+ fs.cpSync(sourceDir, targetDir, {
52
+ recursive: true,
53
+ filter: (source) => !excludedNames.has(path.basename(source))
54
+ })
55
+ }
56
+
57
+ export function formatStarterChoice(starter) {
58
+ const targets = targetBadge(starter.targets)
59
+ return `${starter.name} - ${starter.description}${targets ? ` [${targets}]` : ''}`
60
+ }
61
+
62
+ export function fetchGithubExample(example, targetDir, options = {}) {
63
+ const repo = options.repo || example.repo
64
+ const ref = options.ref || example.ref || 'main'
65
+ if (!repo) throw new Error(`Example '${example.id}' does not define a GitHub repository.`)
66
+
67
+ if (isDirectory(repo)) {
68
+ const sourceDir = path.join(repo, example.path)
69
+ if (!isDirectory(sourceDir)) throw new Error(`Example '${example.id}' was not found at ${sourceDir}.`)
70
+ copyStarterFiles(sourceDir, targetDir)
71
+ return
72
+ }
73
+
74
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gea-example-'))
75
+ try {
76
+ runGit(['clone', '--depth', '1', '--filter=blob:none', '--sparse', '--branch', ref, repo, tmp], options)
77
+ runGit(['-C', tmp, 'sparse-checkout', 'set', example.path], options)
78
+ const sourceDir = path.join(tmp, example.path)
79
+ if (!isDirectory(sourceDir)) throw new Error(`Example '${example.id}' was not found in ${repo} at ${example.path}.`)
80
+ copyStarterFiles(sourceDir, targetDir)
81
+ } finally {
82
+ fs.rmSync(tmp, { recursive: true, force: true })
83
+ }
84
+ }
85
+
86
+ function targetSummary(targets = {}) {
87
+ const enabled = Object.entries(targets)
88
+ .filter(([, value]) => value === true)
89
+ .map(([key]) => key)
90
+ return enabled.length > 0 ? `Targets: ${enabled.join(', ')}` : 'Working Gea example'
91
+ }
92
+
93
+ function targetBadge(targets = {}) {
94
+ return Object.entries(targets)
95
+ .filter(([, value]) => value === true)
96
+ .map(([key]) => key)
97
+ .join(', ')
98
+ }
99
+
100
+ function bundledStarterFromCatalogEntry(entry, startersRoot) {
101
+ if (!entry || typeof entry !== 'object') return []
102
+ const root = path.resolve(startersRoot, entry.path || '')
103
+ if (!isInside(root, startersRoot)) return []
104
+ const starterEntry = entry.entry || 'index.tsx'
105
+ if (!exists(path.join(root, starterEntry))) return []
106
+ const packageJson = readOptionalJson(path.join(root, 'package.json'))
107
+ const targets = entry.targets && typeof entry.targets === 'object' ? entry.targets : packageJson.gea?.targets || {}
108
+ const description = entry.description || packageJson.gea?.description || packageJson.gea?.launcher?.description || packageJson.description || targetSummary(targets)
109
+ const manifest = {
110
+ ...(packageJson.gea || {}),
111
+ description,
112
+ entry: starterEntry,
113
+ runtime: entry.runtime || packageJson.gea?.runtime || 'gea',
114
+ targets
115
+ }
116
+ return [{
117
+ id: String(entry.id || packageJson.gea?.id || ''),
118
+ name: String(entry.name || packageJson.gea?.name || entry.id || ''),
119
+ description: manifest.description,
120
+ root,
121
+ source: 'bundled',
122
+ entry: manifest.entry,
123
+ runtime: manifest.runtime,
124
+ targets,
125
+ manifest,
126
+ packageJson
127
+ }].filter((starter) => starter.id && starter.name)
128
+ }
129
+
130
+ function githubExampleFromCatalogEntry(entry, source) {
131
+ if (!entry || typeof entry !== 'object') return []
132
+ const id = String(entry.id || '')
133
+ const examplePath = String(entry.path || '')
134
+ if (!id || !examplePath || examplePath.startsWith('/') || examplePath.includes('..')) return []
135
+ const targets = entry.targets && typeof entry.targets === 'object' ? entry.targets : {}
136
+ return [{
137
+ id,
138
+ name: String(entry.name || id),
139
+ description: String(entry.description || targetSummary(targets)),
140
+ source: 'github',
141
+ repo: source.repo,
142
+ ref: source.ref,
143
+ path: examplePath,
144
+ entry: String(entry.entry || 'index.tsx'),
145
+ runtime: String(entry.runtime || 'gea'),
146
+ targets,
147
+ manifest: {
148
+ description: String(entry.description || targetSummary(targets)),
149
+ entry: String(entry.entry || 'index.tsx'),
150
+ runtime: String(entry.runtime || 'gea'),
151
+ targets
152
+ },
153
+ packageJson: {}
154
+ }]
155
+ }
156
+
157
+ function readOptionalJson(filePath) {
158
+ if (!exists(filePath)) return {}
159
+ try {
160
+ return readJson(filePath)
161
+ } catch {
162
+ return {}
163
+ }
164
+ }
165
+
166
+ function isInside(child, parent) {
167
+ const relative = path.relative(parent, child)
168
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
169
+ }
170
+
171
+ function isDirectory(filePath) {
172
+ try {
173
+ return fs.statSync(filePath).isDirectory()
174
+ } catch {
175
+ return false
176
+ }
177
+ }
178
+
179
+ function runGit(args, options) {
180
+ if (options.dryRun) {
181
+ options.stdout?.(['git', ...args].join(' '))
182
+ return
183
+ }
184
+ const result = spawnSync('git', args, {
185
+ cwd: options.cwd || process.cwd(),
186
+ env: options.env || process.env,
187
+ stdio: 'inherit'
188
+ })
189
+ if (result.error) throw result.error
190
+ if (result.status !== 0) throw new Error(`git ${args.join(' ')} failed with exit code ${result.status ?? 1}`)
191
+ }
@@ -0,0 +1,15 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ export function nodeAtLeast(major, minor) {
4
+ const [actualMajor, actualMinor] = process.versions.node.split('.').map((value) => Number.parseInt(value, 10))
5
+ return actualMajor > major || (actualMajor === major && actualMinor >= minor)
6
+ }
7
+
8
+ export function commandVersion(command, args, env = process.env) {
9
+ if (!command) return ''
10
+ try {
11
+ return execFileSync(command, args, { encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'pipe'] }).trim()
12
+ } catch {
13
+ return ''
14
+ }
15
+ }
@@ -0,0 +1,25 @@
1
+ import { mount } from '@geastack/core'
2
+ import { counter } from './store'
3
+ import './styles.css'
4
+
5
+ function App() {
6
+ function increment() {
7
+ const value = counter.increment()
8
+ const count = document.querySelector('[data-count]')
9
+ if (count) count.textContent = String(value)
10
+ }
11
+
12
+ return (
13
+ <body class="app">
14
+ <main class="panel">
15
+ <p class="eyebrow">GeaStack</p>
16
+ <h1>Counter</h1>
17
+ <button class="button" onClick={increment}>
18
+ Count <span data-count>0</span>
19
+ </button>
20
+ </main>
21
+ </body>
22
+ )
23
+ }
24
+
25
+ mount(App)
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@geastack/starter-counter",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Minimal JSX counter with one tiny store.",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "check": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@geajs/core": "^1.3.0"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "latest",
16
+ "vite": "latest"
17
+ },
18
+ "gea": {
19
+ "id": "counter",
20
+ "name": "Counter",
21
+ "description": "Minimal JSX counter with one tiny store.",
22
+ "entry": "index.tsx",
23
+ "runtime": "gea",
24
+ "targets": {
25
+ "web": true,
26
+ "esp32": true,
27
+ "rp2350": true,
28
+ "geaos": true,
29
+ "macos": false,
30
+ "ios": false
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ export const counter = {
2
+ value: 0,
3
+ increment() {
4
+ this.value += 1
5
+ return this.value
6
+ }
7
+ }
@@ -0,0 +1,39 @@
1
+ .app {
2
+ width: 100vw;
3
+ height: 100vh;
4
+ margin: 0;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ background: #101418;
9
+ color: #f8fafc;
10
+ font-family: Inter, system-ui, sans-serif;
11
+ }
12
+
13
+ .panel {
14
+ width: min(320px, calc(100vw - 32px));
15
+ padding: 24px;
16
+ border: 1px solid #2dd4bf;
17
+ background: #182026;
18
+ }
19
+
20
+ .eyebrow {
21
+ margin: 0 0 8px;
22
+ color: #2dd4bf;
23
+ font-size: 12px;
24
+ text-transform: uppercase;
25
+ }
26
+
27
+ h1 {
28
+ margin: 0 0 16px;
29
+ font-size: 28px;
30
+ }
31
+
32
+ .button {
33
+ min-width: 120px;
34
+ min-height: 40px;
35
+ border: 0;
36
+ background: #2dd4bf;
37
+ color: #081018;
38
+ font: inherit;
39
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "starters": [
4
+ {
5
+ "id": "counter",
6
+ "name": "Counter",
7
+ "description": "Minimal JSX counter with one tiny store.",
8
+ "path": "bundled/counter",
9
+ "entry": "index.tsx",
10
+ "runtime": "gea",
11
+ "targets": {
12
+ "web": true,
13
+ "esp32": true,
14
+ "rp2350": true,
15
+ "geaos": true,
16
+ "macos": false,
17
+ "ios": false,
18
+ "android": false
19
+ }
20
+ }
21
+ ]
22
+ }