@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 +72 -0
- package/package.json +50 -0
- package/src/args.ts +182 -0
- package/src/build.ts +773 -0
- package/src/cli.ts +126 -0
- package/src/config.ts +256 -0
- package/src/dev.ts +279 -0
- package/src/error.ts +17 -0
- package/src/index.ts +9 -0
- package/src/init.ts +107 -0
- package/src/targets.ts +85 -0
- package/templates/solid/README.md +8 -0
- package/templates/solid/gitignore +3 -0
- package/templates/solid/package.json +19 -0
- package/templates/solid/quickgui.config.ts +7 -0
- package/templates/solid/src/app.tsx +49 -0
- package/templates/solid/tsconfig.json +16 -0
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
|
+
}
|