@gtkx/cli 0.18.0 → 0.18.2
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/dist/builder.d.ts +26 -6
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +8 -31
- package/dist/builder.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +1 -0
- package/dist/create.js.map +1 -0
- package/dist/dev-server.d.ts +1 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/dev-server.js +1 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +1 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +1 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/refresh-runtime.d.ts +1 -0
- package/dist/refresh-runtime.d.ts.map +1 -0
- package/dist/refresh-runtime.js +1 -0
- package/dist/refresh-runtime.js.map +1 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +1 -0
- package/dist/templates.js.map +1 -0
- package/dist/vite-plugin-gtkx-assets.d.ts +1 -0
- package/dist/vite-plugin-gtkx-assets.d.ts.map +1 -0
- package/dist/vite-plugin-gtkx-assets.js +1 -0
- package/dist/vite-plugin-gtkx-assets.js.map +1 -0
- package/dist/vite-plugin-gtkx-built-url.d.ts +18 -0
- package/dist/vite-plugin-gtkx-built-url.d.ts.map +1 -0
- package/dist/vite-plugin-gtkx-built-url.js +43 -0
- package/dist/vite-plugin-gtkx-built-url.js.map +1 -0
- package/dist/vite-plugin-gtkx-native.d.ts +14 -0
- package/dist/vite-plugin-gtkx-native.d.ts.map +1 -0
- package/dist/vite-plugin-gtkx-native.js +53 -0
- package/dist/vite-plugin-gtkx-native.js.map +1 -0
- package/dist/vite-plugin-gtkx-refresh.d.ts +1 -0
- package/dist/vite-plugin-gtkx-refresh.d.ts.map +1 -0
- package/dist/vite-plugin-gtkx-refresh.js +1 -0
- package/dist/vite-plugin-gtkx-refresh.js.map +1 -0
- package/dist/vite-plugin-swc-ssr-refresh.d.ts +1 -0
- package/dist/vite-plugin-swc-ssr-refresh.d.ts.map +1 -0
- package/dist/vite-plugin-swc-ssr-refresh.js +1 -0
- package/dist/vite-plugin-swc-ssr-refresh.js.map +1 -0
- package/package.json +8 -6
- package/src/builder.ts +94 -0
- package/src/cli.tsx +154 -0
- package/src/create.ts +310 -0
- package/src/dev-server.tsx +162 -0
- package/src/global.d.ts +6 -0
- package/src/index.ts +3 -0
- package/src/mcp-client.ts +518 -0
- package/src/refresh-runtime.ts +89 -0
- package/src/templates.ts +26 -0
- package/src/vite-plugin-gtkx-assets.ts +32 -0
- package/src/vite-plugin-gtkx-built-url.ts +48 -0
- package/src/vite-plugin-gtkx-native.ts +64 -0
- package/src/vite-plugin-gtkx-refresh.ts +54 -0
- package/src/vite-plugin-swc-ssr-refresh.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/cli",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "CLI for GTKX - create and develop GTK4 React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -50,9 +50,11 @@
|
|
|
50
50
|
"bin": {
|
|
51
51
|
"gtkx": "bin/gtkx.js"
|
|
52
52
|
},
|
|
53
|
+
"sideEffects": false,
|
|
53
54
|
"files": [
|
|
54
55
|
"bin",
|
|
55
56
|
"dist",
|
|
57
|
+
"src",
|
|
56
58
|
"templates"
|
|
57
59
|
],
|
|
58
60
|
"dependencies": {
|
|
@@ -62,19 +64,19 @@
|
|
|
62
64
|
"ejs": "^4.0.1",
|
|
63
65
|
"react-refresh": "^0.18.0",
|
|
64
66
|
"vite": "^7.3.1",
|
|
65
|
-
"@gtkx/ffi": "0.18.
|
|
66
|
-
"@gtkx/mcp": "0.18.
|
|
67
|
-
"@gtkx/react": "0.18.
|
|
67
|
+
"@gtkx/ffi": "0.18.2",
|
|
68
|
+
"@gtkx/mcp": "0.18.2",
|
|
69
|
+
"@gtkx/react": "0.18.2"
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
70
72
|
"@types/ejs": "^3.1.5",
|
|
71
73
|
"@types/react-refresh": "^0.14.7",
|
|
72
74
|
"memfs": "^4.56.10",
|
|
73
|
-
"@gtkx/testing": "0.18.
|
|
75
|
+
"@gtkx/testing": "0.18.2"
|
|
74
76
|
},
|
|
75
77
|
"peerDependencies": {
|
|
76
78
|
"react": "^19",
|
|
77
|
-
"@gtkx/testing": "0.18.
|
|
79
|
+
"@gtkx/testing": "0.18.2"
|
|
78
80
|
},
|
|
79
81
|
"peerDependenciesMeta": {
|
|
80
82
|
"@gtkx/testing": {
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type InlineConfig, build as viteBuild } from "vite";
|
|
2
|
+
import { gtkxAssets } from "./vite-plugin-gtkx-assets.js";
|
|
3
|
+
import { gtkxBuiltUrl } from "./vite-plugin-gtkx-built-url.js";
|
|
4
|
+
import { gtkxNative } from "./vite-plugin-gtkx-native.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for building a GTKX application for production.
|
|
8
|
+
*/
|
|
9
|
+
export type BuildOptions = {
|
|
10
|
+
/** Path to the entry file (e.g., "src/index.tsx") */
|
|
11
|
+
entry: string;
|
|
12
|
+
/**
|
|
13
|
+
* Base path for resolving asset imports at runtime, relative to the
|
|
14
|
+
* executable directory.
|
|
15
|
+
*
|
|
16
|
+
* When set, asset imports resolve to
|
|
17
|
+
* `path.join(path.dirname(process.execPath), assetBase, filename)`.
|
|
18
|
+
* This is useful for FHS-compliant packaging where assets live under
|
|
19
|
+
* a `share/` directory rather than next to the binary.
|
|
20
|
+
*
|
|
21
|
+
* When omitted, assets resolve relative to the bundle via
|
|
22
|
+
* `import.meta.url`, which works when assets are co-located with
|
|
23
|
+
* the executable (e.g., in `bin/assets/`).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* await build({
|
|
28
|
+
* entry: "./src/index.tsx",
|
|
29
|
+
* assetBase: "../share/my-app",
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
assetBase?: string;
|
|
34
|
+
/** Additional Vite configuration */
|
|
35
|
+
vite?: InlineConfig;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds a GTKX application for production using Vite's SSR build mode.
|
|
40
|
+
*
|
|
41
|
+
* Produces a single minified ESM bundle at `dist/bundle.js` with all
|
|
42
|
+
* dependencies inlined. The native `.node` binary is copied into the
|
|
43
|
+
* output directory as `gtkx.node`, making the bundle fully self-contained
|
|
44
|
+
* with no `node_modules` dependency at runtime.
|
|
45
|
+
*
|
|
46
|
+
* @param options - Build configuration including entry point and Vite options
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { build } from "@gtkx/cli";
|
|
51
|
+
*
|
|
52
|
+
* await build({
|
|
53
|
+
* entry: "./src/index.tsx",
|
|
54
|
+
* vite: { root: process.cwd() },
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @see {@link BuildOptions} for configuration options
|
|
59
|
+
*/
|
|
60
|
+
export const build = async (options: BuildOptions): Promise<void> => {
|
|
61
|
+
const { entry, assetBase, vite: viteConfig } = options;
|
|
62
|
+
const root = viteConfig?.root ?? process.cwd();
|
|
63
|
+
|
|
64
|
+
await viteBuild({
|
|
65
|
+
...viteConfig,
|
|
66
|
+
plugins: [...(viteConfig?.plugins ?? []), gtkxAssets(), gtkxBuiltUrl(assetBase), gtkxNative(root)],
|
|
67
|
+
build: {
|
|
68
|
+
...viteConfig?.build,
|
|
69
|
+
ssr: entry,
|
|
70
|
+
ssrEmitAssets: true,
|
|
71
|
+
assetsInlineLimit: 0,
|
|
72
|
+
outDir: viteConfig?.build?.outDir ?? "dist",
|
|
73
|
+
minify: true,
|
|
74
|
+
rollupOptions: {
|
|
75
|
+
...viteConfig?.build?.rollupOptions,
|
|
76
|
+
output: {
|
|
77
|
+
...((viteConfig?.build?.rollupOptions?.output ?? {}) as Record<string, unknown>),
|
|
78
|
+
entryFileNames: "bundle.js",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
define: {
|
|
83
|
+
...viteConfig?.define,
|
|
84
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
85
|
+
},
|
|
86
|
+
ssr: {
|
|
87
|
+
...viteConfig?.ssr,
|
|
88
|
+
noExternal: true,
|
|
89
|
+
},
|
|
90
|
+
experimental: {
|
|
91
|
+
...viteConfig?.experimental,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
};
|
package/src/cli.tsx
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import "./refresh-runtime.js";
|
|
4
|
+
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { events } from "@gtkx/ffi";
|
|
8
|
+
import type * as Gio from "@gtkx/ffi/gio";
|
|
9
|
+
import { render } from "@gtkx/react";
|
|
10
|
+
import { defineCommand, runMain } from "citty";
|
|
11
|
+
import { build } from "./builder.js";
|
|
12
|
+
import { createApp } from "./create.js";
|
|
13
|
+
import { createDevServer } from "./dev-server.js";
|
|
14
|
+
import { startMcpClient, stopMcpClient } from "./mcp-client.js";
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { version } = require("../package.json") as { version: string };
|
|
18
|
+
|
|
19
|
+
interface AppModule {
|
|
20
|
+
default: () => React.ReactNode;
|
|
21
|
+
appId?: string;
|
|
22
|
+
appFlags?: Gio.ApplicationFlags;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dev = defineCommand({
|
|
26
|
+
meta: {
|
|
27
|
+
name: "dev",
|
|
28
|
+
description: "Start development server with HMR",
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
entry: {
|
|
32
|
+
type: "positional",
|
|
33
|
+
description: "Entry file (e.g., src/app.tsx)",
|
|
34
|
+
required: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async run({ args }) {
|
|
38
|
+
const entryPath = resolve(process.cwd(), args.entry);
|
|
39
|
+
console.log(`[gtkx] Starting dev server for ${entryPath}`);
|
|
40
|
+
|
|
41
|
+
const server = await createDevServer({
|
|
42
|
+
entry: entryPath,
|
|
43
|
+
vite: {
|
|
44
|
+
root: process.cwd(),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const mod = (await server.ssrLoadModule(entryPath)) as AppModule;
|
|
49
|
+
const App = mod.default;
|
|
50
|
+
const appId = mod.appId ?? "org.gtkx.dev";
|
|
51
|
+
const appFlags = mod.appFlags;
|
|
52
|
+
|
|
53
|
+
if (typeof App !== "function") {
|
|
54
|
+
console.error("[gtkx] Entry file must export a default function component");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`[gtkx] Rendering app with ID: ${appId}`);
|
|
59
|
+
render(<App />, appId, appFlags);
|
|
60
|
+
|
|
61
|
+
await startMcpClient(appId);
|
|
62
|
+
events.on("stop", () => {
|
|
63
|
+
stopMcpClient();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log("[gtkx] HMR enabled - watching for changes...");
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const buildCmd = defineCommand({
|
|
71
|
+
meta: {
|
|
72
|
+
name: "build",
|
|
73
|
+
description: "Build application for production",
|
|
74
|
+
},
|
|
75
|
+
args: {
|
|
76
|
+
entry: {
|
|
77
|
+
type: "positional",
|
|
78
|
+
description: "Entry file (default: src/index.tsx)",
|
|
79
|
+
required: false,
|
|
80
|
+
},
|
|
81
|
+
"asset-base": {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Asset base path relative to executable directory (e.g., ../share/my-app)",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
async run({ args }) {
|
|
87
|
+
const entry = resolve(process.cwd(), args.entry ?? "src/index.tsx");
|
|
88
|
+
console.log(`[gtkx] Building ${entry}`);
|
|
89
|
+
|
|
90
|
+
await build({
|
|
91
|
+
entry,
|
|
92
|
+
assetBase: args["asset-base"],
|
|
93
|
+
vite: {
|
|
94
|
+
root: process.cwd(),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log("[gtkx] Build complete: dist/bundle.js");
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const create = defineCommand({
|
|
103
|
+
meta: {
|
|
104
|
+
name: "create",
|
|
105
|
+
description: "Create a new GTKX application",
|
|
106
|
+
},
|
|
107
|
+
args: {
|
|
108
|
+
name: {
|
|
109
|
+
type: "positional",
|
|
110
|
+
description: "Project name",
|
|
111
|
+
required: false,
|
|
112
|
+
},
|
|
113
|
+
"app-id": {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "App ID (e.g., com.example.myapp)",
|
|
116
|
+
},
|
|
117
|
+
pm: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Package manager (pnpm, npm, yarn)",
|
|
120
|
+
},
|
|
121
|
+
testing: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Testing setup (vitest, none)",
|
|
124
|
+
},
|
|
125
|
+
"claude-skills": {
|
|
126
|
+
type: "boolean",
|
|
127
|
+
description: "Include Claude Code skills for AI assistance",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
async run({ args }) {
|
|
131
|
+
await createApp({
|
|
132
|
+
name: args.name,
|
|
133
|
+
appId: args["app-id"],
|
|
134
|
+
packageManager: args.pm as "pnpm" | "npm" | "yarn" | undefined,
|
|
135
|
+
testing: args.testing as "vitest" | "none" | undefined,
|
|
136
|
+
claudeSkills: args["claude-skills"],
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const main = defineCommand({
|
|
142
|
+
meta: {
|
|
143
|
+
name: "gtkx",
|
|
144
|
+
version,
|
|
145
|
+
description: "CLI for GTKX - create and develop GTK4 React applications",
|
|
146
|
+
},
|
|
147
|
+
subCommands: {
|
|
148
|
+
dev,
|
|
149
|
+
build: buildCmd,
|
|
150
|
+
create,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
runMain(main);
|
package/src/create.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import { renderFile, type TemplateContext } from "./templates.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Supported package managers for GTKX projects.
|
|
9
|
+
*/
|
|
10
|
+
export type PackageManager = "pnpm" | "npm" | "yarn";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether to include testing setup in GTKX projects.
|
|
14
|
+
*/
|
|
15
|
+
export type TestingOption = "vitest" | "none";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for creating a new GTKX project.
|
|
19
|
+
*
|
|
20
|
+
* All options are optional; missing values will be prompted interactively.
|
|
21
|
+
*/
|
|
22
|
+
export type CreateOptions = {
|
|
23
|
+
name?: string;
|
|
24
|
+
appId?: string;
|
|
25
|
+
packageManager?: PackageManager;
|
|
26
|
+
testing?: TestingOption;
|
|
27
|
+
claudeSkills?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEPENDENCIES = ["@gtkx/css", "@gtkx/ffi", "@gtkx/react", "react"];
|
|
31
|
+
|
|
32
|
+
const DEV_DEPENDENCIES = ["@gtkx/cli", "@types/react", "typescript", "vite"];
|
|
33
|
+
|
|
34
|
+
const TESTING_DEV_DEPENDENCIES = ["@gtkx/testing", "@gtkx/vitest", "vitest"];
|
|
35
|
+
|
|
36
|
+
const createTemplateContext = (name: string, appId: string, testing: TestingOption): TemplateContext => {
|
|
37
|
+
const title = name
|
|
38
|
+
.split("-")
|
|
39
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
40
|
+
.join(" ");
|
|
41
|
+
|
|
42
|
+
return { name, appId, title, testing };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const getAddCommand = (pm: PackageManager, deps: string[], dev: boolean): string => {
|
|
46
|
+
const devFlag = dev ? (pm === "npm" ? "--save-dev" : "-D") : "";
|
|
47
|
+
const parts = [devFlag, ...deps].filter(Boolean).join(" ");
|
|
48
|
+
|
|
49
|
+
switch (pm) {
|
|
50
|
+
case "npm":
|
|
51
|
+
return `npm install ${parts}`;
|
|
52
|
+
case "yarn":
|
|
53
|
+
return `yarn add ${parts}`;
|
|
54
|
+
case "pnpm":
|
|
55
|
+
return `pnpm add ${parts}`;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getRunCommand = (pm: PackageManager): string => {
|
|
60
|
+
switch (pm) {
|
|
61
|
+
case "npm":
|
|
62
|
+
return "npm run dev";
|
|
63
|
+
case "yarn":
|
|
64
|
+
return "yarn dev";
|
|
65
|
+
case "pnpm":
|
|
66
|
+
return "pnpm dev";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const isValidProjectName = (name: string): boolean => {
|
|
71
|
+
return /^[a-z0-9-]+$/.test(name);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const isValidAppId = (appId: string): boolean => {
|
|
75
|
+
return /^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)+$/.test(appId);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const runCommand = (command: string, cwd: string): Promise<void> => {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const proc = spawn(command, { cwd, stdio: "pipe", shell: true });
|
|
81
|
+
proc.on("close", (code) =>
|
|
82
|
+
code === 0 ? resolve() : reject(new Error(`Command failed with exit code ${code}`)),
|
|
83
|
+
);
|
|
84
|
+
proc.on("error", reject);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const suggestAppId = (name: string): string => {
|
|
89
|
+
const sanitized = name.replace(/-/g, "");
|
|
90
|
+
return `com.${sanitized}.app`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type ResolvedOptions = {
|
|
94
|
+
name: string;
|
|
95
|
+
appId: string;
|
|
96
|
+
packageManager: PackageManager;
|
|
97
|
+
testing: TestingOption;
|
|
98
|
+
claudeSkills: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const checkCancelled = <T>(value: T | symbol): T => {
|
|
102
|
+
if (p.isCancel(value)) {
|
|
103
|
+
p.cancel("Operation cancelled");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
return value as T;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const promptForOptions = async (options: CreateOptions): Promise<ResolvedOptions> => {
|
|
110
|
+
const name =
|
|
111
|
+
options.name ??
|
|
112
|
+
checkCancelled(
|
|
113
|
+
await p.text({
|
|
114
|
+
message: "Project name",
|
|
115
|
+
placeholder: "my-app",
|
|
116
|
+
validate: (value) => {
|
|
117
|
+
if (!value) return "Project name is required";
|
|
118
|
+
if (!isValidProjectName(value)) {
|
|
119
|
+
return "Project name must be lowercase letters, numbers, and hyphens only";
|
|
120
|
+
}
|
|
121
|
+
if (existsSync(resolve(process.cwd(), value))) {
|
|
122
|
+
return `Directory "${value}" already exists`;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const defaultAppId = suggestAppId(name);
|
|
129
|
+
|
|
130
|
+
const appId =
|
|
131
|
+
options.appId ??
|
|
132
|
+
checkCancelled(
|
|
133
|
+
await p.text({
|
|
134
|
+
message: "App ID",
|
|
135
|
+
placeholder: defaultAppId,
|
|
136
|
+
initialValue: defaultAppId,
|
|
137
|
+
validate: (value) => {
|
|
138
|
+
if (!value) return "App ID is required";
|
|
139
|
+
if (!isValidAppId(value)) {
|
|
140
|
+
return "App ID must be reverse domain notation (e.g., com.example.myapp)";
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const packageManager =
|
|
147
|
+
options.packageManager ??
|
|
148
|
+
checkCancelled(
|
|
149
|
+
await p.select({
|
|
150
|
+
message: "Package manager",
|
|
151
|
+
options: [
|
|
152
|
+
{ value: "pnpm", label: "pnpm", hint: "recommended" },
|
|
153
|
+
{ value: "npm", label: "npm" },
|
|
154
|
+
{ value: "yarn", label: "yarn" },
|
|
155
|
+
],
|
|
156
|
+
initialValue: "pnpm",
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const testing: TestingOption =
|
|
161
|
+
options.testing ??
|
|
162
|
+
(checkCancelled(
|
|
163
|
+
await p.confirm({
|
|
164
|
+
message: "Include testing setup (Vitest)?",
|
|
165
|
+
initialValue: true,
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
? "vitest"
|
|
169
|
+
: "none");
|
|
170
|
+
|
|
171
|
+
const claudeSkills =
|
|
172
|
+
options.claudeSkills ??
|
|
173
|
+
checkCancelled(
|
|
174
|
+
await p.confirm({
|
|
175
|
+
message: "Include Claude Code skills?",
|
|
176
|
+
initialValue: true,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return { name, appId, packageManager, testing, claudeSkills };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const scaffoldProject = (projectPath: string, resolved: ResolvedOptions): void => {
|
|
184
|
+
const { name, appId, testing, claudeSkills } = resolved;
|
|
185
|
+
const context = createTemplateContext(name, appId, testing);
|
|
186
|
+
|
|
187
|
+
mkdirSync(projectPath, { recursive: true });
|
|
188
|
+
mkdirSync(join(projectPath, "src"), { recursive: true });
|
|
189
|
+
|
|
190
|
+
if (testing !== "none") {
|
|
191
|
+
mkdirSync(join(projectPath, "tests"), { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
writeFileSync(join(projectPath, "package.json"), renderFile("package.json.ejs", context));
|
|
195
|
+
writeFileSync(join(projectPath, "tsconfig.json"), renderFile("tsconfig.json.ejs", context));
|
|
196
|
+
writeFileSync(join(projectPath, "src", "app.tsx"), renderFile("src/app.tsx.ejs", context));
|
|
197
|
+
writeFileSync(join(projectPath, "src", "dev.tsx"), renderFile("src/dev.tsx.ejs", context));
|
|
198
|
+
writeFileSync(join(projectPath, "src", "index.tsx"), renderFile("src/index.tsx.ejs", context));
|
|
199
|
+
writeFileSync(join(projectPath, "src", "vite-env.d.ts"), renderFile("src/vite-env.d.ts.ejs", context));
|
|
200
|
+
writeFileSync(join(projectPath, ".gitignore"), renderFile("gitignore.ejs", context));
|
|
201
|
+
|
|
202
|
+
if (claudeSkills) {
|
|
203
|
+
const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
|
|
204
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
205
|
+
writeFileSync(join(skillsDir, "SKILL.md"), renderFile("claude/SKILL.md.ejs", context));
|
|
206
|
+
writeFileSync(join(skillsDir, "WIDGETS.md"), renderFile("claude/WIDGETS.md.ejs", context));
|
|
207
|
+
writeFileSync(join(skillsDir, "EXAMPLES.md"), renderFile("claude/EXAMPLES.md.ejs", context));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (testing === "vitest") {
|
|
211
|
+
writeFileSync(join(projectPath, "vitest.config.ts"), renderFile("config/vitest.config.ts.ejs", context));
|
|
212
|
+
writeFileSync(join(projectPath, "tests", "app.test.tsx"), renderFile("tests/app.test.tsx.ejs", context));
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const getDevDependencies = (testing: TestingOption): string[] => {
|
|
217
|
+
const devDeps = [...DEV_DEPENDENCIES];
|
|
218
|
+
if (testing === "vitest") {
|
|
219
|
+
devDeps.push(...TESTING_DEV_DEPENDENCIES);
|
|
220
|
+
}
|
|
221
|
+
return devDeps;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const installDependencies = async (
|
|
225
|
+
projectPath: string,
|
|
226
|
+
name: string,
|
|
227
|
+
packageManager: PackageManager,
|
|
228
|
+
devDeps: string[],
|
|
229
|
+
): Promise<void> => {
|
|
230
|
+
const installSpinner = p.spinner();
|
|
231
|
+
installSpinner.start("Installing dependencies...");
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const addCmd = getAddCommand(packageManager, DEPENDENCIES, false);
|
|
235
|
+
await runCommand(addCmd, projectPath);
|
|
236
|
+
|
|
237
|
+
const addDevCmd = getAddCommand(packageManager, devDeps, true);
|
|
238
|
+
await runCommand(addDevCmd, projectPath);
|
|
239
|
+
|
|
240
|
+
installSpinner.stop("Dependencies installed!");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
installSpinner.stop("Failed to install dependencies");
|
|
243
|
+
p.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
|
+
p.log.info("You can install dependencies manually by running:");
|
|
245
|
+
p.log.info(` cd ${name}`);
|
|
246
|
+
p.log.info(` ${getAddCommand(packageManager, DEPENDENCIES, false)}`);
|
|
247
|
+
p.log.info(` ${getAddCommand(packageManager, devDeps, true)}`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const printNextSteps = (name: string, packageManager: PackageManager, testing: TestingOption): void => {
|
|
252
|
+
const runCmd = getRunCommand(packageManager);
|
|
253
|
+
const nextSteps = `cd ${name}\n${runCmd}`;
|
|
254
|
+
|
|
255
|
+
const testingNote =
|
|
256
|
+
testing !== "none"
|
|
257
|
+
? `
|
|
258
|
+
|
|
259
|
+
To run tests, you need xvfb installed:
|
|
260
|
+
Fedora: sudo dnf install xorg-x11-server-Xvfb
|
|
261
|
+
Ubuntu: sudo apt install xvfb`
|
|
262
|
+
: "";
|
|
263
|
+
|
|
264
|
+
p.note(`${nextSteps}${testingNote}`, "Next steps");
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Creates a new GTKX project with interactive prompts.
|
|
269
|
+
*
|
|
270
|
+
* Scaffolds a complete project structure including:
|
|
271
|
+
* - TypeScript configuration
|
|
272
|
+
* - React component template
|
|
273
|
+
* - Development server entry point
|
|
274
|
+
* - Optional testing setup
|
|
275
|
+
* - Optional Claude Code skills
|
|
276
|
+
*
|
|
277
|
+
* @param options - Pre-filled options to skip prompts
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```tsx
|
|
281
|
+
* import { createApp } from "@gtkx/cli";
|
|
282
|
+
*
|
|
283
|
+
* // Interactive mode
|
|
284
|
+
* await createApp();
|
|
285
|
+
*
|
|
286
|
+
* // With pre-filled options
|
|
287
|
+
* await createApp({
|
|
288
|
+
* name: "my-app",
|
|
289
|
+
* appId: "com.example.myapp",
|
|
290
|
+
* packageManager: "pnpm",
|
|
291
|
+
* testing: "vitest",
|
|
292
|
+
* });
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
export const createApp = async (options: CreateOptions = {}): Promise<void> => {
|
|
296
|
+
p.intro("Create GTKX App");
|
|
297
|
+
|
|
298
|
+
const resolved = await promptForOptions(options);
|
|
299
|
+
const projectPath = resolve(process.cwd(), resolved.name);
|
|
300
|
+
|
|
301
|
+
const s = p.spinner();
|
|
302
|
+
s.start("Creating project structure...");
|
|
303
|
+
scaffoldProject(projectPath, resolved);
|
|
304
|
+
s.stop("Project structure created!");
|
|
305
|
+
|
|
306
|
+
const devDeps = getDevDependencies(resolved.testing);
|
|
307
|
+
await installDependencies(projectPath, resolved.name, resolved.packageManager, devDeps);
|
|
308
|
+
|
|
309
|
+
printNextSteps(resolved.name, resolved.packageManager, resolved.testing);
|
|
310
|
+
};
|