reset-framework-cli 0.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/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +31 -0
- package/src/commands/build.js +115 -0
- package/src/commands/dev.js +160 -0
- package/src/commands/doctor.js +89 -0
- package/src/commands/init.js +629 -0
- package/src/commands/package.js +63 -0
- package/src/index.js +214 -0
- package/src/lib/context.js +66 -0
- package/src/lib/framework.js +55 -0
- package/src/lib/logger.js +11 -0
- package/src/lib/output.js +65 -0
- package/src/lib/process.js +165 -0
- package/src/lib/project.js +357 -0
- package/src/lib/toolchain.js +62 -0
- package/src/lib/ui.js +244 -0
- package/templates/basic/README.md +15 -0
- package/templates/basic/frontend/README.md +73 -0
- package/templates/basic/frontend/eslint.config.js +23 -0
- package/templates/basic/frontend/index.html +13 -0
- package/templates/basic/frontend/package.json +31 -0
- package/templates/basic/frontend/public/favicon.svg +1 -0
- package/templates/basic/frontend/public/icons.svg +24 -0
- package/templates/basic/frontend/src/App.css +138 -0
- package/templates/basic/frontend/src/App.tsx +72 -0
- package/templates/basic/frontend/src/assets/hero.png +0 -0
- package/templates/basic/frontend/src/assets/react.svg +1 -0
- package/templates/basic/frontend/src/assets/vite.svg +1 -0
- package/templates/basic/frontend/src/index.css +111 -0
- package/templates/basic/frontend/src/lib/reset.ts +16 -0
- package/templates/basic/frontend/src/main.tsx +10 -0
- package/templates/basic/frontend/tsconfig.app.json +24 -0
- package/templates/basic/frontend/tsconfig.json +7 -0
- package/templates/basic/frontend/tsconfig.node.json +26 -0
- package/templates/basic/frontend/vite.config.ts +6 -0
- package/templates/basic/reset.config.json +29 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { run as runBuild, description as buildDescription } from "./commands/build.js"
|
|
4
|
+
import { run as runCreateApp, description as createAppDescription } from "./commands/init.js"
|
|
5
|
+
import { run as runDev, description as devDescription } from "./commands/dev.js"
|
|
6
|
+
import { run as runDoctor, description as doctorDescription } from "./commands/doctor.js"
|
|
7
|
+
import { run as runInit, description as initDescription } from "./commands/init.js"
|
|
8
|
+
import { run as runPackage, description as packageDescription } from "./commands/package.js"
|
|
9
|
+
import { createCommandContext } from "./lib/context.js"
|
|
10
|
+
import { logger } from "./lib/logger.js"
|
|
11
|
+
import {
|
|
12
|
+
printBanner,
|
|
13
|
+
printCommandList,
|
|
14
|
+
printKeyValueTable,
|
|
15
|
+
printSection
|
|
16
|
+
} from "./lib/ui.js"
|
|
17
|
+
|
|
18
|
+
const commands = {
|
|
19
|
+
build: {
|
|
20
|
+
description: buildDescription,
|
|
21
|
+
usage: "reset-framework-cli build [--skip-frontend] [--skip-runtime] [--skip-stage] [--dry-run]",
|
|
22
|
+
options: [
|
|
23
|
+
["--skip-frontend", "Skip the frontend build step"],
|
|
24
|
+
["--skip-runtime", "Skip the release runtime build"],
|
|
25
|
+
["--skip-stage", "Skip assembling the final .app bundle"],
|
|
26
|
+
["--dry-run", "Print the build plan without running commands"]
|
|
27
|
+
],
|
|
28
|
+
examples: [
|
|
29
|
+
"reset-framework-cli build",
|
|
30
|
+
"reset-framework-cli build --dry-run"
|
|
31
|
+
],
|
|
32
|
+
run: runBuild
|
|
33
|
+
},
|
|
34
|
+
"create-app": {
|
|
35
|
+
description: createAppDescription,
|
|
36
|
+
usage: "reset-framework-cli create-app <directory> [options]",
|
|
37
|
+
options: [
|
|
38
|
+
["--template <name>", "Choose a starter template"],
|
|
39
|
+
["--tailwind", "Use the Tailwind CSS starter"],
|
|
40
|
+
["--no-tailwind", "Keep the plain CSS starter"],
|
|
41
|
+
["--frontend-dir <path>", "Use a custom relative frontend directory"],
|
|
42
|
+
["--flat", "Place the frontend directly in the project root"],
|
|
43
|
+
["--product-name <name>", "Override the generated product name"],
|
|
44
|
+
["--app-id <id>", "Override the generated app identifier"],
|
|
45
|
+
["--package-manager <name>", "Choose npm, pnpm, yarn, or bun for dependency install"],
|
|
46
|
+
["--no-install", "Skip installing frontend dependencies after scaffolding"],
|
|
47
|
+
["--force", "Allow scaffolding into an existing directory"],
|
|
48
|
+
["--yes", "Skip the interactive questionnaire and use defaults"],
|
|
49
|
+
["--no-prompt", "Disable prompts even in an interactive terminal"],
|
|
50
|
+
["--dry-run", "Preview the scaffold plan without writing files"]
|
|
51
|
+
],
|
|
52
|
+
examples: [
|
|
53
|
+
"reset-framework-cli create-app my-app",
|
|
54
|
+
"reset-framework-cli create-app my-app --tailwind",
|
|
55
|
+
"reset-framework-cli create-app my-app --flat --yes"
|
|
56
|
+
],
|
|
57
|
+
run: runCreateApp
|
|
58
|
+
},
|
|
59
|
+
dev: {
|
|
60
|
+
description: devDescription,
|
|
61
|
+
usage: "reset-framework-cli dev [--skip-frontend] [--skip-runtime] [--no-open] [--dry-run]",
|
|
62
|
+
options: [
|
|
63
|
+
["--skip-frontend", "Reuse an already running frontend dev server"],
|
|
64
|
+
["--skip-runtime", "Reuse an existing native dev build"],
|
|
65
|
+
["--no-open", "Do not launch the desktop window after startup"],
|
|
66
|
+
["--dry-run", "Print the dev plan without starting processes"]
|
|
67
|
+
],
|
|
68
|
+
examples: [
|
|
69
|
+
"reset-framework-cli dev",
|
|
70
|
+
"reset-framework-cli dev --skip-runtime"
|
|
71
|
+
],
|
|
72
|
+
run: runDev
|
|
73
|
+
},
|
|
74
|
+
doctor: {
|
|
75
|
+
description: doctorDescription,
|
|
76
|
+
usage: "reset-framework-cli doctor",
|
|
77
|
+
options: [],
|
|
78
|
+
examples: ["reset-framework-cli doctor"],
|
|
79
|
+
run: runDoctor
|
|
80
|
+
},
|
|
81
|
+
init: {
|
|
82
|
+
description: `${initDescription} (alias for create-app)`,
|
|
83
|
+
usage: "reset-framework-cli init <directory> [options]",
|
|
84
|
+
options: [
|
|
85
|
+
["--template <name>", "Choose a starter template"],
|
|
86
|
+
["--tailwind", "Use the Tailwind CSS starter"],
|
|
87
|
+
["--no-tailwind", "Keep the plain CSS starter"],
|
|
88
|
+
["--frontend-dir <path>", "Use a custom relative frontend directory"],
|
|
89
|
+
["--flat", "Place the frontend directly in the project root"],
|
|
90
|
+
["--product-name <name>", "Override the generated product name"],
|
|
91
|
+
["--app-id <id>", "Override the generated app identifier"],
|
|
92
|
+
["--package-manager <name>", "Choose npm, pnpm, yarn, or bun for dependency install"],
|
|
93
|
+
["--no-install", "Skip installing frontend dependencies after scaffolding"],
|
|
94
|
+
["--force", "Allow scaffolding into an existing directory"],
|
|
95
|
+
["--yes", "Skip the interactive questionnaire and use defaults"],
|
|
96
|
+
["--no-prompt", "Disable prompts even in an interactive terminal"],
|
|
97
|
+
["--dry-run", "Preview the scaffold plan without writing files"]
|
|
98
|
+
],
|
|
99
|
+
examples: ["reset-framework-cli init my-app"],
|
|
100
|
+
run: runInit
|
|
101
|
+
},
|
|
102
|
+
package: {
|
|
103
|
+
description: packageDescription,
|
|
104
|
+
usage: "reset-framework-cli package [--dry-run]",
|
|
105
|
+
options: [
|
|
106
|
+
["--dry-run", "Preview the archive step without writing the package file"]
|
|
107
|
+
],
|
|
108
|
+
examples: [
|
|
109
|
+
"reset-framework-cli package"
|
|
110
|
+
],
|
|
111
|
+
run: runPackage
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const coreWorkflow = [
|
|
116
|
+
"reset-framework-cli create-app my-app",
|
|
117
|
+
"cd my-app",
|
|
118
|
+
"reset-framework-cli dev",
|
|
119
|
+
"reset-framework-cli build",
|
|
120
|
+
"reset-framework-cli package"
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
function getVisibleCommands() {
|
|
124
|
+
return ["create-app", "dev", "build", "package", "doctor"].map((name) => ({
|
|
125
|
+
name,
|
|
126
|
+
description: commands[name].description
|
|
127
|
+
}))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printGeneralHelp() {
|
|
131
|
+
printBanner("reset-framework-cli", "Developer-facing orchestration for Reset Framework desktop apps.")
|
|
132
|
+
|
|
133
|
+
printSection("Usage")
|
|
134
|
+
console.log(" reset-framework-cli <command> [options]")
|
|
135
|
+
console.log("")
|
|
136
|
+
|
|
137
|
+
printSection("Core workflow")
|
|
138
|
+
for (const step of coreWorkflow) {
|
|
139
|
+
console.log(` ${step}`)
|
|
140
|
+
}
|
|
141
|
+
console.log("")
|
|
142
|
+
|
|
143
|
+
printSection("Commands")
|
|
144
|
+
printCommandList(getVisibleCommands())
|
|
145
|
+
console.log("")
|
|
146
|
+
console.log(" reset-framework-cli help <command> Show command-specific help")
|
|
147
|
+
console.log("")
|
|
148
|
+
printSection("Global shortcuts")
|
|
149
|
+
printKeyValueTable([
|
|
150
|
+
["-h, --help", "Show help for the current command"],
|
|
151
|
+
["-y, --yes", "Accept defaults in create-app without prompts"]
|
|
152
|
+
])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printCommandHelp(name) {
|
|
156
|
+
const command = commands[name]
|
|
157
|
+
|
|
158
|
+
if (!command) {
|
|
159
|
+
logger.error(`Unknown command: ${name}`)
|
|
160
|
+
printGeneralHelp()
|
|
161
|
+
process.exit(1)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
printBanner(`reset-framework-cli ${name}`, command.description)
|
|
165
|
+
|
|
166
|
+
printSection("Usage")
|
|
167
|
+
console.log(` ${command.usage}`)
|
|
168
|
+
|
|
169
|
+
if (command.options.length > 0) {
|
|
170
|
+
console.log("")
|
|
171
|
+
printSection("Options")
|
|
172
|
+
printKeyValueTable(command.options)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (command.examples.length > 0) {
|
|
176
|
+
console.log("")
|
|
177
|
+
printSection("Examples")
|
|
178
|
+
for (const example of command.examples) {
|
|
179
|
+
console.log(` ${example}`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const context = createCommandContext(process.argv.slice(2))
|
|
185
|
+
|
|
186
|
+
if (context.command === "help") {
|
|
187
|
+
if (context.args[0]) {
|
|
188
|
+
printCommandHelp(context.args[0])
|
|
189
|
+
} else {
|
|
190
|
+
printGeneralHelp()
|
|
191
|
+
}
|
|
192
|
+
process.exit(0)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (context.flags.help) {
|
|
196
|
+
printCommandHelp(context.command)
|
|
197
|
+
process.exit(0)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const command = commands[context.command]
|
|
201
|
+
|
|
202
|
+
if (!command) {
|
|
203
|
+
logger.error(`Unknown command: ${context.command}`)
|
|
204
|
+
printGeneralHelp()
|
|
205
|
+
process.exit(1)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await command.run(context)
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
212
|
+
logger.error(message)
|
|
213
|
+
process.exit(1)
|
|
214
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
const shortFlagAliases = {
|
|
4
|
+
h: "help",
|
|
5
|
+
y: "yes"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseArgv(argv) {
|
|
9
|
+
const [command = "help", ...rest] = argv
|
|
10
|
+
const args = []
|
|
11
|
+
const flags = {}
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
14
|
+
const token = rest[index]
|
|
15
|
+
|
|
16
|
+
if (!token.startsWith("-")) {
|
|
17
|
+
args.push(token)
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (token.startsWith("--")) {
|
|
22
|
+
const body = token.slice(2)
|
|
23
|
+
const separatorIndex = body.indexOf("=")
|
|
24
|
+
const key = separatorIndex === -1 ? body : body.slice(0, separatorIndex)
|
|
25
|
+
const inlineValue = separatorIndex === -1 ? undefined : body.slice(separatorIndex + 1)
|
|
26
|
+
const next = rest[index + 1]
|
|
27
|
+
|
|
28
|
+
if (inlineValue !== undefined) {
|
|
29
|
+
flags[key] = inlineValue
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!next || next.startsWith("-")) {
|
|
34
|
+
flags[key] = true
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
flags[key] = next
|
|
39
|
+
index += 1
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const shortFlags = token.slice(1).split("")
|
|
44
|
+
for (const flag of shortFlags) {
|
|
45
|
+
const alias = shortFlagAliases[flag]
|
|
46
|
+
if (!alias) {
|
|
47
|
+
flags[flag] = true
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
flags[alias] = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { command, args, flags }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createCommandContext(argv, cwd = process.cwd()) {
|
|
59
|
+
const parsed = parseArgv(argv)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...parsed,
|
|
63
|
+
cwd,
|
|
64
|
+
projectRoot: path.resolve(cwd)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { runCommand } from "./process.js"
|
|
2
|
+
import { ensureVcpkgToolchain } from "./toolchain.js"
|
|
3
|
+
|
|
4
|
+
function getBuildDirectory(frameworkBuildPaths, mode) {
|
|
5
|
+
return mode === "release"
|
|
6
|
+
? frameworkBuildPaths.releaseBuildDir
|
|
7
|
+
: frameworkBuildPaths.devBuildDir
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getBuildType(mode) {
|
|
11
|
+
return mode === "release" ? "Release" : "Debug"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function configureFrameworkBuild(frameworkPaths, frameworkBuildPaths, mode, options = {}) {
|
|
15
|
+
const { dryRun = false, cwd } = options
|
|
16
|
+
const toolchainFile = await ensureVcpkgToolchain({ dryRun })
|
|
17
|
+
const buildDir = getBuildDirectory(frameworkBuildPaths, mode)
|
|
18
|
+
|
|
19
|
+
await runCommand(
|
|
20
|
+
"cmake",
|
|
21
|
+
[
|
|
22
|
+
"-S",
|
|
23
|
+
frameworkPaths.frameworkRoot,
|
|
24
|
+
"-B",
|
|
25
|
+
buildDir,
|
|
26
|
+
"-G",
|
|
27
|
+
"Ninja",
|
|
28
|
+
`-DCMAKE_BUILD_TYPE=${getBuildType(mode)}`,
|
|
29
|
+
"-DCMAKE_CXX_STANDARD=20",
|
|
30
|
+
"-DCMAKE_CXX_STANDARD_REQUIRED=ON",
|
|
31
|
+
"-DCMAKE_CXX_EXTENSIONS=OFF",
|
|
32
|
+
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
|
33
|
+
`-DCMAKE_TOOLCHAIN_FILE=${toolchainFile}`
|
|
34
|
+
],
|
|
35
|
+
{
|
|
36
|
+
cwd,
|
|
37
|
+
dryRun
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function buildFrameworkRuntime(frameworkPaths, frameworkBuildPaths, mode, options = {}) {
|
|
43
|
+
const { dryRun = false, cwd } = options
|
|
44
|
+
const buildDir = getBuildDirectory(frameworkBuildPaths, mode)
|
|
45
|
+
|
|
46
|
+
await configureFrameworkBuild(frameworkPaths, frameworkBuildPaths, mode, {
|
|
47
|
+
cwd,
|
|
48
|
+
dryRun
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await runCommand("cmake", ["--build", buildDir], {
|
|
52
|
+
cwd,
|
|
53
|
+
dryRun
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cp, mkdir, rm } from "node:fs/promises"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
3
|
+
|
|
4
|
+
import { logger } from "./logger.js"
|
|
5
|
+
import { runCommand } from "./process.js"
|
|
6
|
+
|
|
7
|
+
export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config, options = {}) {
|
|
8
|
+
const { dryRun = false, outputPaths, configPath } = options
|
|
9
|
+
|
|
10
|
+
if (!outputPaths) {
|
|
11
|
+
throw new Error("stageMacOSAppBundle requires resolved output paths")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!existsSync(frameworkBuildPaths.releaseAppTemplate) && !dryRun) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Missing runtime app bundle at ${frameworkBuildPaths.releaseAppTemplate}. Run reset-framework-cli build first.`
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Missing frontend build output at ${outputPaths.frontendEntryFile}. Run the frontend build first.`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.info(`Staging app bundle into ${outputPaths.appBundlePath}`)
|
|
27
|
+
|
|
28
|
+
if (dryRun) {
|
|
29
|
+
logger.info(`- copy ${frameworkBuildPaths.releaseAppTemplate} -> ${outputPaths.appBundlePath}`)
|
|
30
|
+
logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
|
|
31
|
+
logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
|
|
32
|
+
return outputPaths
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await mkdir(outputPaths.macosDir, { recursive: true })
|
|
36
|
+
await rm(outputPaths.appBundlePath, { recursive: true, force: true })
|
|
37
|
+
await cp(frameworkBuildPaths.releaseAppTemplate, outputPaths.appBundlePath, { recursive: true })
|
|
38
|
+
|
|
39
|
+
await mkdir(outputPaths.resourcesDir, { recursive: true })
|
|
40
|
+
await cp(configPath, outputPaths.bundledConfigPath, { force: true })
|
|
41
|
+
await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
|
|
42
|
+
await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
|
|
43
|
+
|
|
44
|
+
return outputPaths
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function createMacOSZipArchive(outputPaths, options = {}) {
|
|
48
|
+
const { dryRun = false } = options
|
|
49
|
+
|
|
50
|
+
logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
|
|
51
|
+
|
|
52
|
+
if (process.platform !== "darwin") {
|
|
53
|
+
throw new Error("Packaging is only implemented for macOS right now")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!dryRun) {
|
|
57
|
+
await mkdir(outputPaths.packagesDir, { recursive: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await runCommand(
|
|
61
|
+
"ditto",
|
|
62
|
+
["-c", "-k", "--sequesterRsrc", "--keepParent", outputPaths.appBundlePath, outputPaths.zipPath],
|
|
63
|
+
{ dryRun }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
import { logger } from "./logger.js"
|
|
4
|
+
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
setTimeout(resolve, ms)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveNpmCommand() {
|
|
12
|
+
return process.platform === "win32" ? "npm.cmd" : "npm"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolvePackageManagerCommand(packageManager = "npm") {
|
|
16
|
+
if (packageManager === "npm") {
|
|
17
|
+
return resolveNpmCommand()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (packageManager === "yarn") {
|
|
21
|
+
return process.platform === "win32" ? "yarn.cmd" : "yarn"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (packageManager === "pnpm") {
|
|
25
|
+
return process.platform === "win32" ? "pnpm.cmd" : "pnpm"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (packageManager === "bun") {
|
|
29
|
+
return process.platform === "win32" ? "bun.exe" : "bun"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(`Unsupported package manager: ${packageManager}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getInstallCommandArgs(packageManager = "npm") {
|
|
36
|
+
if (packageManager === "npm") {
|
|
37
|
+
return ["install", "--include=optional"]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (packageManager === "yarn") {
|
|
41
|
+
return ["install"]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ["install"]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatCommand(command, args = []) {
|
|
48
|
+
return [command, ...args].join(" ")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runCommand(command, args = [], options = {}) {
|
|
52
|
+
const { cwd, dryRun = false, env } = options
|
|
53
|
+
|
|
54
|
+
logger.info(`$ ${formatCommand(command, args)}`)
|
|
55
|
+
|
|
56
|
+
if (dryRun) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
const child = spawn(command, args, {
|
|
62
|
+
cwd,
|
|
63
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
64
|
+
stdio: "inherit"
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
child.once("error", reject)
|
|
68
|
+
child.once("exit", (code, signal) => {
|
|
69
|
+
if (code === 0) {
|
|
70
|
+
resolve(undefined)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
reject(
|
|
75
|
+
new Error(
|
|
76
|
+
signal
|
|
77
|
+
? `Command terminated by signal: ${signal}`
|
|
78
|
+
: `Command exited with code ${code}`
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function spawnCommand(command, args = [], options = {}) {
|
|
86
|
+
const { cwd, dryRun = false, env } = options
|
|
87
|
+
|
|
88
|
+
logger.info(`$ ${formatCommand(command, args)}`)
|
|
89
|
+
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return spawn(command, args, {
|
|
95
|
+
cwd,
|
|
96
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
97
|
+
stdio: "inherit"
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function waitForProcessExit(child, label) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
child.once("error", reject)
|
|
104
|
+
child.once("exit", (code, signal) => {
|
|
105
|
+
if (code === 0 || signal === "SIGINT" || signal === "SIGTERM") {
|
|
106
|
+
resolve(undefined)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
reject(
|
|
111
|
+
new Error(
|
|
112
|
+
signal
|
|
113
|
+
? `${label} terminated by signal: ${signal}`
|
|
114
|
+
: `${label} exited with code ${code}`
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function registerChildCleanup(children) {
|
|
122
|
+
const activeChildren = children.filter(Boolean)
|
|
123
|
+
|
|
124
|
+
const cleanup = () => {
|
|
125
|
+
for (const child of activeChildren) {
|
|
126
|
+
if (child.exitCode === null && !child.killed) {
|
|
127
|
+
child.kill("SIGTERM")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const handleSignal = () => {
|
|
133
|
+
cleanup()
|
|
134
|
+
process.exit(0)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.on("SIGINT", handleSignal)
|
|
138
|
+
process.on("SIGTERM", handleSignal)
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
process.off("SIGINT", handleSignal)
|
|
142
|
+
process.off("SIGTERM", handleSignal)
|
|
143
|
+
cleanup()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function waitForUrl(url, options = {}) {
|
|
148
|
+
const { intervalMs = 250, timeoutMs = 15000 } = options
|
|
149
|
+
const deadline = Date.now() + timeoutMs
|
|
150
|
+
|
|
151
|
+
while (Date.now() < deadline) {
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(url)
|
|
154
|
+
if (response.status < 500) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Retry until timeout.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await sleep(intervalMs)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error(`Timed out waiting for ${url}`)
|
|
165
|
+
}
|