create-middag-ui 0.1.4 → 0.2.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 CHANGED
@@ -8,17 +8,24 @@ Bootstrap a [MIDDAG React UI](https://github.com/middag-io/middag-react) layer i
8
8
  npx create-middag-ui
9
9
  ```
10
10
 
11
+ Or specify a custom directory:
12
+
13
+ ```bash
14
+ npx create-middag-ui mydir
15
+ ```
16
+
11
17
  Run from your plugin project root. The wizard:
12
18
 
13
19
  1. **Auto-detects your host** — finds `version.php` (Moodle) or `wp-config.php` (WordPress)
14
- 2. **Scaffolds `ui/`**creates `package.json`, `tsconfig.json`, Vite config, and `.npmrc`
15
- 3. **Generates a hello-world mock** — a working PageContract with a metric card and data table
20
+ 2. **Asks for target directory** default `ui/`, or pass as CLI argument
21
+ 3. **Configures registry** — GitHub Packages (with source maps) or npm public (no auth needed)
22
+ 4. **Scaffolds everything** — config files, demo components, mock environment
23
+ 5. **Installs dependencies** — runs `npm install` automatically with progress feedback
16
24
 
17
- Then install and run:
25
+ Then start developing:
18
26
 
19
27
  ```bash
20
28
  cd ui
21
- npm install
22
29
  npm run dev:mock
23
30
  ```
24
31
 
@@ -28,21 +35,43 @@ Your mock opens at `http://localhost:5174` (Moodle), `5175` (WordPress), or `517
28
35
 
29
36
  ```
30
37
  ui/
31
- .npmrc # GitHub Packages registry for @middag-io scope
32
- package.json # Dependencies and scripts
33
- tsconfig.json # TypeScript config with path aliases
34
- vite.mock.config.ts # Vite dev server config for mock build
35
- src/ # Your components (empty, ready for custom blocks)
38
+ package.json # Dependencies and scripts
39
+ tsconfig.json # TypeScript config with path aliases
40
+ vite.mock.config.ts # Vite dev server config for mock build
41
+ src/
42
+ blocks/
43
+ hello-block.tsx # Custom block example (rename me!)
44
+ components/
45
+ greeting.tsx # Standalone component example (rename me!)
46
+ contracts.ts # PageContract type re-export
36
47
  mock/
37
- index.html # HTML entry point
38
- main.tsx # React entry with registerDefaults() + ContractPage
39
- hello-contract.ts # Example PageContract (metric card + data table)
40
- tailwind.css # Tailwind CSS import
48
+ index.html # HTML entry point
49
+ main.tsx # React entry with registerDefaults() + ContractPage
50
+ hello-contract.ts # Example PageContract (metric card + data table)
51
+ tailwind.css # Tailwind CSS import
41
52
  ```
42
53
 
54
+ ## Dual Registry
55
+
56
+ `@middag-io/react` is available from two registries:
57
+
58
+ ### npm public (default, no auth)
59
+
60
+ Choose "No" when asked about GitHub access. Dependencies install from the public npm registry with zero configuration.
61
+
62
+ ### GitHub Packages (with source maps)
63
+
64
+ Choose "Yes" when asked about GitHub access. The wizard will:
65
+
66
+ 1. Ask for your GitHub Personal Access Token
67
+ 2. Validate the token against GitHub Packages
68
+ 3. Save the registry + token to `~/.npmrc` (global, not project-local)
69
+
70
+ Create a token at [github.com/settings/tokens](https://github.com/settings/tokens) with the `read:packages` scope.
71
+
43
72
  ## After setup
44
73
 
45
- Once `@middag-io/react` is installed, more commands become available from inside `ui/`:
74
+ Once `@middag-io/react` is installed, more commands become available from inside your UI directory:
46
75
 
47
76
  ```bash
48
77
  npx @middag-io/react doctor # Validate project setup
@@ -51,16 +80,6 @@ npx @middag-io/react add-block <t> # Scaffold a new block type
51
80
  npx @middag-io/react upgrade # Check for updates
52
81
  ```
53
82
 
54
- ## Authentication
55
-
56
- `@middag-io/react` is hosted on GitHub Packages. The wizard creates `.npmrc` with the registry, but you need to add your GitHub token:
57
-
58
- ```bash
59
- echo '//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN' >> ui/.npmrc
60
- ```
61
-
62
- Create a token at [github.com/settings/tokens](https://github.com/settings/tokens) with the `read:packages` scope.
63
-
64
83
  ## Documentation
65
84
 
66
85
  - **[Live Demo](https://middag-react-mock.pages.dev)** — 24 screens showing all block types
package/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ /* global console, process */
2
3
 
3
4
  /**
4
5
  * create-middag-ui — Bootstrap a MIDDAG React UI layer.
@@ -6,322 +7,183 @@
6
7
  * Published on public npm so `npx create-middag-ui` works without
7
8
  * GitHub Packages authentication. This package handles:
8
9
  * 1. Host detection (Moodle / WordPress / Custom)
9
- * 2. ui/ directory scaffolding
10
- * 3. .npmrc configuration for @middag-io scope
11
- * 4. Hello-world PageContract generation
12
- * 5. npm install (triggers @middag-io/react download from GitHub Packages)
10
+ * 2. Directory selection (default: ui/, or CLI arg)
11
+ * 3. GitHub access check (dual registry: GitHub Packages or npm public)
12
+ * 4. Directory + config scaffolding
13
+ * 5. Demo files (custom block, standalone component, type re-export)
14
+ * 6. Mock files (hello-contract, entry point, HTML, CSS)
15
+ * 7. npm install with spinner
16
+ * 8. Summary with next steps
13
17
  *
14
- * After init, the remaining CLI commands (doctor, dev, add-block, upgrade)
15
- * are available via: npx @middag-io/react <command>
18
+ * After init, more CLI commands are available via:
19
+ * npx @middag-io/react <command>
16
20
  */
17
21
 
18
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
19
22
  import { join } from "node:path";
20
- import { createInterface } from "node:readline";
21
-
22
- const HOSTS = {
23
- moodle: { name: "Moodle", detect: "version.php", port: 5174 },
24
- wordpress: { name: "WordPress", detect: "wp-config.php", port: 5175 },
25
- custom: { name: "Custom", detect: null, port: 5176 },
26
- };
27
-
28
- // ── Helpers ────────────────────────────────────────────────────────────────
29
-
30
- function ask(question) {
31
- const rl = createInterface({ input: process.stdin, output: process.stdout });
32
- return new Promise((r) =>
33
- rl.question(question, (a) => {
34
- rl.close();
35
- r(a.trim());
36
- }),
37
- );
38
- }
23
+ import { detectHost, HOSTS } from "./lib/detect.js";
24
+ import { ask, select, confirm } from "./lib/prompts.js";
25
+ import { runTokenFlow } from "./lib/auth.js";
26
+ import {
27
+ createTargetDir,
28
+ scaffoldPackageJson,
29
+ scaffoldTsconfig,
30
+ scaffoldViteConfig,
31
+ scaffoldDemoFiles,
32
+ scaffoldMockFiles,
33
+ } from "./lib/scaffold.js";
34
+ import { runNpmInstall } from "./lib/install.js";
35
+ import { log, success, heading, blank, info } from "./lib/ui.js";
36
+
37
+ const TOTAL_STEPS = 10;
38
+ const cwd = process.cwd();
39
+ const startTime = Date.now();
39
40
 
40
- function log(msg) {
41
- console.log(`\x1b[36mcreate-middag-ui\x1b[0m ${msg}`);
42
- }
43
- function success(msg) {
44
- console.log(` \x1b[32m✓\x1b[0m ${msg}`);
45
- }
46
- function warn(msg) {
47
- console.log(` \x1b[33m⚠\x1b[0m ${msg}`);
48
- }
41
+ blank();
42
+ log("Initializing MIDDAG React UI...\n");
49
43
 
50
- function detectHost(cwd) {
51
- for (const [key, host] of Object.entries(HOSTS)) {
52
- if (host.detect && existsSync(join(cwd, host.detect))) return key;
53
- }
54
- return null;
44
+ // ── Step 1: Detect host ──────────────────────────────────────────────────
45
+
46
+ heading(1, TOTAL_STEPS, "Detecting host platform");
47
+
48
+ let hostKey = detectHost(cwd);
49
+ if (hostKey) {
50
+ success(`Detected: ${HOSTS[hostKey].name} (found ${HOSTS[hostKey].detect})`);
51
+ } else {
52
+ info("Could not auto-detect host platform.");
53
+ hostKey = await select("Select platform", [
54
+ { label: "Moodle", value: "moodle" },
55
+ { label: "WordPress", value: "wordpress" },
56
+ { label: "Custom / Other", value: "custom" },
57
+ ]);
58
+ success(`Selected: ${HOSTS[hostKey].name}`);
55
59
  }
56
60
 
57
- // ── Main ───────────────────────────────────────────────────────────────────
61
+ const host = HOSTS[hostKey];
58
62
 
59
- const cwd = process.cwd();
60
- const startTime = Date.now();
63
+ // ── Step 2: Ask directory ────────────────────────────────────────────────
61
64
 
62
- log("Initializing MIDDAG React UI in your project...\n");
65
+ heading(2, TOTAL_STEPS, "Target directory");
63
66
 
64
- // Step 1: Detect host
65
- let host = detectHost(cwd);
66
- if (host) {
67
- success(`Detected host: ${HOSTS[host].name} (found ${HOSTS[host].detect})`);
67
+ const cliArg = process.argv[2];
68
+ let dirName;
69
+
70
+ if (cliArg && !cliArg.startsWith("-")) {
71
+ dirName = cliArg;
72
+ success(`Using directory from argument: ${dirName}/`);
68
73
  } else {
69
- log("Could not auto-detect host platform.");
70
- console.log(" 1) Moodle");
71
- console.log(" 2) WordPress");
72
- console.log(" 3) Custom / Other");
73
- const choice = await ask("\n Select platform [1-3]: ");
74
- host =
75
- ["moodle", "wordpress", "custom"][parseInt(choice, 10) - 1] || "custom";
76
- success(`Selected: ${HOSTS[host].name}`);
74
+ const answer = await ask(` Directory name (default: ui): `);
75
+ dirName = answer || "ui";
76
+ success(`Target: ${dirName}/`);
77
77
  }
78
78
 
79
- const uiDir = join(cwd, "ui");
79
+ const targetDir = join(cwd, dirName);
80
80
 
81
- // Step 2: Create ui/ directory
82
- if (existsSync(uiDir)) {
83
- warn("ui/ directory already exists — skipping scaffold");
84
- } else {
85
- mkdirSync(uiDir, { recursive: true });
86
- success("Created ui/");
87
- }
81
+ // ── Step 3: Ask GitHub access ────────────────────────────────────────────
82
+
83
+ heading(3, TOTAL_STEPS, "Registry configuration");
84
+
85
+ info("@middag-io/react can be installed from:");
86
+ info(" a) GitHub Packages (with source maps, requires token)");
87
+ info(" b) npm public registry (compiled only, no auth needed)");
88
+ blank();
88
89
 
89
- // Step 3: Configure .npmrc with GitHub Packages registry
90
- const npmrcPath = join(uiDir, ".npmrc");
91
- if (!existsSync(npmrcPath)) {
92
- writeFileSync(
93
- npmrcPath,
94
- "@middag-io:registry=https://npm.pkg.github.com\n",
95
- );
96
- success("Created .npmrc with GitHub Packages registry");
97
- warn(
98
- "Add your GitHub token: echo '//npm.pkg.github.com/:_authToken=YOUR_TOKEN' >> ui/.npmrc",
99
- );
90
+ const hasGitHubAccess = await confirm("Do you have GitHub access to middag-io?", false);
91
+
92
+ let registryPath = "public";
93
+ if (hasGitHubAccess) {
94
+ registryPath = await runTokenFlow();
100
95
  } else {
101
- success(".npmrc already exists");
96
+ success("Using npm public registry (no authentication needed)");
102
97
  }
103
98
 
104
- // Step 4: Create package.json
105
- const pkgPath = join(uiDir, "package.json");
106
- if (!existsSync(pkgPath)) {
107
- const projectName = cwd.split("/").pop() || "project";
108
- const pkg = {
109
- name: `${projectName}-ui`,
110
- private: true,
111
- type: "module",
112
- scripts: {
113
- dev: "vite --config vite.mock.config.ts",
114
- "dev:mock": "vite --config vite.mock.config.ts",
115
- build: "vite build",
116
- "build:mock": "vite build --config vite.mock.config.ts",
117
- typecheck: "tsc --noEmit",
118
- lint: "eslint .",
119
- "lint:fix": "eslint . --fix",
120
- },
121
- dependencies: {
122
- "@middag-io/react": "^0.1.0",
123
- },
124
- devDependencies: {
125
- "@types/react": "^19.0.0",
126
- "@types/react-dom": "^19.0.0",
127
- react: "^19.0.0",
128
- "react-dom": "^19.0.0",
129
- "@inertiajs/react": "^2.0.0",
130
- "@inertiajs/core": "^2.0.0",
131
- typescript: "^5.7.0",
132
- vite: "^6.0.0",
133
- "@vitejs/plugin-react": "^4.0.0",
134
- tailwindcss: "^4.0.0",
135
- "@tailwindcss/vite": "^4.0.0",
136
- },
137
- };
138
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
139
- success("Created package.json");
140
- }
99
+ // ── Step 4: Create directory ─────────────────────────────────────────────
100
+
101
+ heading(4, TOTAL_STEPS, "Creating directory");
141
102
 
142
- // Step 5: Create tsconfig.json
143
- const tsconfigPath = join(uiDir, "tsconfig.json");
144
- if (!existsSync(tsconfigPath)) {
145
- const tsconfig = {
146
- compilerOptions: {
147
- target: "ES2022",
148
- module: "ESNext",
149
- moduleResolution: "bundler",
150
- jsx: "react-jsx",
151
- strict: true,
152
- noUnusedLocals: true,
153
- noUnusedParameters: true,
154
- skipLibCheck: true,
155
- paths: { "@/*": ["./src/*"], "@mock/*": ["./mock/*"] },
156
- baseUrl: ".",
157
- },
158
- include: ["src", "mock"],
159
- };
160
- writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
161
- success("Created tsconfig.json");
103
+ const dirCreated = createTargetDir(targetDir);
104
+ if (!dirCreated) {
105
+ process.exit(1);
162
106
  }
163
107
 
164
- // Step 6: Create hello-world contract
165
- const mockDir = join(uiDir, "mock");
166
- mkdirSync(join(uiDir, "src"), { recursive: true });
167
- mkdirSync(mockDir, { recursive: true });
108
+ // ── Step 5: Scaffold config files ────────────────────────────────────────
168
109
 
169
- const helloContractPath = join(mockDir, "hello-contract.ts");
170
- if (!existsSync(helloContractPath)) {
171
- writeFileSync(
172
- helloContractPath,
173
- `import type { PageContract } from "@middag-io/react";
110
+ heading(5, TOTAL_STEPS, "Scaffolding config files");
174
111
 
175
- /**
176
- * Hello World contract — a minimal PageContract to verify your setup.
177
- *
178
- * This is what your ${HOSTS[host].name} backend will send via Inertia.
179
- * Replace this with real data from your server.
180
- */
181
- export const helloContract: PageContract = {
182
- shell: "admin",
183
- meta: {
184
- title: "Hello MIDDAG",
185
- breadcrumbs: [
186
- { label: "Home", href: "/" },
187
- { label: "Hello", href: "/hello" },
188
- ],
189
- },
190
- layout: {
191
- template: "stack",
192
- regions: {
193
- content: [
194
- {
195
- key: "welcome_metrics",
196
- type: "metric_card",
197
- data: {
198
- title: "Setup Complete",
199
- value: "1",
200
- subtitle: "@middag-io/react is working",
201
- trend: { direction: "up", value: "100%", label: "Ready" },
202
- },
203
- },
204
- {
205
- key: "hello_table",
206
- type: "dense_table",
207
- data: {
208
- title: "Example Data",
209
- columns: [
210
- { key: "id", label: "ID", sortable: true },
211
- { key: "name", label: "Name", sortable: true },
212
- { key: "status", label: "Status" },
213
- ],
214
- rows: [
215
- { id: 1, name: "First item", status: "Active" },
216
- { id: 2, name: "Second item", status: "Draft" },
217
- { id: 3, name: "Third item", status: "Active" },
218
- ],
219
- pagination: { current: 1, total: 1, perPage: 10, totalRows: 3 },
220
- },
221
- },
222
- ],
223
- },
224
- },
225
- };
226
- `,
227
- );
228
- success("Created mock/hello-contract.ts (hello-world PageContract)");
229
- }
112
+ scaffoldPackageJson(targetDir, host, cwd);
113
+ scaffoldTsconfig(targetDir);
114
+ scaffoldViteConfig(targetDir, host);
230
115
 
231
- // Step 7: Create mock entry point
232
- const mockMainPath = join(mockDir, "main.tsx");
233
- if (!existsSync(mockMainPath)) {
234
- writeFileSync(
235
- mockMainPath,
236
- `import { StrictMode } from "react";
237
- import { createRoot } from "react-dom/client";
238
- import { ContractPage, registerDefaults } from "@middag-io/react";
239
- import "@middag-io/react/style.css";
240
- import "./tailwind.css";
241
- import { helloContract } from "./hello-contract";
242
-
243
- // Register all default shells, layouts, and blocks
244
- registerDefaults();
245
-
246
- createRoot(document.getElementById("root")!).render(
247
- <StrictMode>
248
- <ContractPage contract={helloContract} />
249
- </StrictMode>,
250
- );
251
- `,
252
- );
253
- success("Created mock/main.tsx");
254
- }
116
+ // ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
255
117
 
256
- const mockIndexPath = join(mockDir, "index.html");
257
- if (!existsSync(mockIndexPath)) {
258
- writeFileSync(
259
- mockIndexPath,
260
- `<!doctype html>
261
- <html lang="en">
262
- <head>
263
- <meta charset="UTF-8" />
264
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
265
- <title>MIDDAG React UI — Mock</title>
266
- </head>
267
- <body>
268
- <div id="root"></div>
269
- <script type="module" src="./main.tsx"></script>
270
- </body>
271
- </html>
272
- `,
273
- );
274
- success("Created mock/index.html");
275
- }
118
+ heading(6, TOTAL_STEPS, "Registry setup");
276
119
 
277
- // Step 8: Create vite mock config
278
- const viteConfigPath = join(uiDir, "vite.mock.config.ts");
279
- if (!existsSync(viteConfigPath)) {
280
- writeFileSync(
281
- viteConfigPath,
282
- `import { defineConfig } from "vite";
283
- import react from "@vitejs/plugin-react";
284
- import tailwindcss from "@tailwindcss/vite";
285
- import { resolve } from "path";
286
-
287
- export default defineConfig({
288
- plugins: [react(), tailwindcss()],
289
- root: "mock",
290
- server: { port: ${HOSTS[host].port} },
291
- resolve: {
292
- alias: {
293
- "@/": resolve(__dirname, "src") + "/",
294
- "@mock/": resolve(__dirname, "mock") + "/",
295
- },
296
- },
297
- });
298
- `,
299
- );
300
- success("Created vite.mock.config.ts");
120
+ if (registryPath === "github") {
121
+ success("GitHub Packages registry configured in ~/.npmrc (global)");
122
+ info("Token was saved during authentication step");
123
+ } else {
124
+ success("No registry config needed (using npm public)");
301
125
  }
302
126
 
303
- // Step 9: Create tailwind CSS
304
- const cssPath = join(mockDir, "tailwind.css");
305
- if (!existsSync(cssPath)) {
306
- writeFileSync(cssPath, '@import "tailwindcss";\n');
307
- success("Created mock/tailwind.css");
308
- }
127
+ // ── Step 7: Scaffold demo files in src/ ──────────────────────────────────
128
+
129
+ heading(7, TOTAL_STEPS, "Creating demo files");
130
+
131
+ scaffoldDemoFiles(targetDir);
132
+
133
+ // ── Step 8: Scaffold mock files ──────────────────────────────────────────
134
+
135
+ heading(8, TOTAL_STEPS, "Creating mock environment");
136
+
137
+ scaffoldMockFiles(targetDir);
138
+
139
+ // ── Step 9: npm install ──────────────────────────────────────────────────
140
+
141
+ heading(9, TOTAL_STEPS, "Installing dependencies");
142
+
143
+ info("This may take a minute...");
144
+ blank();
145
+
146
+ const installOk = await runNpmInstall(targetDir, registryPath);
147
+
148
+ // ── Step 10: Summary ─────────────────────────────────────────────────────
149
+
150
+ heading(10, TOTAL_STEPS, "Done!");
309
151
 
310
- // Summary
311
152
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
312
- console.log("");
313
- log(`Done in ${elapsed}s!\n`);
314
- console.log(" Next steps:");
315
- console.log(" cd ui");
316
- console.log(" npm install");
317
- console.log(" npm run dev:mock\n");
318
- console.log(` Your mock will run at http://localhost:${HOSTS[host].port}`);
319
- console.log("");
320
- console.log(" After install, more commands become available:");
321
- console.log(" npx @middag-io/react doctor # validate setup");
322
- console.log(" npx @middag-io/react dev # start mock server");
323
- console.log(" npx @middag-io/react add-block <t> # scaffold a block");
324
- console.log(" npx @middag-io/react upgrade # check for updates");
325
- console.log("");
153
+ blank();
154
+
155
+ if (installOk) {
156
+ log(`MIDDAG React UI ready in ${dirName}/ (${elapsed}s)\n`);
157
+ console.log(" Start developing:");
158
+ console.log(` cd ${dirName}`);
159
+ console.log(` npm run dev:mock \u2192 mock server at http://localhost:${host.port}`);
160
+ } else {
161
+ log(`Scaffold complete in ${dirName}/ (${elapsed}s) \u2014 install failed\n`);
162
+ console.log(" To retry install:");
163
+ console.log(` cd ${dirName}`);
164
+ console.log(" npm install");
165
+ console.log(` npm run dev:mock \u2192 mock server at http://localhost:${host.port}`);
166
+ }
167
+
168
+ blank();
169
+ console.log(" Your scaffold includes:");
170
+ console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
171
+ console.log(" src/components/greeting.tsx \u2190 standalone component (rename me!)");
172
+ console.log(" src/contracts.ts \u2190 PageContract type re-export");
173
+ console.log(" mock/hello-contract.ts \u2190 example PageContract with data");
174
+
175
+ blank();
176
+ console.log(` Integrate with your ${host.name} plugin:`);
177
+ console.log(" 1. Import { ContractPage } from '@middag-io/react'");
178
+ console.log(" 2. Pass your Inertia page props as the contract");
179
+ console.log(" 3. See: https://docs.middag.io/getting-started");
180
+
181
+ blank();
182
+ console.log(" More commands (after install):");
183
+ console.log(" npx @middag-io/react doctor \u2192 validate setup");
184
+ console.log(" npx @middag-io/react add-block \u2192 scaffold new block type");
185
+ console.log(" npx @middag-io/react upgrade \u2192 check for updates");
186
+
187
+ blank();
326
188
  console.log(" Docs: https://docs.middag.io");
327
- console.log("");
189
+ blank();