@mutka-explorer/create 1.0.0-rc.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 ADDED
@@ -0,0 +1,48 @@
1
+ # @mutka-explorer/create
2
+
3
+ Scaffold a new [Mutka](https://github.com/ilianAZZ/mutka) module — a typed
4
+ TypeScript project wired to [`@mutka-explorer/module`](https://www.npmjs.com/package/@mutka-explorer/module)
5
+ that builds to a single self-contained ESM file (what Mutka loads).
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npm create @mutka-explorer@latest my-module
11
+ # or
12
+ npx @mutka-explorer/create my-module
13
+ ```
14
+
15
+ It prompts for the module id (`author.name`), display name, GitHub username, and
16
+ permissions, then generates:
17
+
18
+ ```text
19
+ my-module/
20
+ src/index.ts ← typed skeleton with a working command
21
+ package.json ← pins @mutka-explorer/module + a tsup build
22
+ tsconfig.json
23
+ mutka.config.json ← points GitHub discovery at dist/index.js
24
+ scripts/dev-install.mjs
25
+ README.md
26
+ ```
27
+
28
+ ### Non-interactive
29
+
30
+ ```bash
31
+ npm create @mutka-explorer@latest my-module -- \
32
+ --yes --author you --name "My Module" --permissions "fs:read,ui" --no-install
33
+ ```
34
+
35
+ Flags: `--id`, `--name`, `--description`, `--author`, `--permissions` (comma-separated),
36
+ `--yes` (accept defaults), `--no-install`, `--pm <npm|pnpm|yarn>`.
37
+
38
+ ## After scaffolding
39
+
40
+ ```bash
41
+ cd my-module
42
+ npm install
43
+ npm run install:local # build + copy into ~/.mutka/modules/<id>/, then reload Mutka
44
+ ```
45
+
46
+ Edit `src/index.ts` — `host` is fully typed. The module runs isolated in a Web
47
+ Worker (no DOM, no native network; use `host.net`). See the
48
+ [developer guide](https://github.com/ilianAZZ/mutka/blob/main/COMMUNITY_MODULES.md).
package/bin/cli.mjs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Entry point for `npm create @mutka-explorer` / `npx @mutka-explorer/create`.
3
+ import { main } from "../lib/main.mjs";
4
+
5
+ main(process.argv.slice(2)).catch((err) => {
6
+ console.error(`\n✖ ${err instanceof Error ? err.message : String(err)}`);
7
+ process.exit(1);
8
+ });
package/lib/main.mjs ADDED
@@ -0,0 +1,109 @@
1
+ // Orchestrates the scaffolder: parse args, fill gaps interactively, validate,
2
+ // then write the project and install. Kept dependency-free.
3
+ import { parseArgs } from "node:util";
4
+ import { readFileSync } from "node:fs";
5
+ import { resolve, basename, dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { ask, confirm } from "./prompt.mjs";
8
+ import { PERMISSIONS } from "./templates.mjs";
9
+ import { writeProject, installDeps, detectPackageManager } from "./scaffold.mjs";
10
+
11
+ const ID_RE = /^[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*$/;
12
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
13
+
14
+ /** This CLI's own version → the @mutka-explorer/module version we pin (lockstep). */
15
+ function typesVersion() {
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ const v = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version;
18
+ return !v || v === "0.0.0" ? "latest" : `^${v}`;
19
+ }
20
+
21
+ export async function main(argv) {
22
+ // `--no-install` isn't native to parseArgs; pull it out before parsing.
23
+ const skipInstall = argv.includes("--no-install");
24
+ const args = argv.filter((a) => a !== "--no-install");
25
+
26
+ const { values, positionals } = parseArgs({
27
+ args,
28
+ allowPositionals: true,
29
+ options: {
30
+ id: { type: "string" },
31
+ name: { type: "string" },
32
+ description: { type: "string" },
33
+ author: { type: "string" },
34
+ permissions: { type: "string" },
35
+ yes: { type: "boolean", short: "y", default: false },
36
+ pm: { type: "string" },
37
+ },
38
+ });
39
+ const auto = values.yes;
40
+
41
+ console.log("\n🧩 create-mutka-module\n");
42
+
43
+ // Target directory (positional, or asked, defaulting to a name-derived slug).
44
+ let dir = positionals[0];
45
+ if (!dir && !auto) dir = await ask("Project directory", "my-mutka-module");
46
+ dir = dir || "my-mutka-module";
47
+ const dirName = basename(resolve(dir));
48
+
49
+ const defaultName = values.name ?? (auto ? dirName : await ask("Display name", dirName));
50
+ const authorGithub =
51
+ values.author ?? (auto ? "" : await ask("Your GitHub username", ""));
52
+ const defaultId =
53
+ values.id ?? `${slug(authorGithub) || "you"}.${slug(defaultName) || dirName}`;
54
+ const id = auto ? defaultId : await ask("Module id (author.name)", defaultId);
55
+ if (!ID_RE.test(id)) {
56
+ throw new Error(`invalid module id "${id}" — use the form author.name (lowercase, e.g. you.my-module)`);
57
+ }
58
+
59
+ const description =
60
+ values.description ?? (auto ? "A Mutka module." : await ask("Description", "A Mutka module."));
61
+
62
+ let permissions;
63
+ if (values.permissions !== undefined) {
64
+ permissions = values.permissions.split(",").map((p) => p.trim()).filter(Boolean);
65
+ } else if (auto) {
66
+ permissions = ["fs:read"];
67
+ } else {
68
+ console.log(`\n Available permissions: ${PERMISSIONS.join(", ")}`);
69
+ const raw = await ask("Permissions (comma-separated)", "fs:read");
70
+ permissions = raw.split(",").map((p) => p.trim()).filter(Boolean);
71
+ }
72
+ const unknown = permissions.filter((p) => !PERMISSIONS.includes(p));
73
+ if (unknown.length) throw new Error(`unknown permission(s): ${unknown.join(", ")}`);
74
+
75
+ const cfg = {
76
+ id,
77
+ name: defaultName,
78
+ description,
79
+ authorName: authorGithub || "",
80
+ authorGithub,
81
+ permissions,
82
+ pkgName: slug(dirName) || "mutka-module",
83
+ typesVersion: typesVersion(),
84
+ };
85
+
86
+ const target = resolve(dir);
87
+ writeProject(target, cfg);
88
+ console.log(`\n✓ created ${dir}/ (id: ${id}, permissions: ${permissions.join(", ")})`);
89
+
90
+ const doInstall = skipInstall ? false : auto ? true : await confirm("Install dependencies now?", true);
91
+ if (doInstall) {
92
+ const pm = values.pm ?? detectPackageManager();
93
+ console.log(`\nInstalling with ${pm}…\n`);
94
+ try {
95
+ installDeps(target, pm);
96
+ } catch {
97
+ console.log(`\n⚠ install failed — run it yourself: cd ${dir} && ${pm} install`);
98
+ }
99
+ }
100
+
101
+ console.log(`
102
+ Next steps:
103
+ cd ${dir}${doInstall ? "" : "\n npm install"}
104
+ npm run install:local # build + load into Mutka, then reload the app
105
+ # edit src/index.ts — host is fully typed
106
+
107
+ Docs: https://github.com/ilianAZZ/mutka/blob/main/COMMUNITY_MODULES.md
108
+ `);
109
+ }
package/lib/prompt.mjs ADDED
@@ -0,0 +1,23 @@
1
+ // Minimal interactive prompts over Node's built-in readline — no dependencies,
2
+ // so the CLI runs instantly via npx with nothing to install.
3
+ import { createInterface } from "node:readline/promises";
4
+ import { stdin, stdout } from "node:process";
5
+
6
+ /** Ask a free-text question with an optional default shown in brackets. */
7
+ export async function ask(question, fallback = "") {
8
+ const rl = createInterface({ input: stdin, output: stdout });
9
+ try {
10
+ const hint = fallback ? ` (${fallback})` : "";
11
+ const answer = (await rl.question(`${question}${hint}: `)).trim();
12
+ return answer || fallback;
13
+ } finally {
14
+ rl.close();
15
+ }
16
+ }
17
+
18
+ /** Ask a yes/no question. Returns a boolean; `def` is used on empty input. */
19
+ export async function confirm(question, def = true) {
20
+ const ans = (await ask(`${question} ${def ? "[Y/n]" : "[y/N]"}`)).toLowerCase();
21
+ if (!ans) return def;
22
+ return ans.startsWith("y");
23
+ }
@@ -0,0 +1,44 @@
1
+ // Writes the project files to disk and (optionally) installs dependencies.
2
+ import { mkdirSync, writeFileSync, existsSync, readdirSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ import {
6
+ indexTs, packageJson, tsconfigJson, mutkaConfigJson, gitignore, devInstallMjs, readme,
7
+ } from "./templates.mjs";
8
+
9
+ /** Detect the package manager that invoked us (npm create / yarn create / pnpm). */
10
+ export function detectPackageManager() {
11
+ const ua = process.env.npm_config_user_agent ?? "";
12
+ if (ua.startsWith("pnpm")) return "pnpm";
13
+ if (ua.startsWith("yarn")) return "yarn";
14
+ return "npm";
15
+ }
16
+
17
+ function writeFileEnsuring(path, contents) {
18
+ mkdirSync(dirname(path), { recursive: true });
19
+ writeFileSync(path, contents);
20
+ }
21
+
22
+ /** Create the project directory tree. Throws if the target exists and is non-empty. */
23
+ export function writeProject(dir, cfg) {
24
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
25
+ throw new Error(`target directory "${dir}" already exists and is not empty`);
26
+ }
27
+ const files = {
28
+ "package.json": packageJson(cfg),
29
+ "tsconfig.json": tsconfigJson(),
30
+ "mutka.config.json": mutkaConfigJson(),
31
+ ".gitignore": gitignore(),
32
+ "README.md": readme(cfg),
33
+ "src/index.ts": indexTs(cfg),
34
+ "scripts/dev-install.mjs": devInstallMjs(),
35
+ };
36
+ for (const [rel, contents] of Object.entries(files)) {
37
+ writeFileEnsuring(join(dir, rel), contents);
38
+ }
39
+ }
40
+
41
+ /** Install dependencies in `dir` with the given package manager. Best-effort. */
42
+ export function installDeps(dir, pm) {
43
+ execSync(`${pm} install`, { cwd: dir, stdio: "inherit" });
44
+ }
@@ -0,0 +1,159 @@
1
+ // The files a scaffolded module project is made of. Each function takes the
2
+ // resolved config and returns file contents as a string. Kept here so `main`/
3
+ // `scaffold` stay about orchestration, not text.
4
+
5
+ /** Every permission a module may declare (mirrors ModulePermission in the app). */
6
+ export const PERMISSIONS = [
7
+ "fs:read", "fs:write", "fs:temp",
8
+ "clipboard:read", "clipboard:write",
9
+ "navigation", "view", "dialog",
10
+ "network:public", "network:local",
11
+ "storage", "secrets", "ui", "discovery", "shell",
12
+ ];
13
+
14
+ const permsLiteral = (perms) => perms.map((p) => `"${p}"`).join(", ");
15
+
16
+ export function indexTs(cfg) {
17
+ return `import type { SandboxModuleDef } from "@mutka-explorer/module";
18
+
19
+ // A Mutka module is a single self-contained ESM file. It imports NOTHING at
20
+ // runtime — it reaches the system only through \`host\`, and every host call is
21
+ // checked against the permissions declared below. \`import type\` above is erased
22
+ // at compile time, so the built file stays self-contained.
23
+ const mod: SandboxModuleDef = {
24
+ id: "${cfg.id}",
25
+ name: "${cfg.name}",
26
+ version: "0.1.0",
27
+ description: "${cfg.description}",
28
+ author: { name: "${cfg.authorName}", github: "${cfg.authorGithub}" },
29
+ tags: [],
30
+ permissions: [${permsLiteral(cfg.permissions)}],
31
+ commands: [
32
+ {
33
+ id: "${cfg.id}.hello",
34
+ label: "${cfg.name}: count items here",
35
+ contextMenu: true,
36
+ contextMenuCategory: "View",
37
+ when: { selection: "any" },
38
+ },
39
+ ],
40
+ setup(host) {
41
+ host.onCommand("${cfg.id}.hello", async (snap) => {
42
+ // host is fully typed — readDir resolves to FileItem[], no cast needed.
43
+ const items = await host.fs.readDir(snap.currentDirectory);
44
+ const dirs = items.filter((i) => i.isDir).length;
45
+ host.log(
46
+ \`\${snap.currentDirectory} → \${items.length} items (\${dirs} folders, \${items.length - dirs} files)\`,
47
+ );
48
+ });
49
+ },
50
+ };
51
+
52
+ export default mod;
53
+ `;
54
+ }
55
+
56
+ export function packageJson(cfg) {
57
+ return JSON.stringify(
58
+ {
59
+ name: cfg.pkgName,
60
+ version: "0.1.0",
61
+ description: cfg.description,
62
+ type: "module",
63
+ private: true,
64
+ // Read by scripts/dev-install.mjs to place the built file under ~/.mutka.
65
+ mutka: { id: cfg.id },
66
+ scripts: {
67
+ build: "tsup src/index.ts --format esm",
68
+ dev: "tsup src/index.ts --format esm --watch",
69
+ "install:local": "npm run build && node scripts/dev-install.mjs",
70
+ },
71
+ devDependencies: {
72
+ "@mutka-explorer/module": cfg.typesVersion,
73
+ tsup: "^8.0.0",
74
+ typescript: "^5.5.0",
75
+ },
76
+ },
77
+ null,
78
+ 2,
79
+ ) + "\n";
80
+ }
81
+
82
+ export function tsconfigJson() {
83
+ return JSON.stringify(
84
+ {
85
+ compilerOptions: {
86
+ target: "ES2020",
87
+ module: "ESNext",
88
+ moduleResolution: "bundler",
89
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
90
+ strict: true,
91
+ skipLibCheck: true,
92
+ noEmit: true,
93
+ types: [],
94
+ },
95
+ include: ["src"],
96
+ },
97
+ null,
98
+ 2,
99
+ ) + "\n";
100
+ }
101
+
102
+ // GitHub discovery installs a repo named `mutka-module-*` by reading either a
103
+ // bare index.js at the root OR this file listing the built entry path(s).
104
+ export function mutkaConfigJson() {
105
+ return JSON.stringify({ projects: ["dist/index.js"] }, null, 2) + "\n";
106
+ }
107
+
108
+ export function gitignore() {
109
+ return ["node_modules/", "*.log", ".DS_Store", ""].join("\n");
110
+ }
111
+
112
+ // Builds, then copies the single bundled file into ~/.mutka/modules/<id>/ so the
113
+ // running app picks it up on reload — the fast local dev loop.
114
+ export function devInstallMjs() {
115
+ return `import { readFileSync, mkdirSync, copyFileSync, existsSync } from "node:fs";
116
+ import { homedir } from "node:os";
117
+ import { join } from "node:path";
118
+
119
+ const pkg = JSON.parse(readFileSync("package.json", "utf8"));
120
+ const id = pkg.mutka?.id;
121
+ if (!id) throw new Error("package.json is missing \\"mutka.id\\"");
122
+ if (!existsSync("dist/index.js")) throw new Error("dist/index.js not found — run \\"npm run build\\" first");
123
+
124
+ const dest = join(homedir(), ".mutka", "modules", id);
125
+ mkdirSync(dest, { recursive: true });
126
+ copyFileSync("dist/index.js", join(dest, "index.js"));
127
+ console.log(\`installed → \${join(dest, "index.js")}\\nReload Mutka (or toggle the module) to load it.\`);
128
+ `;
129
+ }
130
+
131
+ export function readme(cfg) {
132
+ return `# ${cfg.name}
133
+
134
+ A [Mutka](https://github.com/ilianAZZ/mutka) module (\`${cfg.id}\`), written in
135
+ TypeScript against [\`@mutka-explorer/module\`](https://www.npmjs.com/package/@mutka-explorer/module).
136
+
137
+ ## Develop
138
+
139
+ \`\`\`bash
140
+ npm install
141
+ npm run dev # rebuild dist/index.js on change
142
+ npm run install:local # build + copy into ~/.mutka/modules/${cfg.id}/ then reload Mutka
143
+ \`\`\`
144
+
145
+ Your module logic lives in \`src/index.ts\`. It runs ISOLATED in a Web Worker:
146
+ **no DOM, no native network** (\`fetch\`/\`XMLHttpRequest\`/\`WebSocket\` are blocked —
147
+ use \`host.net\`). Pure-logic npm libraries bundle fine; DOM/network ones do not.
148
+
149
+ ## Build & publish
150
+
151
+ \`\`\`bash
152
+ npm run build # bundles src/index.ts → dist/index.js (one self-contained file)
153
+ \`\`\`
154
+
155
+ To distribute via Mutka's GitHub discovery, push this project to a repo named
156
+ \`mutka-module-*\` with \`dist/index.js\` committed; \`mutka.config.json\` points the
157
+ catalog at it. Users then find and install it from the Modules overlay.
158
+ `;
159
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@mutka-explorer/create",
3
+ "version": "1.0.0-rc.1",
4
+ "description": "Scaffold a new Mutka module — a typed TypeScript project wired to @mutka-explorer/module that builds to a single ESM file.",
5
+ "license": "MIT",
6
+ "author": "Mutka contributors",
7
+ "homepage": "https://github.com/ilianAZZ/mutka",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ilianAZZ/mutka.git",
11
+ "directory": "packages/create-module"
12
+ },
13
+ "keywords": [
14
+ "mutka",
15
+ "module",
16
+ "create",
17
+ "scaffold",
18
+ "file-explorer"
19
+ ],
20
+ "type": "module",
21
+ "bin": {
22
+ "create-mutka-module": "bin/cli.mjs"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "lib",
27
+ "README.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }