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 +91 -0
- package/cli.js +189 -0
- package/lib/auth.js +210 -0
- package/lib/detect.js +29 -0
- package/lib/install.js +92 -0
- package/lib/prompts.js +120 -0
- package/lib/scaffold.js +386 -0
- package/lib/ui.js +85 -0
- package/package.json +5 -3
- package/index.js +0 -327
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
|
+
}
|