@flare-desktop/cli 0.0.0-stage → 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 +21 -0
- package/README.md +76 -2
- package/bin/flare.js +4 -0
- package/package.json +42 -4
- package/src/cli.js +93 -0
- package/src/commands/build.js +37 -0
- package/src/commands/bundle.js +44 -0
- package/src/commands/completions.js +45 -0
- package/src/commands/dev.js +91 -0
- package/src/commands/doctor.js +88 -0
- package/src/commands/icon.js +31 -0
- package/src/commands/info.js +65 -0
- package/src/commands/init.js +108 -0
- package/src/commands/inspect.js +35 -0
- package/src/help.js +32 -0
- package/src/index.js +17 -0
- package/src/lib/args.js +65 -0
- package/src/lib/build.js +29 -0
- package/src/lib/cmake.js +102 -0
- package/src/lib/commands.js +15 -0
- package/src/lib/errors.js +7 -0
- package/src/lib/icons.js +127 -0
- package/src/lib/logger.js +19 -0
- package/src/lib/packaging/common.js +131 -0
- package/src/lib/packaging/index.js +228 -0
- package/src/lib/packaging/linux.js +283 -0
- package/src/lib/packaging/macos.js +84 -0
- package/src/lib/packaging/msi.js +143 -0
- package/src/lib/packaging/nsis.js +106 -0
- package/src/lib/packaging/staging.js +123 -0
- package/src/lib/packaging/tools.js +75 -0
- package/src/lib/png.js +288 -0
- package/src/lib/process.js +57 -0
- package/src/lib/project.js +285 -0
- package/src/schema.js +2 -0
- package/src/templates/project.js +284 -0
- package/src/version.js +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flare contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flare-desktop/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The command line interface of [flare](https://github.com/flare-desktop/flare), a C++20 desktop toolkit that renders
|
|
4
|
+
an HTML frontend in the system webview.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install --save-dev @flare-desktop/cli
|
|
8
|
+
npx flare --help
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Create an application:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create flare-desktop@latest hello
|
|
15
|
+
cd hello
|
|
16
|
+
npm install
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Command | Effect |
|
|
21
|
+
|:--|:--|
|
|
22
|
+
| `flare init [name]` | scaffold a project: `flare.config.json`, `package.json`, CMake, C++ entry point and `frontend/` |
|
|
23
|
+
| `flare dev` | build the debug binary and run it with `FLARE_DEV=1` |
|
|
24
|
+
| `flare build [--debug]` | build the project; release by default |
|
|
25
|
+
| `flare bundle` | build a release binary and native packages under `target/bundle` |
|
|
26
|
+
| `flare icon <source.png>` | write the png, ico and icns icon set |
|
|
27
|
+
| `flare info` | version, platform, toolchain and project information |
|
|
28
|
+
| `flare inspect` | list the resolved configuration layers and the merged configuration |
|
|
29
|
+
| `flare doctor` | check Node, CMake, the compiler, git and the configuration |
|
|
30
|
+
| `flare completions <shell>` | bash, zsh, fish or powershell completions |
|
|
31
|
+
|
|
32
|
+
Global options: `--project=<path>`, `--config=<path>`, `--environment=<name>`, `--json`, `--verbose`.
|
|
33
|
+
|
|
34
|
+
## Frontend directory
|
|
35
|
+
|
|
36
|
+
`flare build` passes `build.frontendDist` to CMake as `FLARE_FRONTEND_DIR`. The scaffolded `CMakeLists.txt` uses
|
|
37
|
+
that variable for `FlareEmbedFrontend` and for the development directory. Change the config key, not the CMake
|
|
38
|
+
file:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"build": {
|
|
43
|
+
"frontendDist": "web"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Native packaging
|
|
49
|
+
|
|
50
|
+
`flare bundle` performs a Release build and writes real packages. `--bundles` overrides `bundle.targets`:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx flare bundle
|
|
54
|
+
npx flare bundle --bundles=nsis
|
|
55
|
+
npx flare bundle --bundles=nsis,msi --out=release
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Target | Platform | Artifact | Required tool |
|
|
59
|
+
|:--|:--|:--|:--|
|
|
60
|
+
| `nsis` | Windows | `.exe` installer | `makensis` (NSIS 3) |
|
|
61
|
+
| `msi` | Windows | `.msi` installer | `candle.exe`, `light.exe` (WiX 3.14) |
|
|
62
|
+
| `deb` | Linux | `.deb` package | `dpkg-deb` |
|
|
63
|
+
| `rpm` | Linux | `.rpm` package | `rpmbuild` |
|
|
64
|
+
| `appimage` | Linux | `.AppImage` | `appimagetool` |
|
|
65
|
+
| `app` | macOS | `.app` bundle | none |
|
|
66
|
+
| `dmg` | macOS | `.dmg` disk image | `hdiutil` |
|
|
67
|
+
|
|
68
|
+
With an empty `bundle.targets`, the targets of the current platform are used. Each artifact is written to
|
|
69
|
+
`target/bundle/<target>/`. The command fails before compiling when a required tool is missing, and the error names
|
|
70
|
+
the tool and the expected installation.
|
|
71
|
+
|
|
72
|
+
Packaging uses `bundle.icon` (a square png), `bundle.licenseFile`, `bundle.resources`, `bundle.externalBin`,
|
|
73
|
+
`bundle.publisher`, `bundle.homepage` and `bundle.shortDescription` when they are present. Resources and external
|
|
74
|
+
binaries are staged next to the application binary.
|
|
75
|
+
|
|
76
|
+
The configuration is validated against
|
|
77
|
+
[`schemas/flare.config.schema.json`](https://raw.githubusercontent.com/flare-desktop/flare/main/schemas/flare.config.schema.json).
|
package/bin/flare.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flare-desktop/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command line interface for the flare desktop toolkit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flare": "bin/flare.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./schema": "./src/schema.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"flare",
|
|
21
|
+
"desktop",
|
|
22
|
+
"webview",
|
|
23
|
+
"cpp",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/flare-desktop/flare-cli.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://flare-desktop.github.io/",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/flare-desktop/flare/issues"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test \"test/*.test.js\""
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"ajv": "^8.20.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { buildCommand } from "./commands/build.js";
|
|
4
|
+
import { bundleCommand } from "./commands/bundle.js";
|
|
5
|
+
import { completionsCommand } from "./commands/completions.js";
|
|
6
|
+
import { devCommand } from "./commands/dev.js";
|
|
7
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
8
|
+
import { iconCommand } from "./commands/icon.js";
|
|
9
|
+
import { infoCommand } from "./commands/info.js";
|
|
10
|
+
import { initProject } from "./commands/init.js";
|
|
11
|
+
import { inspectCommand } from "./commands/inspect.js";
|
|
12
|
+
import { parseArguments } from "./lib/args.js";
|
|
13
|
+
import { FlareError } from "./lib/errors.js";
|
|
14
|
+
import { createLogger } from "./lib/logger.js";
|
|
15
|
+
import { helpText } from "./help.js";
|
|
16
|
+
import { VERSION } from "./version.js";
|
|
17
|
+
|
|
18
|
+
export { createLogger };
|
|
19
|
+
|
|
20
|
+
async function initCommand(context) {
|
|
21
|
+
await initProject({
|
|
22
|
+
name: context.positionals[0] ?? "flare-app",
|
|
23
|
+
baseDirectory: context.projectDirectory,
|
|
24
|
+
identifier: context.flags.identifier,
|
|
25
|
+
appVersion: context.flags.appVersion ?? "0.1.0",
|
|
26
|
+
force: Boolean(context.flags.force),
|
|
27
|
+
logger: context.logger
|
|
28
|
+
});
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handlers = {
|
|
33
|
+
init: initCommand,
|
|
34
|
+
dev: devCommand,
|
|
35
|
+
build: buildCommand,
|
|
36
|
+
bundle: bundleCommand,
|
|
37
|
+
icon: iconCommand,
|
|
38
|
+
info: infoCommand,
|
|
39
|
+
inspect: inspectCommand,
|
|
40
|
+
doctor: doctorCommand,
|
|
41
|
+
completions: completionsCommand,
|
|
42
|
+
version: (context) => {
|
|
43
|
+
context.logger.info(VERSION);
|
|
44
|
+
return 0;
|
|
45
|
+
},
|
|
46
|
+
help: (context) => {
|
|
47
|
+
context.logger.info(helpText().trimEnd());
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export async function run(argv = []) {
|
|
53
|
+
const { flags, positionals } = parseArguments(argv);
|
|
54
|
+
const logger = createLogger({ verbose: Boolean(flags.verbose) });
|
|
55
|
+
|
|
56
|
+
if (flags.version) {
|
|
57
|
+
logger.info(VERSION);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (flags.help) {
|
|
62
|
+
logger.info(helpText().trimEnd());
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const commandName = positionals.shift() ?? "help";
|
|
67
|
+
const handler = handlers[commandName];
|
|
68
|
+
if (!handler) {
|
|
69
|
+
logger.error(`unknown command '${commandName}'`);
|
|
70
|
+
logger.info("run 'flare help' to list the commands");
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
flags.environment ??= commandName === "dev" ? "development" : "production";
|
|
75
|
+
|
|
76
|
+
const context = {
|
|
77
|
+
projectDirectory: path.resolve(flags.project ?? "."),
|
|
78
|
+
flags,
|
|
79
|
+
positionals,
|
|
80
|
+
logger
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return await handler(context);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof FlareError) {
|
|
87
|
+
logger.error(error.message);
|
|
88
|
+
return error.exitCode;
|
|
89
|
+
}
|
|
90
|
+
logger.error(error?.stack ?? String(error));
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { compileApplication, runHook } from "../lib/build.js";
|
|
2
|
+
import { loadProject, productName } from "../lib/project.js";
|
|
3
|
+
|
|
4
|
+
export async function buildCommand(context) {
|
|
5
|
+
const { config, files } = loadProject({
|
|
6
|
+
projectDirectory: context.projectDirectory,
|
|
7
|
+
configFile: context.flags.config,
|
|
8
|
+
environment: context.flags.environment
|
|
9
|
+
});
|
|
10
|
+
const mode = context.flags.debug ? "Debug" : "Release";
|
|
11
|
+
|
|
12
|
+
context.logger.debug(`configuration layers: ${files.join(", ")}`);
|
|
13
|
+
context.logger.info(`building '${productName(config)}' ${config.app.version} (${mode.toLowerCase()})`);
|
|
14
|
+
|
|
15
|
+
await runHook(config.build.beforeBuildCommand, {
|
|
16
|
+
cwd: context.projectDirectory,
|
|
17
|
+
logger: context.logger,
|
|
18
|
+
label: "before build command"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { binary, directory } = await compileApplication({
|
|
22
|
+
projectDirectory: context.projectDirectory,
|
|
23
|
+
config,
|
|
24
|
+
mode,
|
|
25
|
+
logger: context.logger
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await runHook(config.build.afterBuildCommand, {
|
|
29
|
+
cwd: context.projectDirectory,
|
|
30
|
+
logger: context.logger,
|
|
31
|
+
label: "after build command"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
context.logger.info(`built '${productName(config)}' into '${directory}'`);
|
|
35
|
+
context.logger.step(`binary ${binary}`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { compileApplication, runHook } from "../lib/build.js";
|
|
2
|
+
import { assertBundleTools, bundleTargets, createBundles } from "../lib/packaging/index.js";
|
|
3
|
+
import { loadProject } from "../lib/project.js";
|
|
4
|
+
|
|
5
|
+
export async function bundleCommand(context) {
|
|
6
|
+
const { config } = loadProject({
|
|
7
|
+
projectDirectory: context.projectDirectory,
|
|
8
|
+
configFile: context.flags.config,
|
|
9
|
+
environment: context.flags.environment
|
|
10
|
+
});
|
|
11
|
+
const targets = bundleTargets(config, context.flags.bundles);
|
|
12
|
+
|
|
13
|
+
assertBundleTools(targets, config);
|
|
14
|
+
|
|
15
|
+
await runHook(config.build.beforeBuildCommand, {
|
|
16
|
+
cwd: context.projectDirectory,
|
|
17
|
+
logger: context.logger,
|
|
18
|
+
label: "before build command"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { binary } = await compileApplication({
|
|
22
|
+
projectDirectory: context.projectDirectory,
|
|
23
|
+
config,
|
|
24
|
+
mode: "Release",
|
|
25
|
+
logger: context.logger
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await runHook(config.build.afterBuildCommand, {
|
|
29
|
+
cwd: context.projectDirectory,
|
|
30
|
+
logger: context.logger,
|
|
31
|
+
label: "after build command"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await createBundles({
|
|
35
|
+
config,
|
|
36
|
+
projectDirectory: context.projectDirectory,
|
|
37
|
+
binary,
|
|
38
|
+
targets,
|
|
39
|
+
output: context.flags.out,
|
|
40
|
+
logger: context.logger
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { COMMAND_NAMES } from "../lib/commands.js";
|
|
2
|
+
import { FlareError } from "../lib/errors.js";
|
|
3
|
+
|
|
4
|
+
const FLAGS = ["--project", "--config", "--environment", "--json", "--verbose", "--help", "--version"];
|
|
5
|
+
|
|
6
|
+
export async function completionsCommand(context) {
|
|
7
|
+
const shell = context.positionals[0] ?? "bash";
|
|
8
|
+
const commands = COMMAND_NAMES.join(" ");
|
|
9
|
+
|
|
10
|
+
if (shell === "bash") {
|
|
11
|
+
context.logger.info(`_flare_complete() {
|
|
12
|
+
local current="\${COMP_WORDS[COMP_CWORD]}"
|
|
13
|
+
COMPREPLY=( $(compgen -W "${commands} ${FLAGS.join(" ")}" -- "$current") )
|
|
14
|
+
}
|
|
15
|
+
complete -F _flare_complete flare`);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (shell === "zsh") {
|
|
20
|
+
context.logger.info(`#compdef flare
|
|
21
|
+
|
|
22
|
+
_arguments '1:command:(${commands})'`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (shell === "fish") {
|
|
27
|
+
for (const command of COMMAND_NAMES) {
|
|
28
|
+
context.logger.info(`complete -c flare -f -a "${command}"`);
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (shell === "powershell") {
|
|
34
|
+
const list = [...COMMAND_NAMES, ...FLAGS].map((value) => `'${value}'`).join(", ");
|
|
35
|
+
context.logger.info(`Register-ArgumentCompleter -Native -CommandName flare -ScriptBlock {
|
|
36
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
37
|
+
${list} | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
38
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
39
|
+
}
|
|
40
|
+
}`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new FlareError(`unsupported shell '${shell}'`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
import { compileApplication, runHook } from "../lib/build.js";
|
|
4
|
+
import { FlareError } from "../lib/errors.js";
|
|
5
|
+
import { loadProject, productName } from "../lib/project.js";
|
|
6
|
+
import { run, startShell, terminate } from "../lib/process.js";
|
|
7
|
+
|
|
8
|
+
async function waitForUrl(url, { timeout, logger }) {
|
|
9
|
+
const deadline = Date.now() + timeout;
|
|
10
|
+
let attempts = 0;
|
|
11
|
+
while (Date.now() < deadline) {
|
|
12
|
+
attempts += 1;
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
15
|
+
if (response.status < 500) return;
|
|
16
|
+
} catch {
|
|
17
|
+
logger.debug(`waiting for ${url} (attempt ${attempts})`);
|
|
18
|
+
}
|
|
19
|
+
await delay(250);
|
|
20
|
+
}
|
|
21
|
+
throw new FlareError(`development server at '${url}' did not answer within ${Math.round(timeout / 1000)}s`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function devCommand(context) {
|
|
25
|
+
const { config, files } = loadProject({
|
|
26
|
+
projectDirectory: context.projectDirectory,
|
|
27
|
+
configFile: context.flags.config,
|
|
28
|
+
environment: context.flags.environment
|
|
29
|
+
});
|
|
30
|
+
const timeout = Number(context.flags.devTimeout ?? 60000);
|
|
31
|
+
|
|
32
|
+
context.logger.info(`development mode for '${productName(config)}'`);
|
|
33
|
+
context.logger.debug(`configuration layers: ${files.join(", ")}`);
|
|
34
|
+
|
|
35
|
+
let server = null;
|
|
36
|
+
let application = null;
|
|
37
|
+
|
|
38
|
+
const stopServer = () => {
|
|
39
|
+
if (server) terminate(server);
|
|
40
|
+
};
|
|
41
|
+
const stopOnSignal = (code) => () => {
|
|
42
|
+
stopServer();
|
|
43
|
+
process.exit(code);
|
|
44
|
+
};
|
|
45
|
+
const sigintHandler = stopOnSignal(130);
|
|
46
|
+
const sigtermHandler = stopOnSignal(143);
|
|
47
|
+
process.once("SIGINT", sigintHandler);
|
|
48
|
+
process.once("SIGTERM", sigtermHandler);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (config.build.beforeDevCommand) {
|
|
52
|
+
context.logger.info(`before dev command: ${config.build.beforeDevCommand}`);
|
|
53
|
+
server = startShell(config.build.beforeDevCommand, { cwd: context.projectDirectory });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.build.devUrl) {
|
|
57
|
+
context.logger.info(`waiting for ${config.build.devUrl}`);
|
|
58
|
+
await waitForUrl(config.build.devUrl, { timeout, logger: context.logger });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { binary } = await compileApplication({
|
|
62
|
+
projectDirectory: context.projectDirectory,
|
|
63
|
+
config,
|
|
64
|
+
mode: "Debug",
|
|
65
|
+
logger: context.logger
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const env = { FLARE_DEV: "1" };
|
|
69
|
+
if (config.build.devUrl) env.FLARE_DEV_URL = config.build.devUrl;
|
|
70
|
+
|
|
71
|
+
context.logger.info(`running ${binary}`);
|
|
72
|
+
application = await run(binary, [], { cwd: context.projectDirectory, env });
|
|
73
|
+
} finally {
|
|
74
|
+
stopServer();
|
|
75
|
+
process.off("SIGINT", sigintHandler);
|
|
76
|
+
process.off("SIGTERM", sigtermHandler);
|
|
77
|
+
if (application && config.build.afterDevCommand) {
|
|
78
|
+
await runHook(config.build.afterDevCommand, {
|
|
79
|
+
cwd: context.projectDirectory,
|
|
80
|
+
logger: context.logger,
|
|
81
|
+
label: "after dev command"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!application) return 0;
|
|
87
|
+
if (application.code !== 0) {
|
|
88
|
+
context.logger.warn(`the application exited with code ${application.code}`);
|
|
89
|
+
}
|
|
90
|
+
return application.code;
|
|
91
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { cmakeVersion, compilerVersion, ninjaVersion } from "../lib/cmake.js";
|
|
4
|
+
import { frontendDirectory, loadProject, platformDisplayName, platformName } from "../lib/project.js";
|
|
5
|
+
import { capture, firstLine } from "../lib/process.js";
|
|
6
|
+
|
|
7
|
+
function gitVersion() {
|
|
8
|
+
const result = capture("git", ["--version"]);
|
|
9
|
+
if (result.error || result.status !== 0) return null;
|
|
10
|
+
return firstLine(result.stdout);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hintsFor(platform) {
|
|
14
|
+
if (platform === "linux") {
|
|
15
|
+
return [
|
|
16
|
+
"install libwebkit2gtk-4.1-dev, libgtk-3-dev and build-essential for the native webview",
|
|
17
|
+
"libwebkit2gtk-4.0-dev works too when the distribution does not ship 4.1"
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
if (platform === "windows") {
|
|
21
|
+
return [
|
|
22
|
+
"the WebView2 runtime ships with Windows 11 and current Windows 10 updates",
|
|
23
|
+
"install the Visual Studio Build Tools with the desktop C++ workload"
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
if (platform === "macos") {
|
|
27
|
+
return ["WKWebView ships with macOS; install the Xcode command line tools for the compiler"];
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function doctorCommand(context) {
|
|
33
|
+
const checks = [];
|
|
34
|
+
const add = (status, label, detail) => checks.push({ status, label, detail });
|
|
35
|
+
const platform = platformName();
|
|
36
|
+
|
|
37
|
+
add("ok", "platform", platformDisplayName());
|
|
38
|
+
|
|
39
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
40
|
+
add(nodeMajor >= 18 ? "ok" : "fail", "node", `${process.version}${nodeMajor >= 18 ? "" : " (18 or newer required)"}`);
|
|
41
|
+
|
|
42
|
+
const cmake = cmakeVersion();
|
|
43
|
+
add(cmake ? "ok" : "fail", "cmake", cmake ?? "not found");
|
|
44
|
+
|
|
45
|
+
const compiler = compilerVersion();
|
|
46
|
+
add(compiler ? "ok" : "warn", "compiler", compiler ?? "not found");
|
|
47
|
+
|
|
48
|
+
const ninja = ninjaVersion();
|
|
49
|
+
add(ninja ? "ok" : "warn", "ninja", ninja ?? "not found, the default cmake generator is used");
|
|
50
|
+
|
|
51
|
+
const git = gitVersion();
|
|
52
|
+
add(git ? "ok" : "warn", "git", git ?? "not found, FetchContent cannot clone flare");
|
|
53
|
+
|
|
54
|
+
let config = null;
|
|
55
|
+
try {
|
|
56
|
+
const project = loadProject({
|
|
57
|
+
projectDirectory: context.projectDirectory,
|
|
58
|
+
configFile: context.flags.config,
|
|
59
|
+
environment: context.flags.environment
|
|
60
|
+
});
|
|
61
|
+
config = project.config;
|
|
62
|
+
add("ok", "configuration", project.configPath);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
add("fail", "configuration", error.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (config) {
|
|
68
|
+
const directory = frontendDirectory(context.projectDirectory, config);
|
|
69
|
+
add(existsSync(directory) ? "ok" : "warn", "frontend", directory);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hints = hintsFor(platform);
|
|
73
|
+
|
|
74
|
+
if (context.flags.json) {
|
|
75
|
+
context.logger.info(JSON.stringify({ platform, checks, hints }, null, 2));
|
|
76
|
+
} else {
|
|
77
|
+
context.logger.info("flare doctor");
|
|
78
|
+
for (const check of checks) {
|
|
79
|
+
context.logger.info(` ${check.status.padEnd(5)}${check.label.padEnd(16)}${check.detail}`);
|
|
80
|
+
}
|
|
81
|
+
if (hints.length > 0) {
|
|
82
|
+
context.logger.info(" hints:");
|
|
83
|
+
for (const hint of hints) context.logger.info(` ${hint}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return checks.some((check) => check.status === "fail") ? 1 : 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { FlareError } from "../lib/errors.js";
|
|
5
|
+
import { buildIconSet } from "../lib/icons.js";
|
|
6
|
+
import { readPng } from "../lib/png.js";
|
|
7
|
+
|
|
8
|
+
export async function iconCommand(context) {
|
|
9
|
+
const source = context.positionals[0];
|
|
10
|
+
if (!source) throw new FlareError("usage: flare icon <source.png>");
|
|
11
|
+
|
|
12
|
+
const sourcePath = path.resolve(context.projectDirectory, source);
|
|
13
|
+
if (!existsSync(sourcePath)) throw new FlareError(`'${sourcePath}' does not exist`);
|
|
14
|
+
if (path.extname(sourcePath).toLowerCase() !== ".png") {
|
|
15
|
+
throw new FlareError("only png sources are supported; export the image as png first");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const image = readPng(readFileSync(sourcePath));
|
|
19
|
+
const files = buildIconSet(image);
|
|
20
|
+
const output = path.resolve(context.projectDirectory, context.flags.out || "icons");
|
|
21
|
+
mkdirSync(output, { recursive: true });
|
|
22
|
+
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const target = path.join(output, file.name);
|
|
25
|
+
writeFileSync(target, file.data);
|
|
26
|
+
context.logger.step(`wrote ${target}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
context.logger.info(`generated ${files.length} icon(s) from '${sourcePath}'`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { architectureName, loadProject, platformDisplayName } from "../lib/project.js";
|
|
5
|
+
import { buildDirectory, cmakeVersion, compilerVersion, executableNames } from "../lib/cmake.js";
|
|
6
|
+
import { VERSION } from "../version.js";
|
|
7
|
+
|
|
8
|
+
function pad(label) {
|
|
9
|
+
return label.padEnd(18);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function infoCommand(context) {
|
|
13
|
+
const { config, files } = loadProject({
|
|
14
|
+
projectDirectory: context.projectDirectory,
|
|
15
|
+
configFile: context.flags.config,
|
|
16
|
+
environment: context.flags.environment
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const binary = path.join(
|
|
20
|
+
buildDirectory(context.projectDirectory, "Release"),
|
|
21
|
+
"bin",
|
|
22
|
+
executableNames(config.app.mainBinaryName || config.app.productName || "flare-app")[0]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const report = {
|
|
26
|
+
flare: VERSION,
|
|
27
|
+
node: process.versions.node,
|
|
28
|
+
platform: platformDisplayName(),
|
|
29
|
+
architecture: architectureName(),
|
|
30
|
+
cmake: cmakeVersion(),
|
|
31
|
+
compiler: compilerVersion(),
|
|
32
|
+
project: context.projectDirectory,
|
|
33
|
+
configuration: files,
|
|
34
|
+
productName: config.app.productName,
|
|
35
|
+
identifier: config.app.identifier,
|
|
36
|
+
version: config.app.version,
|
|
37
|
+
windows: config.windows.length,
|
|
38
|
+
frontendDist: path.resolve(context.projectDirectory, config.build.frontendDist || "frontend"),
|
|
39
|
+
bundleTargets: config.bundle.targets,
|
|
40
|
+
binary: existsSync(binary) ? binary : null
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (context.flags.json) {
|
|
44
|
+
context.logger.info(JSON.stringify(report, null, 2));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
context.logger.info(`${pad("flare")}${VERSION}`);
|
|
49
|
+
context.logger.info(`${pad("node")}${process.versions.node}`);
|
|
50
|
+
context.logger.info(`${pad("platform")}${platformDisplayName()}`);
|
|
51
|
+
context.logger.info(`${pad("architecture")}${architectureName()}`);
|
|
52
|
+
context.logger.info(`${pad("cmake")}${report.cmake ?? "not found"}`);
|
|
53
|
+
context.logger.info(`${pad("compiler")}${report.compiler ?? "not found"}`);
|
|
54
|
+
context.logger.info(`${pad("project")}${context.projectDirectory}`);
|
|
55
|
+
context.logger.info(`${pad("product name")}${config.app.productName}`);
|
|
56
|
+
context.logger.info(`${pad("identifier")}${config.app.identifier}`);
|
|
57
|
+
context.logger.info(`${pad("version")}${config.app.version}`);
|
|
58
|
+
context.logger.info(`${pad("windows")}${config.windows.length}`);
|
|
59
|
+
context.logger.info(`${pad("frontend")}${report.frontendDist}`);
|
|
60
|
+
context.logger.info(
|
|
61
|
+
`${pad("bundle targets")}${config.bundle.targets.length > 0 ? config.bundle.targets.join(", ") : "platform defaults"}`
|
|
62
|
+
);
|
|
63
|
+
if (report.binary) context.logger.info(`${pad("binary")}${report.binary}`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|