@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
package/src/args.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const options = {}
|
|
3
|
+
const positionals = []
|
|
4
|
+
const passthrough = []
|
|
5
|
+
let pass = false
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
8
|
+
const arg = argv[i]
|
|
9
|
+
if (pass) {
|
|
10
|
+
passthrough.push(arg)
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
if (arg === '--') {
|
|
14
|
+
pass = true
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
if (arg === '-h') {
|
|
18
|
+
setOption(options, 'help', true)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
if (arg === '-v') {
|
|
22
|
+
setOption(options, 'version', true)
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
if (arg.startsWith('--')) {
|
|
26
|
+
const raw = arg.slice(2)
|
|
27
|
+
if (raw.startsWith('no-') && !raw.includes('=')) {
|
|
28
|
+
setOption(options, raw.slice(3), false)
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
const eq = raw.indexOf('=')
|
|
32
|
+
if (eq !== -1) {
|
|
33
|
+
setOption(options, raw.slice(0, eq), raw.slice(eq + 1))
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
const next = argv[i + 1]
|
|
37
|
+
if (next && !next.startsWith('-')) {
|
|
38
|
+
setOption(options, raw, next)
|
|
39
|
+
i += 1
|
|
40
|
+
} else {
|
|
41
|
+
setOption(options, raw, true)
|
|
42
|
+
}
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
positionals.push(arg)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { options, positionals, passthrough }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function option(parsed, name, fallback = undefined) {
|
|
52
|
+
const value = parsed.options[name]
|
|
53
|
+
if (value === undefined) return fallback
|
|
54
|
+
return Array.isArray(value) ? value[value.length - 1] : value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function optionList(parsed, name) {
|
|
58
|
+
const value = parsed.options[name]
|
|
59
|
+
if (value === undefined) return []
|
|
60
|
+
return (Array.isArray(value) ? value : [value])
|
|
61
|
+
.flatMap((entry) => String(entry).split(','))
|
|
62
|
+
.map((entry) => entry.trim())
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function flag(parsed, name) {
|
|
67
|
+
return option(parsed, name, false) !== false && option(parsed, name, false) !== undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setOption(options, name, value) {
|
|
71
|
+
if (options[name] === undefined) {
|
|
72
|
+
options[name] = value
|
|
73
|
+
} else if (Array.isArray(options[name])) {
|
|
74
|
+
options[name].push(value)
|
|
75
|
+
} else {
|
|
76
|
+
options[name] = [options[name], value]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
export const knownBoards = Object.freeze([
|
|
2
|
+
{
|
|
3
|
+
id: 'waveshare-amoled-206',
|
|
4
|
+
label: 'Waveshare ESP32-S3 Touch AMOLED 2.06',
|
|
5
|
+
alias: 'amoled',
|
|
6
|
+
target: 'esp32-s3-touch-amoled-2.06',
|
|
7
|
+
adapter: 'esp32-idf',
|
|
8
|
+
mcu: 'ESP32-S3',
|
|
9
|
+
capabilities: {
|
|
10
|
+
display: '2.06 inch AMOLED touch panel',
|
|
11
|
+
touch: 'board integrated touch',
|
|
12
|
+
wireless: ['WiFi', 'BLE'],
|
|
13
|
+
storage: ['flash', 'PSRAM'],
|
|
14
|
+
audio: [],
|
|
15
|
+
gps: false
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'waveshare-amoled-18',
|
|
20
|
+
label: 'Waveshare ESP32-S3 Touch AMOLED 1.8',
|
|
21
|
+
alias: 'amoled-18',
|
|
22
|
+
target: 'esp32-s3-touch-amoled-1.8',
|
|
23
|
+
adapter: 'esp32-idf',
|
|
24
|
+
mcu: 'ESP32-S3',
|
|
25
|
+
capabilities: {
|
|
26
|
+
display: '1.8 inch AMOLED touch panel',
|
|
27
|
+
touch: 'board integrated touch',
|
|
28
|
+
wireless: ['WiFi', 'BLE'],
|
|
29
|
+
storage: ['flash', 'PSRAM'],
|
|
30
|
+
audio: [],
|
|
31
|
+
gps: false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'lilygo-t-display-s3-long',
|
|
36
|
+
label: 'LILYGO T-Display-S3 Long',
|
|
37
|
+
alias: 't-display-long',
|
|
38
|
+
target: 'esp32-s3-lilygo-t-display-s3-long',
|
|
39
|
+
adapter: 'esp32-idf',
|
|
40
|
+
mcu: 'ESP32-S3R8',
|
|
41
|
+
capabilities: {
|
|
42
|
+
display: '3.4 inch 180x640 AMOLED (AXS15231B)',
|
|
43
|
+
touch: 'capacitive touch hardware (target driver pending)',
|
|
44
|
+
wireless: ['WiFi', 'BLE'],
|
|
45
|
+
storage: ['16 MB flash', '8 MB PSRAM'],
|
|
46
|
+
audio: [],
|
|
47
|
+
gps: false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'waveshare-amoled-175',
|
|
52
|
+
label: 'Waveshare ESP32-S3 Touch AMOLED 1.75',
|
|
53
|
+
alias: 'amoled-175',
|
|
54
|
+
target: 'esp32-s3-touch-amoled-1.75',
|
|
55
|
+
adapter: 'esp32-idf',
|
|
56
|
+
mcu: 'ESP32-S3',
|
|
57
|
+
capabilities: {
|
|
58
|
+
display: '1.75 inch AMOLED touch panel',
|
|
59
|
+
touch: 'board integrated touch',
|
|
60
|
+
wireless: ['WiFi', 'BLE'],
|
|
61
|
+
storage: ['flash', 'PSRAM'],
|
|
62
|
+
audio: [],
|
|
63
|
+
gps: false
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'esp32-s3-epaper-154',
|
|
68
|
+
label: 'ESP32-S3 e-paper 1.54',
|
|
69
|
+
alias: 'epaper',
|
|
70
|
+
target: 'esp32-s3-epaper-1.54',
|
|
71
|
+
adapter: 'esp32-idf',
|
|
72
|
+
mcu: 'ESP32-S3',
|
|
73
|
+
capabilities: {
|
|
74
|
+
display: '1.54 inch e-paper',
|
|
75
|
+
touch: 'none',
|
|
76
|
+
wireless: ['WiFi', 'BLE'],
|
|
77
|
+
storage: ['flash'],
|
|
78
|
+
audio: [],
|
|
79
|
+
gps: false
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'elecrow-rotary-21',
|
|
84
|
+
label: 'Elecrow ESP32-S3 Rotary 2.1',
|
|
85
|
+
alias: 'rotary',
|
|
86
|
+
target: 'esp32-s3-elecrow-rotary-2.1',
|
|
87
|
+
adapter: 'esp32-idf',
|
|
88
|
+
mcu: 'ESP32-S3',
|
|
89
|
+
capabilities: {
|
|
90
|
+
display: '2.1 inch round display',
|
|
91
|
+
touch: 'rotary/input board controls',
|
|
92
|
+
wireless: ['WiFi', 'BLE'],
|
|
93
|
+
storage: ['flash', 'PSRAM'],
|
|
94
|
+
audio: [],
|
|
95
|
+
gps: false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'waveshare-p4-lcd-7',
|
|
100
|
+
label: 'Waveshare ESP32-P4 Touch LCD 7',
|
|
101
|
+
alias: 'waveshare-p4-7',
|
|
102
|
+
target: 'esp32-p4-waveshare-touch-lcd-7',
|
|
103
|
+
adapter: 'esp32-idf',
|
|
104
|
+
mcu: 'ESP32-P4',
|
|
105
|
+
capabilities: {
|
|
106
|
+
display: '7 inch RGB touch LCD',
|
|
107
|
+
touch: 'board integrated touch',
|
|
108
|
+
wireless: [],
|
|
109
|
+
storage: ['flash', 'PSRAM'],
|
|
110
|
+
audio: [],
|
|
111
|
+
gps: false
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'm5stack-tab5',
|
|
116
|
+
label: 'M5Stack Tab5',
|
|
117
|
+
alias: 'm5tab',
|
|
118
|
+
target: 'esp32-p4-m5stack-tab5',
|
|
119
|
+
adapter: 'esp32-idf',
|
|
120
|
+
mcu: 'ESP32-P4 + ESP32-C6',
|
|
121
|
+
capabilities: {
|
|
122
|
+
display: '5 inch 1280x720 IPS touch panel (ST7123/ST7121 current, ILI9881C legacy)',
|
|
123
|
+
touch: 'GT911 or ST7123/ST7121 integrated touch',
|
|
124
|
+
wireless: ['ESP32-C6 WiFi 6', 'BLE/Thread/Zigbee capable'],
|
|
125
|
+
storage: ['16 MB flash', '32 MB PSRAM', 'microSD'],
|
|
126
|
+
audio: ['ES8388 codec', 'ES7210 dual-mic AEC', 'NS4150B 1W speaker', '3.5mm headphone'],
|
|
127
|
+
gps: false,
|
|
128
|
+
sensors: ['BMI270 IMU', 'RX8130CE RTC', 'INA226 power monitor', 'IP2326 charger', 'PI4IOE5V6408 IO expanders', 'SC2356 camera', 'SIT3088 RS485']
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'waveshare-p4-lcd-35',
|
|
133
|
+
label: 'Waveshare ESP32-P4 Touch LCD 3.5',
|
|
134
|
+
alias: 'waveshare-p4-3.5',
|
|
135
|
+
target: 'esp32-p4-waveshare-touch-lcd-3.5',
|
|
136
|
+
adapter: 'esp32-idf',
|
|
137
|
+
mcu: 'ESP32-P4',
|
|
138
|
+
capabilities: {
|
|
139
|
+
display: '3.5 inch RGB touch LCD',
|
|
140
|
+
touch: 'board integrated touch',
|
|
141
|
+
wireless: [],
|
|
142
|
+
storage: ['flash', 'PSRAM'],
|
|
143
|
+
audio: [],
|
|
144
|
+
gps: false
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'waveshare-rp2350-amoled-241',
|
|
149
|
+
label: 'Waveshare RP2350 Touch AMOLED 2.41',
|
|
150
|
+
alias: 'waveshare-rp2350-amoled-2.41',
|
|
151
|
+
target: 'rp2350-waveshare-touch-amoled-2.41',
|
|
152
|
+
adapter: 'rp2350-pico',
|
|
153
|
+
mcu: 'RP2350',
|
|
154
|
+
capabilities: {
|
|
155
|
+
display: '2.41 inch 450x600 AMOLED touch panel (RM690B0)',
|
|
156
|
+
touch: 'FT6336 capacitive touch',
|
|
157
|
+
wireless: [],
|
|
158
|
+
storage: ['16 MB flash', '2 MB PSRAM'],
|
|
159
|
+
audio: [],
|
|
160
|
+
gps: false,
|
|
161
|
+
sensors: ['QMI8658 IMU', 'PCF85063 RTC', 'ETA6098 power']
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'pimoroni-tufty-2350',
|
|
166
|
+
label: 'Pimoroni Tufty 2350',
|
|
167
|
+
alias: 'tufty-2350',
|
|
168
|
+
target: 'rp2350-tufty-2350',
|
|
169
|
+
adapter: 'rp2350-pico',
|
|
170
|
+
mcu: 'RP2350',
|
|
171
|
+
capabilities: {
|
|
172
|
+
display: '2.8 inch 320x240 IPS LCD (ST7789 parallel)',
|
|
173
|
+
touch: 'none',
|
|
174
|
+
wireless: ['WiFi', 'BLE'],
|
|
175
|
+
storage: ['16 MB flash', '8 MB PSRAM'],
|
|
176
|
+
audio: [],
|
|
177
|
+
gps: false,
|
|
178
|
+
sensors: ['PCF85063 RTC', 'buttons', 'battery sense', 'rear lighting']
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
export const customBoardDefaults = Object.freeze({
|
|
184
|
+
targetFamily: 'esp32',
|
|
185
|
+
adapter: 'esp32-idf',
|
|
186
|
+
baseTarget: '',
|
|
187
|
+
mcu: '',
|
|
188
|
+
display: {
|
|
189
|
+
kind: '',
|
|
190
|
+
controller: '',
|
|
191
|
+
interface: '',
|
|
192
|
+
resolution: ''
|
|
193
|
+
},
|
|
194
|
+
touch: {
|
|
195
|
+
controller: '',
|
|
196
|
+
interface: ''
|
|
197
|
+
},
|
|
198
|
+
wireless: {
|
|
199
|
+
wifi: '',
|
|
200
|
+
ble: ''
|
|
201
|
+
},
|
|
202
|
+
gps: {
|
|
203
|
+
module: '',
|
|
204
|
+
interface: ''
|
|
205
|
+
},
|
|
206
|
+
audio: {
|
|
207
|
+
codec: '',
|
|
208
|
+
output: '',
|
|
209
|
+
input: ''
|
|
210
|
+
},
|
|
211
|
+
storage: [],
|
|
212
|
+
sensors: [],
|
|
213
|
+
power: '',
|
|
214
|
+
transports: {},
|
|
215
|
+
notes: ''
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
export function boardById(id) {
|
|
219
|
+
return knownBoards.find((board) => board.id === id) || null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function boardByTarget(target) {
|
|
223
|
+
return knownBoards.find((board) => board.target === target) || null
|
|
224
|
+
}
|
package/src/context.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { option } from './args.mjs'
|
|
6
|
+
import { exists, findUp } from './fs-utils.mjs'
|
|
7
|
+
|
|
8
|
+
const srcDir = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
export const cliPackageRoot = path.resolve(srcDir, '..')
|
|
10
|
+
const requireFromCli = createRequire(import.meta.url)
|
|
11
|
+
|
|
12
|
+
export function createContext(parsed, _env = process.env, cwd = process.cwd()) {
|
|
13
|
+
const absoluteCwd = path.resolve(cwd)
|
|
14
|
+
const projectRoot = findNodeProjectRoot(absoluteCwd) || absoluteCwd
|
|
15
|
+
const initialAnchors = [projectRoot, cliPackageRoot]
|
|
16
|
+
const targetsRoot = resolveInstalledPackageDir('@geastack/targets', initialAnchors)
|
|
17
|
+
const corePackageDir = resolveInstalledPackageDir('@geastack/core', initialAnchors)
|
|
18
|
+
const packageAnchors = [projectRoot, cliPackageRoot, targetsRoot, corePackageDir].filter(Boolean)
|
|
19
|
+
const compilerPackageDir = resolveInstalledPackageDir('@geastack/compiler', packageAnchors)
|
|
20
|
+
const projectBoardsConfig = path.join(projectRoot, '.gea', 'boards.json')
|
|
21
|
+
const explicitBoardsConfig = option(parsed, 'boards-config') || ''
|
|
22
|
+
const boardsConfig = explicitBoardsConfig || (exists(projectBoardsConfig) ? projectBoardsConfig : '')
|
|
23
|
+
|
|
24
|
+
const ctx = {
|
|
25
|
+
cwd: absoluteCwd,
|
|
26
|
+
projectRoot,
|
|
27
|
+
projectBoardsConfig,
|
|
28
|
+
cliPackageRoot,
|
|
29
|
+
compilerPackageDir,
|
|
30
|
+
corePackageDir,
|
|
31
|
+
chipsPackageDir: resolveInstalledPackageDir('@geastack/chips', packageAnchors),
|
|
32
|
+
elementsPackageDir: resolveInstalledPackageDir('@geastack/elements', packageAnchors),
|
|
33
|
+
enginePackageDir: resolveInstalledPackageDir('@geastack/engine', packageAnchors),
|
|
34
|
+
geaosPackageDir: resolveInstalledPackageDir('@geastack/geaos', packageAnchors),
|
|
35
|
+
hostPackageDir: resolveInstalledPackageDir('@geastack/host', packageAnchors),
|
|
36
|
+
pluginPackageDir: resolveInstalledPackageDir('@geastack/geatsc-plugin-gea', packageAnchors),
|
|
37
|
+
boardsConfig: boardsConfig ? path.resolve(absoluteCwd, boardsConfig) : '',
|
|
38
|
+
examplesRoot: projectRoot,
|
|
39
|
+
simulatorRoot: '',
|
|
40
|
+
androidRoot: '',
|
|
41
|
+
appleRoot: '',
|
|
42
|
+
targetsRoot
|
|
43
|
+
}
|
|
44
|
+
ctx.scripts = {
|
|
45
|
+
board: packageFile(ctx.targetsRoot, 'scripts', 'board'),
|
|
46
|
+
webBuild: '',
|
|
47
|
+
webDev: '',
|
|
48
|
+
androidBuild: '',
|
|
49
|
+
macosBuild: '',
|
|
50
|
+
iosBuild: '',
|
|
51
|
+
geaEmbedded: packageFile(ctx.corePackageDir, 'bin', 'gea-embedded.mjs')
|
|
52
|
+
}
|
|
53
|
+
return ctx
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createChildEnv(ctx, env = process.env) {
|
|
57
|
+
const out = {
|
|
58
|
+
...env,
|
|
59
|
+
GEA_APPS_ROOT: ctx.projectRoot,
|
|
60
|
+
GEA_CLI_BIN: path.join(ctx.cliPackageRoot, 'bin', 'gea.mjs'),
|
|
61
|
+
GEA_CHIPS_DIR: ctx.chipsPackageDir,
|
|
62
|
+
GEA_CORE_PACKAGE: ctx.corePackageDir,
|
|
63
|
+
GEA_CORE_DIR: ctx.corePackageDir,
|
|
64
|
+
GEA_COMPILER_DIR: ctx.compilerPackageDir,
|
|
65
|
+
GEA_ELEMENTS_DIR: ctx.elementsPackageDir,
|
|
66
|
+
GEA_ENGINE_DIR: ctx.enginePackageDir,
|
|
67
|
+
GEA_EXTRA_APP_DIRS: appendPathList(env.GEA_EXTRA_APP_DIRS, ctx.projectRoot),
|
|
68
|
+
GEA_GEAOS_PACKAGE_DIR: ctx.geaosPackageDir,
|
|
69
|
+
GEA_HOST_DIR: ctx.hostPackageDir,
|
|
70
|
+
GEA_PLUGIN_DIR: ctx.pluginPackageDir,
|
|
71
|
+
GEA_TARGETS_ROOT: ctx.targetsRoot
|
|
72
|
+
}
|
|
73
|
+
if (ctx.boardsConfig) out.GEA_BOARDS_CONFIG = ctx.boardsConfig
|
|
74
|
+
for (const [name, value] of Object.entries(out)) {
|
|
75
|
+
if (name.startsWith('GEA_') && value === '') delete out[name]
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveInstalledPackageDir(packageName, anchors) {
|
|
81
|
+
for (const anchor of anchors) {
|
|
82
|
+
let current = path.resolve(anchor)
|
|
83
|
+
while (true) {
|
|
84
|
+
const candidate = path.join(current, 'node_modules', ...packageName.split('/'))
|
|
85
|
+
if (exists(path.join(candidate, 'package.json'))) return candidate
|
|
86
|
+
const parent = path.dirname(current)
|
|
87
|
+
if (parent === current) break
|
|
88
|
+
current = parent
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return path.dirname(requireFromCli.resolve(`${packageName}/package.json`))
|
|
93
|
+
} catch {
|
|
94
|
+
return ''
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function packageFile(packageRoot, ...segments) {
|
|
99
|
+
return packageRoot ? path.join(packageRoot, ...segments) : ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function appendPathList(current, value) {
|
|
103
|
+
const entries = String(current || '').split(path.delimiter).filter(Boolean)
|
|
104
|
+
if (value && !entries.includes(value)) entries.push(value)
|
|
105
|
+
return entries.join(path.delimiter)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findNodeProjectRoot(start) {
|
|
109
|
+
return findUp(start, (dir) => exists(path.join(dir, 'package.json')))
|
|
110
|
+
}
|