@flare-desktop/cli 0.1.1 → 0.1.3

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,181 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ import { loadProject, mergeConfig, parseConfig } from "./project.js";
5
+
6
+ function cppString(value) {
7
+ const bytes = Buffer.from(value, "utf8");
8
+ let output = '"';
9
+ for (const byte of bytes) {
10
+ if (byte === 34) output += '\\"';
11
+ else if (byte === 92) output += "\\\\";
12
+ else if (byte >= 32 && byte <= 126) output += String.fromCharCode(byte);
13
+ else output += `\\${byte.toString(8).padStart(3, "0")}`;
14
+ }
15
+ return `${output}"`;
16
+ }
17
+
18
+ function cppJson(value) {
19
+ if (value === null) return "Flare::JSON(nullptr)";
20
+ if (Array.isArray(value)) return `Flare::JSON::array({${value.map(cppJson).join(", ")}})`;
21
+ if (typeof value === "object") {
22
+ const entries = Object.entries(value).map(([key, item]) => `{${cppString(key)}, ${cppJson(item)}}`);
23
+ return `Flare::JSON::object({${entries.join(", ")}})`;
24
+ }
25
+ if (typeof value === "string") return `Flare::JSON(${cppString(value)})`;
26
+ if (typeof value === "boolean") return `Flare::JSON(${value})`;
27
+ if (typeof value === "number" && Number.isFinite(value)) return `Flare::JSON(${value})`;
28
+ throw new TypeError("unsupported JSON value in configuration");
29
+ }
30
+
31
+ function scalar(value, kind) {
32
+ if (kind === "string" && typeof value === "string") return cppString(value);
33
+ if (kind === "bool" && typeof value === "boolean") return `${value}`;
34
+ if (kind === "number" && typeof value === "number" && Number.isFinite(value)) return `${value}`;
35
+ if (kind === "int" && Number.isSafeInteger(value)) return `${value}`;
36
+ if (kind === "strings" && Array.isArray(value)) {
37
+ return `{${value.filter((item) => typeof item === "string").map(cppString).join(", ")}}`;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ const FIELDS = {
43
+ package: { productName: "string", version: "string" },
44
+ app: { productName: "string", mainBinaryName: "string", version: "string", identifier: "string" },
45
+ build: {
46
+ beforeDevCommand: "string", afterDevCommand: "string", beforeBuildCommand: "string",
47
+ afterBuildCommand: "string", frontendDist: "string", devUrl: "string", removeUnusedCommands: "strings"
48
+ },
49
+ "build.features": { customProtocol: "bool", devtools: "bool", isolation: "bool", systemTray: "bool" },
50
+ bundle: {
51
+ active: "bool", identifier: "string", icon: "string", version: "string", publisher: "string",
52
+ homepage: "string", longDescription: "string", copyright: "string", category: "string",
53
+ shortDescription: "string", licenseFile: "string", targets: "strings", resources: "strings",
54
+ externalBin: "strings"
55
+ },
56
+ "bundle.linuxBundle": { depends: "string", desktopTemplate: "string", section: "string", priority: "string", files: "strings" },
57
+ "bundle.windows": { language: "string", installerIcon: "string", installMode: "int", webviewInstallModeOffline: "bool" },
58
+ "bundle.macos": { minimumSystemVersion: "string", frameworks: "bool", signingIdentity: "string" },
59
+ security: {
60
+ freezePrototype: "bool", dangerousDisableAssetCspModification: "bool", dangerousUseHttpScheme: "bool",
61
+ dangerousAcceptInvalidCertificates: "bool", capabilities: "strings", dangerousRemoteDomainIpcAccess: "strings"
62
+ },
63
+ window: {
64
+ label: "string", title: "string", resizable: "bool", maximized: "bool", fullscreen: "bool",
65
+ alwaysOnTop: "bool", decorations: "bool", transparent: "bool", visible: "bool", center: "bool",
66
+ devtools: "bool", url: "string"
67
+ }
68
+ };
69
+
70
+ function emitFields(lines, target, input, fields) {
71
+ if (input === null || typeof input !== "object" || Array.isArray(input)) return;
72
+ for (const [key, kind] of Object.entries(fields)) {
73
+ const result = scalar(input[key], kind);
74
+ if (result !== null) lines.push(`${target}.${key} = ${result};`);
75
+ }
76
+ }
77
+
78
+ function emitSize(lines, target, input) {
79
+ if (!input || typeof input !== "object" || Array.isArray(input)) return;
80
+ for (const key of ["width", "height"]) {
81
+ const result = scalar(input[key], "number");
82
+ if (result !== null) lines.push(`${target}.${key} = ${result};`);
83
+ }
84
+ }
85
+
86
+ function emitCsp(lines, input) {
87
+ if (typeof input === "string") {
88
+ const directives = input.split(";").map((statement) => statement.trim()).filter(Boolean);
89
+ lines.push(`config.security.csp.enabled = ${input.trim() ? "true" : "false"};`);
90
+ for (const statement of directives) {
91
+ const [name, ...sources] = statement.split(/\s+/);
92
+ lines.push(`config.security.csp.directives.push_back(Flare::CspDirective{${cppString(name)}, ${scalar(sources, "strings")}});`);
93
+ }
94
+ } else if (input && typeof input === "object" && !Array.isArray(input)) {
95
+ lines.push("config.security.csp.enabled = true;");
96
+ for (const [name, sources] of Object.entries(input).sort(([a], [b]) => a.localeCompare(b))) {
97
+ const values = typeof sources === "string" ? sources.trim().split(/\s+/).filter(Boolean) : sources;
98
+ lines.push(`config.security.csp.directives.push_back(Flare::CspDirective{${cppString(name)}, ${scalar(values, "strings") ?? "{}"}});`);
99
+ }
100
+ }
101
+ }
102
+
103
+ export function generateEmbeddedConfig(config, raw = config, namespace = "FlareGenerated") {
104
+ if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(namespace)) throw new Error("invalid C++ namespace");
105
+ config = structuredClone(config);
106
+ const lines = [
107
+ "#pragma once", "", "#include <flare/config/config.h>", "", `namespace ${namespace} {`,
108
+ "inline Flare::Config EmbeddedConfig() {", "Flare::Config config;"
109
+ ];
110
+ if (raw.package && typeof raw.package === "object" && !Array.isArray(raw.package)) {
111
+ config.package.version ??= "0.1.0";
112
+ }
113
+ if (raw.build && typeof raw.build === "object" && !Array.isArray(raw.build)) {
114
+ config.build.frontendDist ??= "../dist";
115
+ }
116
+ if (raw.bundle && typeof raw.bundle === "object" && !Array.isArray(raw.bundle)) {
117
+ config.bundle.active ??= true;
118
+ config.bundle.icon ??= "icons/icon.png";
119
+ config.bundle.category ??= "DeveloperTool";
120
+ }
121
+ if (raw.bundle?.linux && typeof raw.bundle.linux === "object") {
122
+ config.bundle.linux.section ??= "utils";
123
+ config.bundle.linux.priority ??= "optional";
124
+ }
125
+ if (raw.bundle?.windows && typeof raw.bundle.windows === "object") {
126
+ config.bundle.windows.language ??= "en-US";
127
+ }
128
+ emitFields(lines, "config.package", config.package, FIELDS.package);
129
+ emitFields(lines, "config.app", config.app, FIELDS.app);
130
+ emitFields(lines, "config.build", config.build, FIELDS.build);
131
+ emitFields(lines, "config.build.features", config.build?.features, FIELDS["build.features"]);
132
+ emitFields(lines, "config.bundle", config.bundle, FIELDS.bundle);
133
+ emitFields(lines, "config.bundle.linuxBundle", config.bundle?.linux, FIELDS["bundle.linuxBundle"]);
134
+ emitFields(lines, "config.bundle.windows", config.bundle?.windows, FIELDS["bundle.windows"]);
135
+ emitFields(lines, "config.bundle.macos", config.bundle?.macos, FIELDS["bundle.macos"]);
136
+ emitFields(lines, "config.security", config.security, FIELDS.security);
137
+ emitCsp(lines, config.security?.csp);
138
+ if (typeof config.$schema === "string") lines.push(`config.schema = ${cppString(config.$schema)};`);
139
+ const windows = Array.isArray(config.windows) && config.windows.length ? config.windows : [{}];
140
+ for (const value of windows) {
141
+ lines.push("{ Flare::WindowConfig window;");
142
+ emitFields(lines, "window", value, FIELDS.window);
143
+ emitSize(lines, "window.size", value);
144
+ emitSize(lines, "window.size", value.size);
145
+ lines.push("window.minimumSize = Flare::WindowSize{0.0, 0.0};");
146
+ lines.push("window.maximumSize = Flare::WindowSize{0.0, 0.0};");
147
+ emitSize(lines, "window.minimumSize", value.minimumSize);
148
+ emitSize(lines, "window.maximumSize", value.maximumSize);
149
+ if (value.position && typeof value.position === "object" && !Array.isArray(value.position)) {
150
+ const x = scalar(value.position.x, "number") ?? "0.0";
151
+ const y = scalar(value.position.y, "number") ?? "0.0";
152
+ lines.push(`window.position = Flare::WindowPosition{${x}, ${y}};`);
153
+ }
154
+ if (typeof value.theme === "string") {
155
+ const theme = { light: "Light", dark: "Dark", automatic: "Automatic", system: "Automatic", auto: "Automatic" }[value.theme.toLowerCase()];
156
+ if (theme) lines.push(`window.theme = Flare::WindowTheme::${theme};`);
157
+ }
158
+ lines.push("config.windows.push_back(window); }");
159
+ }
160
+ if (config.plugins && typeof config.plugins === "object" && !Array.isArray(config.plugins)) {
161
+ for (const [name, value] of Object.entries(config.plugins).sort(([a], [b]) => a.localeCompare(b))) {
162
+ lines.push(`config.plugins.push_back(Flare::PluginConfig{${cppString(name)}, ${cppJson(value)}});`);
163
+ }
164
+ }
165
+ lines.push(`config.raw = ${cppJson(raw)};`);
166
+ lines.push("return config;", "}", "}", "");
167
+ return lines.join("\n");
168
+ }
169
+
170
+ export function writeEmbeddedConfig({ projectDirectory, configFile = "flare.config.json", environment = "production", output }) {
171
+ const { config, files } = loadProject({ projectDirectory, configFile, environment });
172
+ let raw = {};
173
+ for (const file of files) raw = mergeConfig(raw, parseConfig(readFileSync(file, "utf8"), file));
174
+ writeFileSync(output, generateEmbeddedConfig(config, raw), "utf8");
175
+ const result = spawnSync("clang-format", ["-style=file", "-i", output], { cwd: projectDirectory });
176
+ if (result.error && result.error.code !== "ENOENT") throw result.error;
177
+ if (!result.error && result.status !== 0) {
178
+ throw new Error(`clang-format failed: ${result.stderr.toString("utf8")}`);
179
+ }
180
+ return files;
181
+ }
package/src/lib/icons.js CHANGED
@@ -1,23 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+
1
3
  import { FlareError } from "./errors.js";
2
- import { resizePng, writePng } from "./png.js";
4
+ import { readPng, resizePng, writePng } from "./png.js";
5
+
6
+ let defaultMark;
3
7
 
4
8
  export function placeholderIcon(size = 512) {
5
- const data = Buffer.alloc(size * size * 4);
6
- const center = (size - 1) / 2;
7
- const radius = size * 0.46;
8
- for (let y = 0; y < size; y += 1) {
9
- for (let x = 0; x < size; x += 1) {
10
- const target = (y * size + x) * 4;
11
- const distance = Math.hypot(x - center, y - center) / radius;
12
- const glow = Math.max(0, Math.min(1, 1 - Math.abs(distance - 0.55) * 1.6));
13
- const edge = distance <= 1 ? 1 : 0;
14
- data[target] = edge ? Math.round(44 + 172 * glow) : 0;
15
- data[target + 1] = edge ? Math.round(35 + 132 * glow) : 0;
16
- data[target + 2] = edge ? Math.round(40 + 137 * glow) : 0;
17
- data[target + 3] = edge ? 255 : 0;
18
- }
19
- }
20
- return { width: size, height: size, data };
9
+ defaultMark ??= readPng(readFileSync(new URL("../assets/flare-mark.png", import.meta.url)));
10
+ return resizePng(defaultMark, size, size);
21
11
  }
22
12
 
23
13
  const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
@@ -175,8 +175,8 @@ export function mergeConfig(base, overlay) {
175
175
  function normalizeWindow(window, fallbackTitle) {
176
176
  const value = isObject(window) ? { ...window } : {};
177
177
  const title = value.title ?? fallbackTitle;
178
- const width = value.width ?? value.size?.width ?? 800;
179
- const height = value.height ?? value.size?.height ?? 600;
178
+ const width = value.size?.width ?? value.width ?? 800;
179
+ const height = value.size?.height ?? value.height ?? 600;
180
180
  return {
181
181
  ...value,
182
182
  label: value.label ?? "main",
@@ -212,7 +212,8 @@ export function normalizeConfig(raw) {
212
212
  if (!config.app.mainBinaryName) config.app.mainBinaryName = config.app.productName;
213
213
 
214
214
  const fallbackTitle = config.app.productName || "flare-app";
215
- const windows = Array.isArray(config.windows) && config.windows.length > 0 ? config.windows : [{}];
215
+ const sourceWindows = Array.isArray(config.windows) ? config.windows : config.app.windows;
216
+ const windows = Array.isArray(sourceWindows) && sourceWindows.length > 0 ? sourceWindows : [{}];
216
217
  config.windows = windows.map((window) => normalizeWindow(window, fallbackTitle));
217
218
 
218
219
  return config;
package/src/lib/sdk.js ADDED
@@ -0,0 +1,54 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import path from "node:path";
4
+ import { extract } from "tar";
5
+
6
+ import { FlareError } from "./errors.js";
7
+
8
+ export function sdkTarget(platform = process.platform, arch = process.arch) {
9
+ if (!(["x64", "arm64"].includes(arch))) throw new FlareError(`prebuilt SDK is not available for ${arch}; choose source mode`);
10
+ if (platform === "win32") {
11
+ if (arch !== "x64") throw new FlareError("prebuilt Windows SDK supports x64 MSVC only; choose source mode");
12
+ return { platform: "windows-x64", runtimes: ["md", "mt"] };
13
+ }
14
+ if (platform === "darwin") return { platform: `macos-${arch}`, runtimes: ["default"] };
15
+ if (platform === "linux" && arch === "x64") return { platform: "linux-x64", runtimes: ["default"] };
16
+ throw new FlareError(`prebuilt SDK is not available for ${platform}-${arch}; choose source mode`);
17
+ }
18
+
19
+ export async function installSdk({ destination, version, platform = process.platform, arch = process.arch, fetchImpl = fetch }) {
20
+ const target = sdkTarget(platform, arch);
21
+ mkdirSync(path.dirname(destination), { recursive: true });
22
+ const temporary = mkdtempSync(path.join(path.dirname(destination), ".flare-sdk-"));
23
+ const sdk = path.join(temporary, "flare");
24
+ mkdirSync(sdk);
25
+ try {
26
+ const checksumsUrl = `https://github.com/flare-desktop/flare/releases/download/v${version}/SHA256SUMS.txt`;
27
+ const checksumsResponse = await fetchImpl(checksumsUrl);
28
+ if (!checksumsResponse.ok) throw new FlareError(`SDK checksums are unavailable (HTTP ${checksumsResponse.status}); publish core v${version} first`);
29
+ const sums = new Map((await checksumsResponse.text()).split("\n").map((line) => /^(\w{64})\s+\*?(\S+)$/.exec(line)).filter(Boolean).map((match) => [match[2], match[1].toLowerCase()]));
30
+ for (const runtime of target.runtimes) {
31
+ const file = `flare-sdk-v${version}-${target.platform}-${runtime}.tar.gz`;
32
+ const url = `https://github.com/flare-desktop/flare/releases/download/v${version}/${file}`;
33
+ let response;
34
+ try { response = await fetchImpl(url); } catch (error) { throw new FlareError(`failed to download ${file}: ${error.message}`); }
35
+ if (!response.ok) throw new FlareError(`SDK archive ${file} is unavailable (HTTP ${response.status}); publish core v${version} SDK release first, or choose source mode`);
36
+ const archive = path.join(temporary, file);
37
+ const data = Buffer.from(await response.arrayBuffer());
38
+ const digest = createHash("sha256").update(data).digest("hex");
39
+ if (!sums.has(file) || sums.get(file) !== digest) throw new FlareError(`SDK checksum mismatch for ${file}`);
40
+ writeFileSync(archive, data);
41
+ await extract({ file: archive, cwd: temporary, strict: true, filter: (entry) => entry === "flare" || entry.startsWith("flare/") });
42
+ const manifest = JSON.parse(readFileSync(path.join(sdk, "sdk-manifest.json"), "utf8"));
43
+ if (manifest.version !== version || manifest.target !== target.platform || manifest.crt !== runtime) {
44
+ throw new FlareError(`SDK archive ${file} has incorrect platform or version metadata`);
45
+ }
46
+ }
47
+ rmSync(destination, { recursive: true, force: true });
48
+ mkdirSync(path.dirname(destination), { recursive: true });
49
+ renameSync(sdk, destination);
50
+ } finally {
51
+ rmSync(temporary, { recursive: true, force: true });
52
+ }
53
+ return target;
54
+ }
@@ -0,0 +1,2 @@
1
+ export const CORE_VERSION = "0.1.1";
2
+ export const MIN_CLI_VERSION = "0.1.2";
@@ -0,0 +1,32 @@
1
+ BasedOnStyle: Google
2
+ Standard: c++20
3
+ ColumnLimit: 120
4
+ IndentWidth: 4
5
+ TabWidth: 4
6
+ ContinuationIndentWidth: 4
7
+ ConstructorInitializerIndentWidth: 4
8
+ AccessModifierOffset: -4
9
+ NamespaceIndentation: All
10
+ UseTab: Never
11
+ PointerAlignment: Left
12
+ DerivePointerAlignment: false
13
+ AllowShortFunctionsOnASingleLine: Inline
14
+ AllowShortIfStatementsOnASingleLine: WithoutElse
15
+ AllowShortLoopsOnASingleLine: false
16
+ RemoveBracesLLVM: true
17
+ SortIncludes: Never
18
+ FixNamespaceComments: false
19
+ AlignAfterOpenBracket: BlockIndent
20
+ AlignOperands: DontAlign
21
+ AlignTrailingComments: false
22
+ AllowAllArgumentsOnNextLine: false
23
+ AllowAllParametersOfDeclarationOnNextLine: false
24
+ BinPackArguments: false
25
+ BinPackParameters: false
26
+ BreakBeforeBinaryOperators: NonAssignment
27
+ BreakConstructorInitializers: BeforeColon
28
+ PackConstructorInitializers: Never
29
+ LambdaBodyIndentation: OuterScope
30
+ PenaltyBreakBeforeFirstCallParameter: 10
31
+ AlwaysBreakAfterReturnType: None
32
+ PenaltyReturnTypeOnItsOwnLine: 1000
@@ -0,0 +1,135 @@
1
+ export type Payload = Record<string, unknown> | unknown[] | string | number | boolean | null;
2
+
3
+ export interface InvokeOptions {
4
+ id?: string;
5
+ window?: string;
6
+ webview?: string;
7
+ }
8
+
9
+ export interface ListenOptions {
10
+ window?: string;
11
+ }
12
+
13
+ export interface IpcErrorShape {
14
+ code: string;
15
+ message: string;
16
+ details?: Payload;
17
+ }
18
+
19
+ export interface IpcFailure extends Error {
20
+ code: string;
21
+ details?: Payload;
22
+ }
23
+
24
+ export interface ChannelMessage {
25
+ sequence: number;
26
+ payload: Payload;
27
+ state: 'open' | 'closed';
28
+ }
29
+
30
+ export interface Channel {
31
+ identifier: number;
32
+ name: string;
33
+ send(payload?: Payload): Promise<void>;
34
+ deliver(message: ChannelMessage): void;
35
+ close(): void;
36
+ }
37
+
38
+ export interface Metadata {
39
+ readonly protocol: string;
40
+ readonly host: string;
41
+ readonly version: string;
42
+ }
43
+
44
+ export function isFlare(): boolean;
45
+ export function metadata(): Metadata;
46
+ export function invoke<T = unknown>(command: string, payload?: Payload, options?: InvokeOptions): Promise<T>;
47
+ export function listen(event: string, handler: (payload: Payload) => void, options?: ListenOptions): Promise<() => Promise<void>>;
48
+ export function once(event: string, handler: (payload: Payload) => void, options?: ListenOptions): Promise<() => Promise<void>>;
49
+ export function unlisten(event: string, id: number): Promise<void>;
50
+ export function emit(event: string, payload?: Payload): Promise<void>;
51
+ export function dispatch(event: string, payload?: Payload): number;
52
+ export function createChannel(onMessage: (message: ChannelMessage) => void): Channel;
53
+ export function currentWindow(): {
54
+ label: string;
55
+ close(): Promise<unknown>;
56
+ setTitle(title: string): Promise<unknown>;
57
+ show(): Promise<unknown>;
58
+ hide(): Promise<unknown>;
59
+ };
60
+
61
+ export const app: {
62
+ version(): Promise<string>;
63
+ describe(): Promise<Payload>;
64
+ config(): Promise<Payload>;
65
+ platform(): Promise<Payload>;
66
+ exit(): Promise<unknown>;
67
+ };
68
+
69
+ export const window: {
70
+ list(): Promise<string[]>;
71
+ emit(event: string, payload?: Payload): Promise<unknown>;
72
+ };
73
+
74
+ export const plugins: { list(): Promise<Payload> };
75
+ export interface MenuItem {
76
+ id?: string;
77
+ kind?: 'text' | 'check' | 'radio' | 'separator' | 'submenu' | 'predefined';
78
+ text?: string;
79
+ accelerator?: string;
80
+ enabled?: boolean;
81
+ checked?: boolean;
82
+ icon?: string;
83
+ items?: MenuItem[];
84
+ }
85
+
86
+ export const menu: {
87
+ describe(): Promise<{ id: string; items: MenuItem[] }>;
88
+ set(items: MenuItem[]): Promise<boolean>;
89
+ dispatch(id: string): Promise<boolean>;
90
+ onCommand(handler: (id: string, payload: { id: string }) => void): () => Promise<boolean>;
91
+ };
92
+ export const commands: {
93
+ register(name: string, handler: (payload: Payload) => Payload): void;
94
+ unregister(name: string): boolean;
95
+ has(name: string): boolean;
96
+ names(): string[];
97
+ };
98
+ export const log: {
99
+ trace(message: string): Promise<void>;
100
+ debug(message: string): Promise<void>;
101
+ info(message: string): Promise<void>;
102
+ warn(message: string): Promise<void>;
103
+ error(message: string): Promise<void>;
104
+ };
105
+
106
+ export type DirectoryName =
107
+ | 'appData'
108
+ | 'appLocalData'
109
+ | 'appConfig'
110
+ | 'appCache'
111
+ | 'appLog'
112
+ | 'home'
113
+ | 'documents'
114
+ | 'downloads'
115
+ | 'desktop'
116
+ | 'pictures'
117
+ | 'temp'
118
+ | 'executable'
119
+ | 'resource'
120
+ | 'current';
121
+
122
+ export const path: {
123
+ resolve(name: DirectoryName | string): Promise<{ name: string; path: string }>;
124
+ all(): Promise<Record<DirectoryName, string>>;
125
+ };
126
+
127
+ export const tray: {
128
+ describe(): Promise<Payload & { menu: { id: string; items: MenuItem[] } }>;
129
+ setIcon(icon: string): Promise<Payload>;
130
+ setTooltip(tooltip: string): Promise<Payload>;
131
+ setTitle(title: string): Promise<Payload>;
132
+ setVisible(visible: boolean): Promise<Payload>;
133
+ setMenu(items: MenuItem[]): Promise<Payload>;
134
+ dispatch(id: string): Promise<boolean>;
135
+ };