create-middag-ui 0.1.0 → 0.2.0

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,91 @@
1
+ # create-middag-ui
2
+
3
+ Bootstrap a [MIDDAG React UI](https://github.com/middag-io/middag-react) layer in your Moodle or WordPress plugin.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx create-middag-ui
9
+ ```
10
+
11
+ Or specify a custom directory:
12
+
13
+ ```bash
14
+ npx create-middag-ui mydir
15
+ ```
16
+
17
+ Run from your plugin project root. The wizard:
18
+
19
+ 1. **Auto-detects your host** — finds `version.php` (Moodle) or `wp-config.php` (WordPress)
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
24
+
25
+ Then start developing:
26
+
27
+ ```bash
28
+ cd ui
29
+ npm run dev:mock
30
+ ```
31
+
32
+ Your mock opens at `http://localhost:5174` (Moodle), `5175` (WordPress), or `5176` (Custom).
33
+
34
+ ## What it creates
35
+
36
+ ```
37
+ ui/
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
47
+ mock/
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
52
+ ```
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
+
72
+ ## After setup
73
+
74
+ Once `@middag-io/react` is installed, more commands become available from inside your UI directory:
75
+
76
+ ```bash
77
+ npx @middag-io/react doctor # Validate project setup
78
+ npx @middag-io/react dev # Start mock dev server
79
+ npx @middag-io/react add-block <t> # Scaffold a new block type
80
+ npx @middag-io/react upgrade # Check for updates
81
+ ```
82
+
83
+ ## Documentation
84
+
85
+ - **[Live Demo](https://middag-react-mock.pages.dev)** — 24 screens showing all block types
86
+ - **[Full Documentation](https://docs.middag.io)** — Getting started, host guides, API reference
87
+ - **[GitHub](https://github.com/middag-io/middag-react)** — Source code and issues
88
+
89
+ ## License
90
+
91
+ MIT
package/cli.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /* global console, process */
3
+
4
+ /**
5
+ * create-middag-ui — Bootstrap a MIDDAG React UI layer.
6
+ *
7
+ * Published on public npm so `npx create-middag-ui` works without
8
+ * GitHub Packages authentication. This package handles:
9
+ * 1. Host detection (Moodle / WordPress / Custom)
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
17
+ *
18
+ * After init, more CLI commands are available via:
19
+ * npx @middag-io/react <command>
20
+ */
21
+
22
+ import { join } from "node:path";
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();
40
+
41
+ blank();
42
+ log("Initializing MIDDAG React UI...\n");
43
+
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}`);
59
+ }
60
+
61
+ const host = HOSTS[hostKey];
62
+
63
+ // ── Step 2: Ask directory ────────────────────────────────────────────────
64
+
65
+ heading(2, TOTAL_STEPS, "Target directory");
66
+
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}/`);
73
+ } else {
74
+ const answer = await ask(` Directory name (default: ui): `);
75
+ dirName = answer || "ui";
76
+ success(`Target: ${dirName}/`);
77
+ }
78
+
79
+ const targetDir = join(cwd, dirName);
80
+
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();
89
+
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();
95
+ } else {
96
+ success("Using npm public registry (no authentication needed)");
97
+ }
98
+
99
+ // ── Step 4: Create directory ─────────────────────────────────────────────
100
+
101
+ heading(4, TOTAL_STEPS, "Creating directory");
102
+
103
+ const dirCreated = createTargetDir(targetDir);
104
+ if (!dirCreated) {
105
+ process.exit(1);
106
+ }
107
+
108
+ // ── Step 5: Scaffold config files ────────────────────────────────────────
109
+
110
+ heading(5, TOTAL_STEPS, "Scaffolding config files");
111
+
112
+ scaffoldPackageJson(targetDir, host, cwd);
113
+ scaffoldTsconfig(targetDir);
114
+ scaffoldViteConfig(targetDir, host);
115
+
116
+ // ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
117
+
118
+ heading(6, TOTAL_STEPS, "Registry setup");
119
+
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)");
125
+ }
126
+
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!");
151
+
152
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
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();
188
+ console.log(" Docs: https://docs.middag.io");
189
+ blank();
package/lib/auth.js ADDED
@@ -0,0 +1,210 @@
1
+ /* global console, process */
2
+ /**
3
+ * auth.js — GitHub token flow (ask, validate, save to ~/.npmrc).
4
+ *
5
+ * Token is stored in ~/.npmrc (global), never in project-local .npmrc.
6
+ * Two lines are written:
7
+ * @middag-io:registry=https://npm.pkg.github.com
8
+ * //npm.pkg.github.com/:_authToken=TOKEN
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { spawnSync } from "node:child_process";
15
+ import { askSecret, confirm } from "./prompts.js";
16
+ import { success, warn, error, info, createSpinner } from "./ui.js";
17
+
18
+ const REGISTRY_URL = "https://npm.pkg.github.com";
19
+ const SCOPE_LINE = `@middag-io:registry=${REGISTRY_URL}`;
20
+ const TOKEN_PREFIX = `//npm.pkg.github.com/:_authToken=`;
21
+
22
+ /**
23
+ * Check if ~/.npmrc already has a valid GitHub Packages token for @middag-io.
24
+ *
25
+ * @returns {{ hasScope: boolean, hasToken: boolean }}
26
+ */
27
+ export function checkExistingAuth() {
28
+ const npmrcPath = join(homedir(), ".npmrc");
29
+ if (!existsSync(npmrcPath)) {
30
+ return { hasScope: false, hasToken: false };
31
+ }
32
+
33
+ try {
34
+ const content = readFileSync(npmrcPath, "utf-8");
35
+ const hasScope = content.includes(SCOPE_LINE);
36
+ const hasToken = content.includes(TOKEN_PREFIX);
37
+ return { hasScope, hasToken };
38
+ } catch {
39
+ return { hasScope: false, hasToken: false };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Validate a GitHub token by running `npm whoami` against GitHub Packages.
45
+ *
46
+ * @param {string} token - GitHub personal access token
47
+ * @returns {{ valid: boolean, username: string|null }}
48
+ */
49
+ export function validateToken(token) {
50
+ try {
51
+ const result = spawnSync(
52
+ "npm",
53
+ ["whoami", `--registry=${REGISTRY_URL}`],
54
+ {
55
+ env: {
56
+ ...process.env,
57
+ // Temporarily set the token for this check
58
+ npm_config__npm_pkg_github_com__authToken: token,
59
+ },
60
+ timeout: 15000,
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ encoding: "utf-8",
63
+ },
64
+ );
65
+
66
+ if (result.status === 0 && result.stdout) {
67
+ return { valid: true, username: result.stdout.trim() };
68
+ }
69
+ return { valid: false, username: null };
70
+ } catch {
71
+ return { valid: false, username: null };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Save scope + token to ~/.npmrc (global).
77
+ * Appends only the lines that are missing.
78
+ *
79
+ * @param {string} token - GitHub personal access token
80
+ */
81
+ export function saveTokenToGlobalNpmrc(token) {
82
+ const npmrcPath = join(homedir(), ".npmrc");
83
+ let content = "";
84
+
85
+ if (existsSync(npmrcPath)) {
86
+ try {
87
+ content = readFileSync(npmrcPath, "utf-8");
88
+ } catch (err) {
89
+ throw new Error(`Cannot read ~/.npmrc: ${err.message}`, { cause: err });
90
+ }
91
+ }
92
+
93
+ // If an existing token line exists, replace it in-place
94
+ if (content.includes(TOKEN_PREFIX)) {
95
+ const updated = content.replace(
96
+ new RegExp(`${TOKEN_PREFIX.replace(/[/]/g, "\\/")}.*`, "g"),
97
+ `${TOKEN_PREFIX}${token}`,
98
+ );
99
+ try {
100
+ writeFileSync(npmrcPath, updated);
101
+ } catch (err) {
102
+ throw new Error(`Cannot write ~/.npmrc: ${err.message}`, { cause: err });
103
+ }
104
+ // Still need to add scope if missing
105
+ if (!content.includes(SCOPE_LINE)) {
106
+ try {
107
+ appendFileSync(npmrcPath, `\n${SCOPE_LINE}\n`);
108
+ } catch (err) {
109
+ throw new Error(`Cannot append to ~/.npmrc: ${err.message}`, { cause: err });
110
+ }
111
+ }
112
+ return;
113
+ }
114
+
115
+ // No existing token — append new lines
116
+ const lines = [];
117
+ if (!content.includes(SCOPE_LINE)) {
118
+ lines.push(SCOPE_LINE);
119
+ }
120
+ lines.push(`${TOKEN_PREFIX}${token}`);
121
+
122
+ const prefix = content && !content.endsWith("\n") ? "\n" : "";
123
+ try {
124
+ appendFileSync(npmrcPath, `${prefix}${lines.join("\n")}\n`);
125
+ } catch (err) {
126
+ throw new Error(`Cannot write to ~/.npmrc: ${err.message}`, { cause: err });
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Interactive GitHub token flow.
132
+ *
133
+ * 1. Ask for token (masked input)
134
+ * 2. Validate with npm whoami
135
+ * 3. Save to ~/.npmrc if valid
136
+ * 4. If invalid, offer fallback to npm publico
137
+ *
138
+ * @returns {Promise<'github'|'public'>} Which registry path was chosen
139
+ */
140
+ export async function runTokenFlow() {
141
+ // Check if already configured
142
+ const existing = checkExistingAuth();
143
+ if (existing.hasScope && existing.hasToken) {
144
+ const spinner = createSpinner("Checking existing GitHub Packages auth...");
145
+ const result = spawnSync(
146
+ "npm",
147
+ ["whoami", `--registry=${REGISTRY_URL}`],
148
+ {
149
+ timeout: 15000,
150
+ stdio: ["ignore", "pipe", "pipe"],
151
+ encoding: "utf-8",
152
+ },
153
+ );
154
+ spinner.stop();
155
+
156
+ if (result.status === 0 && result.stdout) {
157
+ success(`Already authenticated as ${result.stdout.trim()} on GitHub Packages`);
158
+ return "github";
159
+ }
160
+ warn("Existing GitHub Packages token is invalid or expired");
161
+ }
162
+
163
+ console.log("");
164
+ info("Create a GitHub Personal Access Token at:");
165
+ info(" https://github.com/settings/tokens");
166
+ info("Required scope: read:packages");
167
+ console.log("");
168
+
169
+ const token = await askSecret(" GitHub token: ");
170
+
171
+ if (!token) {
172
+ warn("No token provided");
173
+ const useFallback = await confirm("Continue without token? (uses npm public registry)", true);
174
+ if (useFallback) {
175
+ return "public";
176
+ }
177
+ process.exit(1);
178
+ }
179
+
180
+ // Validate
181
+ const spinner = createSpinner("Validating token...");
182
+ const result = validateToken(token);
183
+ if (result.valid) {
184
+ spinner.stop(`Token valid \u2014 authenticated as ${result.username}`);
185
+ } else {
186
+ spinner.fail("Token invalid or missing read:packages scope");
187
+ console.log("");
188
+ info("Make sure your token has the read:packages scope.");
189
+ info("Create one at: https://github.com/settings/tokens");
190
+ console.log("");
191
+ const useFallback = await confirm("Continue without token? (uses npm public registry)", true);
192
+ if (useFallback) {
193
+ return "public";
194
+ }
195
+ process.exit(1);
196
+ }
197
+
198
+ // Save to ~/.npmrc
199
+ try {
200
+ saveTokenToGlobalNpmrc(token);
201
+ success("Saved registry + token to ~/.npmrc (global)");
202
+ } catch (err) {
203
+ error(`Failed to save token: ${err.message}`);
204
+ warn("You can add it manually to ~/.npmrc:");
205
+ info(` ${SCOPE_LINE}`);
206
+ info(` ${TOKEN_PREFIX}<your-token>`);
207
+ }
208
+
209
+ return "github";
210
+ }
package/lib/detect.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * detect.js — Host detection (Moodle / WordPress / Custom).
3
+ *
4
+ * Checks for marker files in the working directory.
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ export const HOSTS = {
11
+ moodle: { name: "Moodle", detect: "version.php", port: 5174 },
12
+ wordpress: { name: "WordPress", detect: "wp-config.php", port: 5175 },
13
+ custom: { name: "Custom", detect: null, port: 5176 },
14
+ };
15
+
16
+ /**
17
+ * Detect host platform by checking for marker files.
18
+ *
19
+ * @param {string} cwd - Directory to check
20
+ * @returns {string|null} Host key ('moodle', 'wordpress') or null
21
+ */
22
+ export function detectHost(cwd) {
23
+ for (const [key, host] of Object.entries(HOSTS)) {
24
+ if (host.detect && existsSync(join(cwd, host.detect))) {
25
+ return key;
26
+ }
27
+ }
28
+ return null;
29
+ }
package/lib/install.js ADDED
@@ -0,0 +1,92 @@
1
+ /* global console, process */
2
+ /**
3
+ * install.js — npm install runner with spinner.
4
+ *
5
+ * Spawns `npm install` in the target directory, shows a spinner,
6
+ * and surfaces errors clearly if it fails.
7
+ */
8
+
9
+ import { spawn } from "node:child_process";
10
+ import { createSpinner, error, warn, info } from "./ui.js";
11
+
12
+ /**
13
+ * Run `npm install` in the target directory.
14
+ *
15
+ * Shows a spinner while running. On failure, prints the npm error
16
+ * output and suggests troubleshooting steps.
17
+ *
18
+ * @param {string} targetDir - Absolute path to the directory
19
+ * @param {'github'|'public'} registryPath - Which registry was chosen
20
+ * @returns {Promise<boolean>} true if install succeeded
21
+ */
22
+ export async function runNpmInstall(targetDir, registryPath) {
23
+ const registryLabel =
24
+ registryPath === "github" ? "GitHub Packages" : "npm public registry";
25
+
26
+ const spinner = createSpinner(
27
+ `Installing dependencies from ${registryLabel}...`,
28
+ );
29
+
30
+ return new Promise((resolve) => {
31
+ const child = spawn("npm", ["install"], {
32
+ cwd: targetDir,
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ env: process.env,
35
+ });
36
+
37
+ let stderr = "";
38
+
39
+ child.stdout.on("data", () => {
40
+ // consume stdout silently
41
+ });
42
+
43
+ child.stderr.on("data", (data) => {
44
+ stderr += data.toString();
45
+ });
46
+
47
+ child.on("error", (err) => {
48
+ spinner.fail("npm install failed to start");
49
+ error(`Could not run npm: ${err.message}`);
50
+ info("Make sure npm is installed and in your PATH.");
51
+ resolve(false);
52
+ });
53
+
54
+ child.on("close", (code) => {
55
+ if (code === 0) {
56
+ spinner.stop("Dependencies installed");
57
+ resolve(true);
58
+ } else {
59
+ spinner.fail("npm install failed");
60
+
61
+ // Show the npm error, trimmed
62
+ if (stderr) {
63
+ const lines = stderr.trim().split("\n");
64
+ // Show last 15 lines max (most relevant npm errors are at the end)
65
+ const relevant = lines.slice(-15);
66
+ console.log("");
67
+ for (const line of relevant) {
68
+ console.log(` ${line}`);
69
+ }
70
+ console.log("");
71
+ }
72
+
73
+ // Suggest troubleshooting based on path
74
+ if (registryPath === "github") {
75
+ warn("Troubleshooting:");
76
+ info(" 1. Check your GitHub token has read:packages scope");
77
+ info(" 2. Verify token in ~/.npmrc:");
78
+ info(" cat ~/.npmrc | grep github");
79
+ info(" 3. Try: npm whoami --registry=https://npm.pkg.github.com");
80
+ info(" 4. Or re-run: npx create-middag-ui (choose 'No' for GitHub access)");
81
+ } else {
82
+ warn("Troubleshooting:");
83
+ info(" 1. Check your internet connection");
84
+ info(" 2. Try running manually: cd " + targetDir + " && npm install");
85
+ info(" 3. Check npm registry: npm config get registry");
86
+ }
87
+
88
+ resolve(false);
89
+ }
90
+ });
91
+ });
92
+ }