@twardoch/namzy 0.1.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,72 @@
1
+ # namzy
2
+
3
+ Generates fun, human-friendly project names. Seeded by timestamp so you get
4
+ fresh names every run — but pass `--seed` for reproducibility.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install namzy
10
+ # or run without installing:
11
+ npx namzy
12
+ ```
13
+
14
+ ## CLI
15
+
16
+ ```sh
17
+ # One word (default)
18
+ namzy
19
+
20
+ # PascalCase two-word name
21
+ namzy --shape joined
22
+
23
+ # Space-separated two words
24
+ namzy --shape spaced
25
+
26
+ # Generate 5 names
27
+ namzy --count 5 --shape joined
28
+
29
+ # Fetch words from the web (falls back to offline on failure)
30
+ namzy --online --shape spaced
31
+
32
+ # Help
33
+ namzy --help
34
+ ```
35
+
36
+ ## Library
37
+
38
+ ```ts
39
+ import { generate } from "namzy";
40
+
41
+ // Single word (default)
42
+ const name = await generate();
43
+
44
+ // Joined PascalCase
45
+ const joined = await generate({ shape: "joined" });
46
+
47
+ // Spaced, seeded for reproducibility
48
+ const seeded = await generate({ shape: "spaced", seed: 42 });
49
+
50
+ // Online mode (falls back to offline)
51
+ const online = await generate({ online: true, shape: "joined" });
52
+
53
+ console.log(name, joined, seeded, online);
54
+ ```
55
+
56
+ ## Modes
57
+
58
+ | Mode | Description |
59
+ |------|-------------|
60
+ | `--offline` | Uses bundled wordlist (default, always works) |
61
+ | `--online` | Fetches from `random-word-api.herokuapp.com`, falls back offline |
62
+
63
+ ## How it works
64
+
65
+ Picks words from geographic names (Tokyo, Oslo, Cairo…) and evocative English
66
+ words (ember, flint, willow…), then applies a small phonetic mangling pass
67
+ (e.g. `s→z`, `ph→f`, `oo→u`) for a playful feel. Results are deterministic
68
+ for a given seed.
69
+
70
+ ## License
71
+
72
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // this_file: src/cli.ts
3
+ import { generate } from "./index.js";
4
+ const HELP = `
5
+ namzy — generate fun fused project names
6
+
7
+ Usage:
8
+ namzy [options]
9
+
10
+ Options:
11
+ --online Fetch words from a public API (falls back to offline)
12
+ --count <N> Number of names to generate (default: 1)
13
+ --help Show this help
14
+
15
+ Examples:
16
+ namzy
17
+ namzy --count 5
18
+ namzy --online --count 3
19
+ `.trimStart();
20
+ function parseArgs(argv) {
21
+ const args = argv.slice(2);
22
+ const opts = {};
23
+ let count = 1;
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ switch (arg) {
27
+ case "--help":
28
+ case "-h":
29
+ process.stdout.write(HELP);
30
+ process.exit(0);
31
+ break;
32
+ case "--online":
33
+ opts.online = true;
34
+ break;
35
+ case "--count": {
36
+ const n = parseInt(args[++i], 10);
37
+ if (isNaN(n) || n < 1) {
38
+ process.stderr.write(`--count must be a positive integer\n`);
39
+ process.exit(1);
40
+ }
41
+ count = n;
42
+ break;
43
+ }
44
+ default:
45
+ process.stderr.write(`Unknown flag: ${arg}\nRun namzy --help for usage.\n`);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ return { opts, count };
50
+ }
51
+ async function main() {
52
+ const { opts, count } = parseArgs(process.argv);
53
+ const base = Date.now();
54
+ for (let i = 0; i < count; i++) {
55
+ const name = await generate({ ...opts, seed: base + i * 1337 });
56
+ process.stdout.write(name + "\n");
57
+ }
58
+ }
59
+ main().catch((err) => {
60
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,12 @@
1
+ import { mangle, mulberry32, joinClean } from "./mangle.js";
2
+ export interface NamzyOptions {
3
+ online?: boolean;
4
+ seed?: number;
5
+ }
6
+ /**
7
+ * Generate a single fused, mangled namzy name.
8
+ * Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
9
+ */
10
+ export declare function generate(opts?: NamzyOptions): Promise<string>;
11
+ export { mangle, joinClean, mulberry32 };
12
+ export { GEO, COMMON } from "./wordlist.js";
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ // this_file: src/index.ts
2
+ import { GEO, COMMON } from "./wordlist.js";
3
+ import { mangle, mulberry32, joinClean } from "./mangle.js";
4
+ function capitalize(s) {
5
+ return s.charAt(0).toUpperCase() + s.slice(1);
6
+ }
7
+ function pick(arr, rng) {
8
+ return arr[Math.floor(rng() * arr.length)];
9
+ }
10
+ async function fetchOnlineWords() {
11
+ const url = `https://random-word-api.herokuapp.com/word?number=2&length=6`;
12
+ const resp = await fetch(url);
13
+ if (!resp.ok)
14
+ throw new Error(`HTTP ${resp.status}`);
15
+ const data = (await resp.json());
16
+ if (!Array.isArray(data) || data.length < 2)
17
+ throw new Error("Bad response");
18
+ return data.slice(0, 2).map((w) => w.toLowerCase().replace(/[^a-z]/g, ""));
19
+ }
20
+ /**
21
+ * Generate a single fused, mangled namzy name.
22
+ * Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
23
+ */
24
+ export async function generate(opts) {
25
+ const online = opts?.online ?? false;
26
+ const seed = opts?.seed ?? Date.now();
27
+ const rng = mulberry32(seed);
28
+ let w1;
29
+ let w2;
30
+ if (online) {
31
+ try {
32
+ const words = await fetchOnlineWords();
33
+ [w1, w2] = words;
34
+ }
35
+ catch {
36
+ w1 = pick(GEO, rng).toLowerCase();
37
+ w2 = pick(COMMON, rng).toLowerCase();
38
+ }
39
+ }
40
+ else {
41
+ w1 = pick(GEO, rng).toLowerCase();
42
+ w2 = pick(COMMON, rng).toLowerCase();
43
+ }
44
+ const fused = joinClean(w1, w2);
45
+ return capitalize(mangle(fused));
46
+ }
47
+ export { mangle, joinClean, mulberry32 };
48
+ export { GEO, COMMON } from "./wordlist.js";
@@ -0,0 +1,10 @@
1
+ /** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
2
+ export declare function mulberry32(seed: number): () => number;
3
+ /** Consonant rotation: c→q, f→v, k→c, q→k, s→z, z→s, v→f, w→u. Case-preserving. */
4
+ export declare function mangle(s: string): string;
5
+ /**
6
+ * Fuse two lowercase words at a clean junction.
7
+ * Drops the first char of `b` while it duplicates the tail of `a`,
8
+ * or while the seam is vowel-vowel. Max two trims.
9
+ */
10
+ export declare function joinClean(a: string, b: string): string;
package/dist/mangle.js ADDED
@@ -0,0 +1,54 @@
1
+ // this_file: src/mangle.ts
2
+ /** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
3
+ export function mulberry32(seed) {
4
+ let s = seed >>> 0;
5
+ return () => {
6
+ s += 0x6d2b79f5;
7
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
8
+ t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
9
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
10
+ };
11
+ }
12
+ const ROTATION = {
13
+ c: "q", f: "v", k: "c", q: "k",
14
+ s: "z", z: "s", v: "f", w: "u",
15
+ };
16
+ /** Consonant rotation: c→q, f→v, k→c, q→k, s→z, z→s, v→f, w→u. Case-preserving. */
17
+ export function mangle(s) {
18
+ let out = "";
19
+ for (const ch of s) {
20
+ const lower = ch.toLowerCase();
21
+ const repl = ROTATION[lower];
22
+ if (repl === undefined) {
23
+ out += ch;
24
+ }
25
+ else {
26
+ out += ch === lower ? repl : repl.toUpperCase();
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+ const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
32
+ /**
33
+ * Fuse two lowercase words at a clean junction.
34
+ * Drops the first char of `b` while it duplicates the tail of `a`,
35
+ * or while the seam is vowel-vowel. Max two trims.
36
+ */
37
+ export function joinClean(a, b) {
38
+ let head = a;
39
+ let tail = b;
40
+ for (let i = 0; i < 2 && head.length > 0 && tail.length > 0; i++) {
41
+ const last = head[head.length - 1].toLowerCase();
42
+ const first = tail[0].toLowerCase();
43
+ if (last === first) {
44
+ tail = tail.slice(1);
45
+ }
46
+ else if (VOWELS.has(last) && VOWELS.has(first)) {
47
+ tail = tail.slice(1);
48
+ }
49
+ else {
50
+ break;
51
+ }
52
+ }
53
+ return head + tail;
54
+ }
package/dist/web.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { generate, mangle, joinClean, mulberry32, GEO, COMMON } from "./index.js";
2
+ export type { NamzyOptions } from "./index.js";
package/dist/web.js ADDED
@@ -0,0 +1,3 @@
1
+ // this_file: src/web.ts
2
+ // Browser entry point — exposes namzy.generate() on window.namzy.
3
+ export { generate, mangle, joinClean, mulberry32, GEO, COMMON } from "./index.js";
@@ -0,0 +1,4 @@
1
+ /** Geographic names (A-Za-z only, no diacritics) */
2
+ export declare const GEO: string[];
3
+ /** Common evocative English words */
4
+ export declare const COMMON: string[];
@@ -0,0 +1,17 @@
1
+ // this_file: src/wordlist.ts
2
+ /** Geographic names (A-Za-z only, no diacritics) */
3
+ export const GEO = [
4
+ "tokyo", "paris", "oslo", "berlin", "lagos", "lima", "boston", "vienna",
5
+ "cairo", "kyoto", "dubai", "seoul", "milan", "tunis", "perth", "genoa",
6
+ "porto", "bruges", "ghent", "brest", "minsk", "sofia", "riga", "lyon",
7
+ "basel", "natal", "recife", "darwin", "hobart", "odessa", "varna", "Split",
8
+ "kotor", "tiran", "skopje", "bitola", "plovdiv", "gabrovo", "varna", "ruse",
9
+ ];
10
+ /** Common evocative English words */
11
+ export const COMMON = [
12
+ "river", "stone", "ember", "frost", "harbor", "willow", "copper", "marble",
13
+ "anchor", "lantern", "cedar", "falcon", "coral", "dagger", "flint", "grove",
14
+ "haven", "iron", "jasper", "kelp", "larch", "mast", "nettle", "oak",
15
+ "pine", "quill", "reed", "sage", "thorn", "umber", "vale", "wren",
16
+ "yarrow", "zenith", "amber", "birch", "cliff", "drift", "forge", "glade",
17
+ ];
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@twardoch/namzy",
3
+ "version": "0.1.0",
4
+ "description": "Generates fun fused human-friendly project names",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "namzy": "./dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "build:web": "esbuild src/web.ts --bundle --format=iife --global-name=namzy --outfile=../docs/namzy.js --target=es2020",
21
+ "start": "node dist/cli.js",
22
+ "dev": "tsx src/cli.ts"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "esbuild": "^0.24.0",
30
+ "tsx": "^4.0.0",
31
+ "typescript": "^5.0.0"
32
+ }
33
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // this_file: src/cli.ts
3
+
4
+ import { generate } from "./index.js";
5
+ import type { NamzyOptions } from "./index.js";
6
+
7
+ const HELP = `
8
+ namzy — generate fun fused project names
9
+
10
+ Usage:
11
+ namzy [options]
12
+
13
+ Options:
14
+ --online Fetch words from a public API (falls back to offline)
15
+ --count <N> Number of names to generate (default: 1)
16
+ --help Show this help
17
+
18
+ Examples:
19
+ namzy
20
+ namzy --count 5
21
+ namzy --online --count 3
22
+ `.trimStart();
23
+
24
+ function parseArgs(argv: string[]): { opts: NamzyOptions; count: number } {
25
+ const args = argv.slice(2);
26
+ const opts: NamzyOptions = {};
27
+ let count = 1;
28
+
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ switch (arg) {
32
+ case "--help":
33
+ case "-h":
34
+ process.stdout.write(HELP);
35
+ process.exit(0);
36
+ break;
37
+ case "--online":
38
+ opts.online = true;
39
+ break;
40
+ case "--count": {
41
+ const n = parseInt(args[++i], 10);
42
+ if (isNaN(n) || n < 1) {
43
+ process.stderr.write(`--count must be a positive integer\n`);
44
+ process.exit(1);
45
+ }
46
+ count = n;
47
+ break;
48
+ }
49
+ default:
50
+ process.stderr.write(`Unknown flag: ${arg}\nRun namzy --help for usage.\n`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+ return { opts, count };
55
+ }
56
+
57
+ async function main(): Promise<void> {
58
+ const { opts, count } = parseArgs(process.argv);
59
+ const base = Date.now();
60
+ for (let i = 0; i < count; i++) {
61
+ const name = await generate({ ...opts, seed: base + i * 1337 });
62
+ process.stdout.write(name + "\n");
63
+ }
64
+ }
65
+
66
+ main().catch((err) => {
67
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
68
+ process.exit(1);
69
+ });
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ // this_file: src/index.ts
2
+
3
+ import { GEO, COMMON } from "./wordlist.js";
4
+ import { mangle, mulberry32, joinClean } from "./mangle.js";
5
+
6
+ export interface NamzyOptions {
7
+ online?: boolean;
8
+ seed?: number;
9
+ }
10
+
11
+ function capitalize(s: string): string {
12
+ return s.charAt(0).toUpperCase() + s.slice(1);
13
+ }
14
+
15
+ function pick<T>(arr: T[], rng: () => number): T {
16
+ return arr[Math.floor(rng() * arr.length)];
17
+ }
18
+
19
+ async function fetchOnlineWords(): Promise<string[]> {
20
+ const url = `https://random-word-api.herokuapp.com/word?number=2&length=6`;
21
+ const resp = await fetch(url);
22
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
23
+ const data = (await resp.json()) as string[];
24
+ if (!Array.isArray(data) || data.length < 2) throw new Error("Bad response");
25
+ return data.slice(0, 2).map((w) => w.toLowerCase().replace(/[^a-z]/g, ""));
26
+ }
27
+
28
+ /**
29
+ * Generate a single fused, mangled namzy name.
30
+ * Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
31
+ */
32
+ export async function generate(opts?: NamzyOptions): Promise<string> {
33
+ const online = opts?.online ?? false;
34
+ const seed = opts?.seed ?? Date.now();
35
+ const rng = mulberry32(seed);
36
+
37
+ let w1: string;
38
+ let w2: string;
39
+
40
+ if (online) {
41
+ try {
42
+ const words = await fetchOnlineWords();
43
+ [w1, w2] = words;
44
+ } catch {
45
+ w1 = pick(GEO, rng).toLowerCase();
46
+ w2 = pick(COMMON, rng).toLowerCase();
47
+ }
48
+ } else {
49
+ w1 = pick(GEO, rng).toLowerCase();
50
+ w2 = pick(COMMON, rng).toLowerCase();
51
+ }
52
+
53
+ const fused = joinClean(w1, w2);
54
+ return capitalize(mangle(fused));
55
+ }
56
+
57
+ export { mangle, joinClean, mulberry32 };
58
+ export { GEO, COMMON } from "./wordlist.js";
package/src/mangle.ts ADDED
@@ -0,0 +1,56 @@
1
+ // this_file: src/mangle.ts
2
+
3
+ /** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
4
+ export function mulberry32(seed: number): () => number {
5
+ let s = seed >>> 0;
6
+ return () => {
7
+ s += 0x6d2b79f5;
8
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
9
+ t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
10
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
11
+ };
12
+ }
13
+
14
+ const ROTATION: Record<string, string> = {
15
+ c: "q", f: "v", k: "c", q: "k",
16
+ s: "z", z: "s", v: "f", w: "u",
17
+ };
18
+
19
+ /** Consonant rotation: c→q, f→v, k→c, q→k, s→z, z→s, v→f, w→u. Case-preserving. */
20
+ export function mangle(s: string): string {
21
+ let out = "";
22
+ for (const ch of s) {
23
+ const lower = ch.toLowerCase();
24
+ const repl = ROTATION[lower];
25
+ if (repl === undefined) {
26
+ out += ch;
27
+ } else {
28
+ out += ch === lower ? repl : repl.toUpperCase();
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+
34
+ const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
35
+
36
+ /**
37
+ * Fuse two lowercase words at a clean junction.
38
+ * Drops the first char of `b` while it duplicates the tail of `a`,
39
+ * or while the seam is vowel-vowel. Max two trims.
40
+ */
41
+ export function joinClean(a: string, b: string): string {
42
+ let head = a;
43
+ let tail = b;
44
+ for (let i = 0; i < 2 && head.length > 0 && tail.length > 0; i++) {
45
+ const last = head[head.length - 1].toLowerCase();
46
+ const first = tail[0].toLowerCase();
47
+ if (last === first) {
48
+ tail = tail.slice(1);
49
+ } else if (VOWELS.has(last) && VOWELS.has(first)) {
50
+ tail = tail.slice(1);
51
+ } else {
52
+ break;
53
+ }
54
+ }
55
+ return head + tail;
56
+ }
package/src/web.ts ADDED
@@ -0,0 +1,5 @@
1
+ // this_file: src/web.ts
2
+ // Browser entry point — exposes namzy.generate() on window.namzy.
3
+
4
+ export { generate, mangle, joinClean, mulberry32, GEO, COMMON } from "./index.js";
5
+ export type { NamzyOptions } from "./index.js";
@@ -0,0 +1,19 @@
1
+ // this_file: src/wordlist.ts
2
+
3
+ /** Geographic names (A-Za-z only, no diacritics) */
4
+ export const GEO: string[] = [
5
+ "tokyo", "paris", "oslo", "berlin", "lagos", "lima", "boston", "vienna",
6
+ "cairo", "kyoto", "dubai", "seoul", "milan", "tunis", "perth", "genoa",
7
+ "porto", "bruges", "ghent", "brest", "minsk", "sofia", "riga", "lyon",
8
+ "basel", "natal", "recife", "darwin", "hobart", "odessa", "varna", "Split",
9
+ "kotor", "tiran", "skopje", "bitola", "plovdiv", "gabrovo", "varna", "ruse",
10
+ ];
11
+
12
+ /** Common evocative English words */
13
+ export const COMMON: string[] = [
14
+ "river", "stone", "ember", "frost", "harbor", "willow", "copper", "marble",
15
+ "anchor", "lantern", "cedar", "falcon", "coral", "dagger", "flint", "grove",
16
+ "haven", "iron", "jasper", "kelp", "larch", "mast", "nettle", "oak",
17
+ "pine", "quill", "reed", "sage", "thorn", "umber", "vale", "wren",
18
+ "yarrow", "zenith", "amber", "birch", "cliff", "drift", "forge", "glade",
19
+ ];