@quickgui/cli 0.0.1

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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @quickgui/cli
2
+
3
+ Project scaffolding, development, and production packaging for QuickGUI applications written with
4
+ Solid 2.
5
+
6
+ ```console
7
+ bunx @quickgui/cli init my-app
8
+ cd my-app
9
+ bun run dev
10
+ ```
11
+
12
+ ## Development
13
+
14
+ `quickgui dev` creates a genuine native development application. On macOS this is an ad-hoc-signed
15
+ `.app` under `.quickgui/dev/<target>/` and the CLI runs its `Contents/MacOS` executable.
16
+
17
+ Each development build compiles two Bun entrypoints into the bundle's executable: a minimal native
18
+ main-thread host and the current TS/TSX application Worker. Source edits create and sign a candidate
19
+ `.app`; after its first native window completes an event-loop turn, the CLI stops the prior process.
20
+ A compile or startup failure leaves the prior app running. The generated `.app` is self-contained
21
+ and can also be launched directly from Finder or LaunchServices.
22
+
23
+ AppKit/Winit stays permanently on the process main thread. Bun timers, fetch, streaming, and other
24
+ application work stay on the Worker's supported event loop. Bounded native queues and an explicit
25
+ Winit wake connect them without an idle polling interval.
26
+
27
+ QuickGUI uses candidate-first process restart instead of in-isolate hot replacement because
28
+ AppKit/Winit application state is process-owned.
29
+
30
+ ## Production builds
31
+
32
+ `quickgui build` compiles the application, Solid renderer, Bun runtime, and target N-API addon into
33
+ a self-contained executable. A production macOS target emits a signed `.app` and a versioned `.dmg`
34
+ created with `create-dmg` under `dist/<target>/`; disk-image creation requires Node.js 20 or later.
35
+
36
+ ```console
37
+ bun run build --target darwin-arm64
38
+ bun run build --target windows-x64
39
+ bun run build --sign "Developer ID Application: Example (TEAMID)" --notarize quickgui-notary
40
+ ```
41
+
42
+ A target build requires the installed `@quickgui/native` package to contain that target's addon.
43
+ `--notarize` names credentials previously stored with `xcrun notarytool store-credentials`; the CLI
44
+ waits for acceptance, staples the DMG, and validates the ticket. The same profile can be configured
45
+ as `macos.notarization.keychainProfile`.
46
+
47
+ ## Configuration
48
+
49
+ ```ts
50
+ import { defineConfig } from "@quickgui/cli";
51
+
52
+ export default defineConfig({
53
+ name: "My App",
54
+ identifier: "com.example.my-app",
55
+ entry: "src/app.tsx",
56
+ version: "0.1.0",
57
+ resources: ["assets"],
58
+ protocols: ["my-app"],
59
+ macos: {
60
+ icon: "assets/AppIcon.icns",
61
+ minimumSystemVersion: "13.0",
62
+ signingIdentity: "Developer ID Application: Example (TEAMID)",
63
+ notarization: { keychainProfile: "quickgui-notary" },
64
+ },
65
+ });
66
+ ```
67
+
68
+ `protocols` is written into a signed macOS app's `CFBundleURLTypes`. On Windows and Linux, register
69
+ the same scheme at runtime with `DeepLink.register(...)`; macOS registration is intentionally
70
+ declarative because Launch Services reads it from the application bundle.
71
+
72
+ See the [CLI guide](../../docs/cli.md) for every command and option.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@quickgui/cli",
3
+ "version": "0.0.1",
4
+ "description": "Project, development, and packaging CLI for QuickGUI",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/egoist/quickgui.git",
8
+ "directory": "packages/cli"
9
+ },
10
+ "bugs": "https://github.com/egoist/quickgui/issues",
11
+ "homepage": "https://github.com/egoist/quickgui#readme",
12
+ "type": "module",
13
+ "bin": {
14
+ "quickgui": "src/cli.ts"
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.ts"
18
+ },
19
+ "files": [
20
+ "src/args.ts",
21
+ "src/build.ts",
22
+ "src/cli.ts",
23
+ "src/config.ts",
24
+ "src/dev.ts",
25
+ "src/error.ts",
26
+ "src/index.ts",
27
+ "src/init.ts",
28
+ "src/targets.ts",
29
+ "templates",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "test": "bun test"
34
+ },
35
+ "dependencies": {
36
+ "@quickgui/native": "0.0.1",
37
+ "@quickgui/solid": "0.0.1",
38
+ "create-dmg": "^8.1.0"
39
+ },
40
+ "engines": {
41
+ "bun": ">=1.3.0"
42
+ },
43
+ "os": [
44
+ "darwin"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "license": "MIT OR Apache-2.0"
50
+ }
package/src/args.ts ADDED
@@ -0,0 +1,182 @@
1
+ import { CliError } from "./error.ts";
2
+ import { parseTarget, type QuickGuiTarget } from "./targets.ts";
3
+
4
+ export type ParsedCliCommand =
5
+ | { command: "help"; topic?: "init" | "dev" | "build" }
6
+ | { command: "version" }
7
+ | {
8
+ command: "init";
9
+ directory: string;
10
+ install: boolean;
11
+ name?: string;
12
+ identifier?: string;
13
+ }
14
+ | {
15
+ command: "dev";
16
+ project: string;
17
+ configFile: string;
18
+ once: boolean;
19
+ launch: boolean;
20
+ target?: QuickGuiTarget;
21
+ signingIdentity?: string;
22
+ }
23
+ | {
24
+ command: "build";
25
+ project: string;
26
+ configFile: string;
27
+ target?: QuickGuiTarget;
28
+ outDir?: string;
29
+ signingIdentity?: string;
30
+ notarizationProfile?: string;
31
+ };
32
+
33
+ interface OptionSpec {
34
+ key: string;
35
+ value: boolean;
36
+ }
37
+
38
+ interface ParsedOptions {
39
+ values: Map<string, string | true>;
40
+ positionals: string[];
41
+ }
42
+
43
+ export function parseCliArgs(argv: string[]): ParsedCliCommand {
44
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
45
+ return { command: "help" };
46
+ }
47
+ if (argv[0] === "--version" || argv[0] === "-v") return { command: "version" };
48
+
49
+ const command = argv[0];
50
+ const rest = argv.slice(1);
51
+ if (command === "help") {
52
+ if (rest.length > 1 || (rest[0] && !["init", "dev", "build"].includes(rest[0]))) {
53
+ throw new CliError("Usage: quickgui help [init|dev|build]");
54
+ }
55
+ return rest[0]
56
+ ? { command: "help", topic: rest[0] as "init" | "dev" | "build" }
57
+ : { command: "help" };
58
+ }
59
+ if (rest.includes("--help") || rest.includes("-h")) {
60
+ if (command === "init" || command === "dev" || command === "build") {
61
+ return { command: "help", topic: command };
62
+ }
63
+ }
64
+
65
+ if (command === "init") {
66
+ const parsed = parseOptions(rest, {
67
+ "--name": { key: "name", value: true },
68
+ "--identifier": { key: "identifier", value: true },
69
+ "--no-install": { key: "noInstall", value: false },
70
+ });
71
+ if (parsed.positionals.length > 1) throw new CliError("Usage: quickgui init [directory]");
72
+ const name = stringOption(parsed, "name");
73
+ const identifier = stringOption(parsed, "identifier");
74
+ return {
75
+ command: "init",
76
+ directory: parsed.positionals[0] ?? "quickgui-app",
77
+ install: !parsed.values.has("noInstall"),
78
+ ...(name ? { name } : {}),
79
+ ...(identifier ? { identifier } : {}),
80
+ };
81
+ }
82
+
83
+ if (command === "dev") {
84
+ const parsed = parseOptions(rest, {
85
+ "--project": { key: "project", value: true },
86
+ "--config": { key: "configFile", value: true },
87
+ "--target": { key: "target", value: true },
88
+ "--sign": { key: "signingIdentity", value: true },
89
+ "--once": { key: "once", value: false },
90
+ "--no-launch": { key: "noLaunch", value: false },
91
+ });
92
+ rejectPositionals(parsed, "quickgui dev");
93
+ const target = stringOption(parsed, "target");
94
+ const signingIdentity = stringOption(parsed, "signingIdentity");
95
+ return {
96
+ command: "dev",
97
+ project: stringOption(parsed, "project") ?? ".",
98
+ configFile: stringOption(parsed, "configFile") ?? "quickgui.config.ts",
99
+ once: parsed.values.has("once"),
100
+ launch: !parsed.values.has("noLaunch"),
101
+ ...(target ? { target: parseTarget(target) } : {}),
102
+ ...(signingIdentity ? { signingIdentity } : {}),
103
+ };
104
+ }
105
+
106
+ if (command === "build") {
107
+ const parsed = parseOptions(rest, {
108
+ "--project": { key: "project", value: true },
109
+ "--config": { key: "configFile", value: true },
110
+ "--target": { key: "target", value: true },
111
+ "--out-dir": { key: "outDir", value: true },
112
+ "--sign": { key: "signingIdentity", value: true },
113
+ "--notarize": { key: "notarizationProfile", value: true },
114
+ });
115
+ rejectPositionals(parsed, "quickgui build");
116
+ const target = stringOption(parsed, "target");
117
+ const outDir = stringOption(parsed, "outDir");
118
+ const signingIdentity = stringOption(parsed, "signingIdentity");
119
+ const notarizationProfile = stringOption(parsed, "notarizationProfile");
120
+ return {
121
+ command: "build",
122
+ project: stringOption(parsed, "project") ?? ".",
123
+ configFile: stringOption(parsed, "configFile") ?? "quickgui.config.ts",
124
+ ...(target ? { target: parseTarget(target) } : {}),
125
+ ...(outDir ? { outDir } : {}),
126
+ ...(signingIdentity ? { signingIdentity } : {}),
127
+ ...(notarizationProfile ? { notarizationProfile } : {}),
128
+ };
129
+ }
130
+
131
+ throw new CliError(`Unknown command: ${command}\nRun \`quickgui --help\` for usage.`);
132
+ }
133
+
134
+ function parseOptions(argv: string[], specs: Record<string, OptionSpec>): ParsedOptions {
135
+ const values = new Map<string, string | true>();
136
+ const positionals: string[] = [];
137
+ let positionalOnly = false;
138
+
139
+ for (let index = 0; index < argv.length; index += 1) {
140
+ const token = argv[index]!;
141
+ if (!positionalOnly && token === "--") {
142
+ positionalOnly = true;
143
+ continue;
144
+ }
145
+ if (positionalOnly || !token.startsWith("-")) {
146
+ positionals.push(token);
147
+ continue;
148
+ }
149
+ const equal = token.indexOf("=");
150
+ const name = equal === -1 ? token : token.slice(0, equal);
151
+ const inlineValue = equal === -1 ? undefined : token.slice(equal + 1);
152
+ const spec = specs[name];
153
+ if (!spec) throw new CliError(`Unknown option: ${name}`);
154
+ if (values.has(spec.key)) throw new CliError(`Option may only be specified once: ${name}`);
155
+ if (!spec.value) {
156
+ if (inlineValue !== undefined) throw new CliError(`Option does not take a value: ${name}`);
157
+ values.set(spec.key, true);
158
+ continue;
159
+ }
160
+ const value = inlineValue ?? argv[++index];
161
+ if (value === undefined || value.length === 0) {
162
+ throw new CliError(`Option requires a value: ${name}`);
163
+ }
164
+ if (inlineValue === undefined && specs[value]) {
165
+ throw new CliError(`Option requires a value: ${name}`);
166
+ }
167
+ values.set(spec.key, value);
168
+ }
169
+
170
+ return { values, positionals };
171
+ }
172
+
173
+ function stringOption(parsed: ParsedOptions, key: string): string | undefined {
174
+ const value = parsed.values.get(key);
175
+ return typeof value === "string" ? value : undefined;
176
+ }
177
+
178
+ function rejectPositionals(parsed: ParsedOptions, usage: string): void {
179
+ if (parsed.positionals.length > 0) {
180
+ throw new CliError(`${usage} does not accept positional arguments`);
181
+ }
182
+ }