@aathan807/rw-design 1.0.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/index.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ const { CliError } = require("./src/utils/errors");
3
+
4
+ const commands = {
5
+ init: () => require("./src/commands/init")(process.argv.slice(3)),
6
+ check: () => require("./src/commands/check")(),
7
+ update: () => require("./src/commands/update")(),
8
+ list: () => require("./src/commands/list")(),
9
+ pull: () => require("./src/commands/pull")(process.argv[3]),
10
+ };
11
+
12
+ function usage() {
13
+ console.log(
14
+ "rw-design — RandomWalk Design Intelligence\n\n" +
15
+ "Usage:\n" +
16
+ " rw-design init Install design intelligence into this project (interactive)\n" +
17
+ " rw-design check Check whether an update is available\n" +
18
+ " rw-design update Update installed design intelligence\n" +
19
+ " rw-design list Browse everything available in the KB\n" +
20
+ " rw-design pull <id> Install one knowledge file (+ dependencies)\n"
21
+ );
22
+ }
23
+
24
+ async function main() {
25
+ const command = process.argv[2];
26
+ const handler = commands[command];
27
+
28
+ if (!handler) {
29
+ usage();
30
+ process.exitCode = command ? 1 : 0;
31
+ return;
32
+ }
33
+
34
+ await handler();
35
+ }
36
+
37
+ main().catch((err) => {
38
+ if (err && err.isCliError) {
39
+ console.error(`\nError: ${err.message}`);
40
+ } else {
41
+ console.error(`\nUnexpected error: ${err && err.message ? err.message : err}`);
42
+ }
43
+ process.exitCode = 1;
44
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@aathan807/rw-design",
3
+ "version": "1.0.0",
4
+ "description": "RandomWalk Design Intelligence CLI — installs versioned design knowledge into any project.",
5
+ "bin": {
6
+ "rw-design": "./index.js"
7
+ },
8
+ "main": "index.js",
9
+ "files": [
10
+ "index.js",
11
+ "src"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "test": "node --check index.js"
21
+ },
22
+ "keywords": ["design", "cli", "randomwalk", "design-intelligence"],
23
+ "author": "aathan807",
24
+ "license": "ISC",
25
+ "dependencies": {
26
+ "inquirer": "^14.0.2"
27
+ }
28
+ }
@@ -0,0 +1,34 @@
1
+ const path = require("path");
2
+ const github = require("../github/client");
3
+ const fs = require("../utils/filesystem");
4
+ const source = require("../config/source");
5
+ const { compareVersions } = require("../utils/version");
6
+
7
+ /**
8
+ * `rw-design check` — compare the installed KB version against the remote.
9
+ */
10
+ module.exports = async function check() {
11
+ const installRoot = path.join(process.cwd(), source.installDir);
12
+ const localVersion = fs.readJSON(path.join(installRoot, "version.json"));
13
+
14
+ if (!localVersion) {
15
+ console.log(
16
+ "Design Intelligence is not installed in this project.\n\nRun:\n rw-design init"
17
+ );
18
+ return;
19
+ }
20
+
21
+ const remoteVersion = await github.fetchJSON("version.json");
22
+ const cmp = compareVersions(localVersion.version, remoteVersion.version);
23
+
24
+ if (cmp >= 0) {
25
+ console.log(`✓ Design Intelligence is up to date.\n\nCurrent version: ${localVersion.version}`);
26
+ } else {
27
+ console.log(
28
+ "⚡ Design Intelligence update available\n\n" +
29
+ `Installed: ${localVersion.version}\n` +
30
+ `Latest: ${remoteVersion.version}\n\n` +
31
+ "Run:\n rw-design update"
32
+ );
33
+ }
34
+ };
@@ -0,0 +1,68 @@
1
+ const path = require("path");
2
+ const inquirer = require("inquirer").default;
3
+ const github = require("../github/client");
4
+ const fs = require("../utils/filesystem");
5
+ const source = require("../config/source");
6
+ const { buildQuestions } = require("../profile/questions");
7
+ const { CliError } = require("../utils/errors");
8
+ const { installFromProfile } = require("./install-core");
9
+
10
+ /**
11
+ * `rw-design init` — first-time install.
12
+ * Interactive by default; non-interactive with:
13
+ * --defaults accept the default/first choice for every question
14
+ * --profile <file.json> read the full profile from a JSON file
15
+ * Validates env, resolves the KB, downloads files, and writes the installed
16
+ * layout + agent instructions. Safe to re-run.
17
+ */
18
+ module.exports = async function init(args = []) {
19
+ const cwd = process.cwd();
20
+ github.requireToken(); // throws friendly error if missing
21
+ if (!fs.isWritable(cwd)) {
22
+ throw new CliError(`Current directory is not writable:\n${cwd}`);
23
+ }
24
+
25
+ const installRoot = path.join(cwd, source.installDir);
26
+
27
+ console.log("Fetching Design Intelligence catalog...\n");
28
+ const registry = await github.fetchJSON("registry.json");
29
+ const remoteVersion = await github.fetchJSON("version.json");
30
+
31
+ const questions = buildQuestions(registry);
32
+ let profile;
33
+ const profileFlag = args.indexOf("--profile");
34
+ if (profileFlag !== -1) {
35
+ const file = args[profileFlag + 1];
36
+ if (!file) throw new CliError("Usage: rw-design init --profile <file.json>");
37
+ profile = fs.readJSON(path.resolve(cwd, file));
38
+ if (!profile) throw new CliError(`Profile file not found: ${file}`);
39
+ // Fill anything the file omits with question defaults.
40
+ for (const q of questions) {
41
+ if (profile[q.name] === undefined) profile[q.name] = q.default ?? q.choices[0].value;
42
+ }
43
+ } else if (args.includes("--defaults")) {
44
+ profile = Object.fromEntries(questions.map((q) => [q.name, q.default ?? q.choices[0].value]));
45
+ console.log("Using defaults:", JSON.stringify(profile));
46
+ } else {
47
+ profile = { ...(await inquirer.prompt(questions)) };
48
+ }
49
+
50
+ console.log("\nResolving design knowledge for your project...\n");
51
+ const { resolved } = await installFromProfile({
52
+ installRoot,
53
+ profile,
54
+ registry,
55
+ remoteVersion,
56
+ source,
57
+ });
58
+
59
+ for (const r of resolved) {
60
+ console.log(` ✓ ${r.id}`);
61
+ }
62
+
63
+ console.log(
64
+ `\n🎉 Design Intelligence installed into ${source.installDir}/` +
65
+ `\n ${resolved.length} files · KB version ${remoteVersion.version}` +
66
+ `\n\nRead ${source.installDir}/CLAUDE.md and start building.`
67
+ );
68
+ };
@@ -0,0 +1,168 @@
1
+ const path = require("path");
2
+ const github = require("../github/client");
3
+ const fs = require("../utils/filesystem");
4
+ const { resolve } = require("../resolver/resolve");
5
+ const { CliError } = require("../utils/errors");
6
+
7
+ /**
8
+ * Shared installation logic used by `init` and `update`.
9
+ * Given a profile, resolves the file set from the remote registry, downloads
10
+ * each file into <installRoot>/knowledge/<id>.md, and (re)generates:
11
+ * - DESIGN.md — single consolidated entry file (tokens + rules + index)
12
+ * - CLAUDE.md / AGENTS.md — agent instructions pointing at DESIGN.md
13
+ * - manifest.json / profile.json / version.json
14
+ */
15
+
16
+ const KNOWLEDGE_SUBDIR = "knowledge";
17
+
18
+ function localRelPath(entry) {
19
+ return `${KNOWLEDGE_SUBDIR}/${entry.id}.md`;
20
+ }
21
+
22
+ /** Strip YAML frontmatter from a knowledge file body. */
23
+ function stripFrontmatter(text) {
24
+ return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
25
+ }
26
+
27
+ async function installFromProfile({ installRoot, profile, registry, remoteVersion, source }) {
28
+ const resolved = resolve(profile, registry);
29
+ if (resolved.length === 0) {
30
+ // Should not happen (brand/foundations are always-on), but guard anyway.
31
+ throw new CliError("No design knowledge matched this project profile.");
32
+ }
33
+
34
+ const results = [];
35
+ const contentsById = new Map();
36
+ for (const entry of resolved) {
37
+ const content = await github.fetchFile(entry.path);
38
+ fs.writeText(path.join(installRoot, localRelPath(entry)), content);
39
+ contentsById.set(entry.id, content);
40
+ results.push({
41
+ id: entry.id,
42
+ title: entry.title,
43
+ category: entry.category,
44
+ source: entry.path,
45
+ local: localRelPath(entry),
46
+ });
47
+ }
48
+
49
+ // manifest.json — what is installed + where it came from.
50
+ const manifest = {
51
+ source: { owner: source.owner, repository: source.repo, branch: source.branch },
52
+ profile,
53
+ installedFiles: results,
54
+ };
55
+ fs.writeJSON(path.join(installRoot, "manifest.json"), manifest);
56
+
57
+ // profile.json — kept separately so `update` can re-resolve.
58
+ fs.writeJSON(path.join(installRoot, "profile.json"), profile);
59
+
60
+ // version.json — installed KB version.
61
+ fs.writeJSON(path.join(installRoot, "version.json"), { version: remoteVersion.version });
62
+
63
+ // DESIGN.md — the single consolidated entry file agents read first.
64
+ fs.writeText(
65
+ path.join(installRoot, "DESIGN.md"),
66
+ renderDesignMd({ profile, results, contentsById, version: remoteVersion.version })
67
+ );
68
+
69
+ // Agent instruction files (Claude Code / Codex) — point at DESIGN.md.
70
+ const instructions = renderAgentInstructions({ profile, results, version: remoteVersion.version });
71
+ fs.writeText(path.join(installRoot, "CLAUDE.md"), instructions);
72
+ fs.writeText(path.join(installRoot, "AGENTS.md"), instructions);
73
+
74
+ return { resolved: results };
75
+ }
76
+
77
+ /**
78
+ * Render the consolidated DESIGN.md: profile front matter, the full theme
79
+ * token sheets inlined (brand + themes are the highest-priority layers), and a
80
+ * priority-ordered index into the per-topic knowledge files.
81
+ */
82
+ function renderDesignMd({ profile, results, contentsById, version }) {
83
+ const fm = [
84
+ "---",
85
+ `generated_by: rw-design`,
86
+ `kb_version: "${version}"`,
87
+ `company: ${profile.company}`,
88
+ `product: ${profile.product}`,
89
+ `project_type: ${profile.projectType}`,
90
+ `platform: ${profile.platform}`,
91
+ `framework: ${profile.framework}`,
92
+ ...(profile.density ? [`density: ${profile.density}`] : []),
93
+ ...(profile.visualTone ? [`visual_tone: ${profile.visualTone}`] : []),
94
+ ...(profile.colorMode ? [`color_mode: ${profile.colorMode}`] : []),
95
+ "---",
96
+ ].join("\n");
97
+
98
+ const inlineCategories = ["brand", "theme"];
99
+ const inlined = results
100
+ .filter((r) => inlineCategories.includes(r.category))
101
+ .map((r) => stripFrontmatter(contentsById.get(r.id) || ""))
102
+ .join("\n\n---\n\n");
103
+
104
+ const groups = new Map();
105
+ for (const r of results) {
106
+ if (inlineCategories.includes(r.category)) continue;
107
+ if (!groups.has(r.category)) groups.set(r.category, []);
108
+ groups.get(r.category).push(r);
109
+ }
110
+ const index = [...groups.entries()]
111
+ .map(([category, entries]) => {
112
+ const rows = entries.map((r) => `- [\`${r.id}\`](${r.local}) — ${r.title}`).join("\n");
113
+ return `### ${category[0].toUpperCase()}${category.slice(1)}\n${rows}`;
114
+ })
115
+ .join("\n\n");
116
+
117
+ return `${fm}
118
+
119
+ # Design System — ${profile.product === "none" ? "RandomWalk" : profile.product} / ${profile.projectType}
120
+
121
+ > **Read this file before generating any UI.** It is the design contract for this
122
+ > project. Tokens and brand rules are inlined below; topic-specific rules are in the
123
+ > linked knowledge files. Priority: brand → theme → product → pattern → topic files →
124
+ > skill. Accessibility is a hard floor. Checklists are acceptance criteria.
125
+
126
+ ${inlined}
127
+
128
+ ---
129
+
130
+ ## Knowledge index (read the relevant file before building that surface)
131
+
132
+ ${index}
133
+
134
+ _Generated by rw-design from KB version ${version}. Do not edit by hand._
135
+ `;
136
+ }
137
+
138
+ function renderAgentInstructions({ profile, results, version }) {
139
+ const list = results.map((r) => `- \`${r.local}\` — ${r.title} (\`${r.id}\`)`).join("\n");
140
+ return `# Design Intelligence — Agent Instructions
141
+
142
+ This project has RandomWalk design intelligence installed (KB version ${version}).
143
+ **Start with \`DESIGN.md\` in this directory** — it is the consolidated design contract
144
+ (tokens, brand rules, and an index into everything below). These files are authoritative
145
+ for design, UI, and UX decisions; follow them over generic defaults.
146
+
147
+ ## Project profile
148
+ - Company: ${profile.company}
149
+ - Product: ${profile.product}
150
+ - Project type: ${profile.projectType}
151
+ - Platform: ${profile.platform}
152
+ - Framework: ${profile.framework}
153
+
154
+ ## Installed knowledge (priority order)
155
+ ${list}
156
+
157
+ ## How to apply
158
+ 1. Read \`DESIGN.md\` first; use \`manifest.json\` to see what is installed.
159
+ 2. When building a specific surface, consult the matching knowledge file(s) above.
160
+ 3. Resolve conflicts by this priority: **brand → theme → product → pattern → navigation / interaction / content / presentation → skill**. Higher wins — except accessibility, which is a hard floor nothing may violate.
161
+ 4. Files with ids starting \`checklist.\` are acceptance criteria: verify built UI against the relevant checklist sections before considering it done.
162
+ 5. Do not edit these files by hand — they are managed by \`rw-design update\`.
163
+
164
+ _Generated by rw-design. Run \`rw-design check\` for updates._
165
+ `;
166
+ }
167
+
168
+ module.exports = { installFromProfile };
@@ -0,0 +1,33 @@
1
+ const github = require("../github/client");
2
+
3
+ /**
4
+ * `rw-design list` — browse the KB registry: every installable knowledge file,
5
+ * grouped by category, with targeting metadata.
6
+ */
7
+ module.exports = async function list() {
8
+ const registry = await github.fetchJSON("registry.json");
9
+ const version = await github.fetchJSON("version.json");
10
+
11
+ const groups = new Map();
12
+ for (const entry of registry.files) {
13
+ if (!groups.has(entry.category)) groups.set(entry.category, []);
14
+ groups.get(entry.category).push(entry);
15
+ }
16
+
17
+ console.log(`Design Intelligence KB — version ${version.version}\n`);
18
+ for (const [category, entries] of groups) {
19
+ console.log(`${category.toUpperCase()}`);
20
+ for (const e of entries.sort((a, b) => a.id.localeCompare(b.id))) {
21
+ const scope = [
22
+ e.applies_to?.length ? `applies: ${e.applies_to.join(", ")}` : "",
23
+ e.platforms?.length ? `platforms: ${e.platforms.join(", ")}` : "",
24
+ e.variant_of ? `variant of ${e.variant_of}` : "",
25
+ ]
26
+ .filter(Boolean)
27
+ .join(" · ");
28
+ console.log(` ${e.id.padEnd(32)} ${e.title}${scope ? ` (${scope})` : ""}`);
29
+ }
30
+ console.log("");
31
+ }
32
+ console.log(`${registry.files.length} files. Install one with: rw-design pull <id>`);
33
+ };
@@ -0,0 +1,86 @@
1
+ const path = require("path");
2
+ const github = require("../github/client");
3
+ const fs = require("../utils/filesystem");
4
+ const source = require("../config/source");
5
+ const { CliError } = require("../utils/errors");
6
+
7
+ /**
8
+ * `rw-design pull <id>` — install a single knowledge file (plus its transitive
9
+ * dependencies) on top of whatever is already installed. Useful for grabbing
10
+ * knowledge outside your profile's automatic resolution.
11
+ */
12
+ module.exports = async function pull(id) {
13
+ if (!id) {
14
+ throw new CliError("Usage: rw-design pull <id>\n\nSee available ids with: rw-design list");
15
+ }
16
+
17
+ const installRoot = path.join(process.cwd(), source.installDir);
18
+ const registry = await github.fetchJSON("registry.json");
19
+ const byId = new Map(registry.files.map((f) => [f.id, f]));
20
+
21
+ if (!byId.has(id)) {
22
+ throw new CliError(`Unknown id: ${id}\n\nSee available ids with: rw-design list`);
23
+ }
24
+
25
+ // Close over dependencies.
26
+ const wanted = new Set([id]);
27
+ let changed = true;
28
+ while (changed) {
29
+ changed = false;
30
+ for (const wid of [...wanted]) {
31
+ for (const dep of byId.get(wid).dependencies || []) {
32
+ if (byId.has(dep) && !wanted.has(dep)) {
33
+ wanted.add(dep);
34
+ changed = true;
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ // Skip files already installed (manifest is the record of truth).
41
+ const manifestPath = path.join(installRoot, "manifest.json");
42
+ const manifest =
43
+ fs.readJSON(manifestPath) || {
44
+ source: { owner: source.owner, repository: source.repo, branch: source.branch },
45
+ profile: null,
46
+ installedFiles: [],
47
+ };
48
+ const installedIds = new Set(manifest.installedFiles.map((f) => f.id));
49
+
50
+ const toInstall = [...wanted].map((wid) => byId.get(wid));
51
+ let added = 0;
52
+ for (const entry of toInstall) {
53
+ const local = `knowledge/${entry.id}.md`;
54
+ const content = await github.fetchFile(entry.path);
55
+ fs.writeText(path.join(installRoot, local), content);
56
+ if (installedIds.has(entry.id)) {
57
+ console.log(` ✓ ${entry.id} (refreshed)`);
58
+ } else {
59
+ manifest.installedFiles.push({
60
+ id: entry.id,
61
+ title: entry.title,
62
+ category: entry.category,
63
+ source: entry.path,
64
+ local,
65
+ });
66
+ console.log(` ✓ ${entry.id} (added)`);
67
+ added++;
68
+ }
69
+ }
70
+
71
+ fs.writeJSON(manifestPath, manifest);
72
+
73
+ // Record the KB version if this is a fresh directory (don't clobber an existing one).
74
+ const versionPath = path.join(installRoot, "version.json");
75
+ if (!fs.exists(versionPath)) {
76
+ const remoteVersion = await github.fetchJSON("version.json");
77
+ fs.writeJSON(versionPath, { version: remoteVersion.version });
78
+ }
79
+
80
+ console.log(
81
+ `\n✓ Pulled ${toInstall.length} file(s) (${added} new).` +
82
+ (manifest.profile
83
+ ? ""
84
+ : "\n Note: no project profile found — run `rw-design init` for full setup (DESIGN.md, agent instructions).")
85
+ );
86
+ };
@@ -0,0 +1,52 @@
1
+ const path = require("path");
2
+ const github = require("../github/client");
3
+ const fs = require("../utils/filesystem");
4
+ const source = require("../config/source");
5
+ const { compareVersions } = require("../utils/version");
6
+ const { CliError } = require("../utils/errors");
7
+ const { installFromProfile } = require("./install-core");
8
+
9
+ /**
10
+ * `rw-design update` — re-resolve from the saved profile and refresh files.
11
+ * Re-resolving (rather than just re-downloading the old set) means newly
12
+ * applicable knowledge is picked up as the KB grows.
13
+ */
14
+ module.exports = async function update() {
15
+ const installRoot = path.join(process.cwd(), source.installDir);
16
+ const localVersion = fs.readJSON(path.join(installRoot, "version.json"));
17
+ const profile = fs.readJSON(path.join(installRoot, "profile.json"));
18
+
19
+ if (!localVersion || !profile) {
20
+ throw new CliError(
21
+ "Design Intelligence is not installed in this project.\n\nRun:\n rw-design init"
22
+ );
23
+ }
24
+
25
+ const remoteVersion = await github.fetchJSON("version.json");
26
+ if (compareVersions(localVersion.version, remoteVersion.version) >= 0) {
27
+ console.log(`✓ Already up to date.\n\nCurrent version: ${localVersion.version}`);
28
+ return;
29
+ }
30
+
31
+ console.log(
32
+ "Design Intelligence update available\n\n" +
33
+ `${localVersion.version} → ${remoteVersion.version}\n\nUpdating...\n`
34
+ );
35
+
36
+ const registry = await github.fetchJSON("registry.json");
37
+ const { resolved } = await installFromProfile({
38
+ installRoot,
39
+ profile,
40
+ registry,
41
+ remoteVersion,
42
+ source,
43
+ });
44
+
45
+ for (const r of resolved) {
46
+ console.log(` ✓ ${r.local} updated`);
47
+ }
48
+
49
+ console.log(
50
+ `\nDesign Intelligence updated successfully.\n\n${localVersion.version} → ${remoteVersion.version}`
51
+ );
52
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Where the design knowledge base lives. Single place to configure the source
3
+ * repository (principle: keep the GitHub repo configurable).
4
+ */
5
+ module.exports = {
6
+ owner: "AathanRW",
7
+ repo: "rw-design-kb",
8
+ branch: "main",
9
+
10
+ // Path (inside the developer's project) where design intelligence is installed.
11
+ installDir: ".design-intelligence",
12
+ };
@@ -0,0 +1,78 @@
1
+ const { CliError } = require("../utils/errors");
2
+ const source = require("../config/source");
3
+
4
+ /**
5
+ * GitHub access layer. Responsible for authentication, URL building, fetching
6
+ * files from the private KB, and mapping API/network failures to friendly errors.
7
+ * Never logs or exposes the token or request headers.
8
+ */
9
+
10
+ function requireToken() {
11
+ const token = process.env.GITHUB_TOKEN;
12
+ if (!token || !token.trim()) {
13
+ throw new CliError(
14
+ "GITHUB_TOKEN is not configured.\n\n" +
15
+ 'Set it first, e.g. (PowerShell): $env:GITHUB_TOKEN="<your token>"'
16
+ );
17
+ }
18
+ return token;
19
+ }
20
+
21
+ function contentsUrl(path) {
22
+ const { owner, repo, branch } = source;
23
+ return `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`;
24
+ }
25
+
26
+ async function fetchRaw(path, { asJson = false } = {}) {
27
+ const token = requireToken();
28
+ let response;
29
+ try {
30
+ response = await fetch(contentsUrl(path), {
31
+ headers: {
32
+ Authorization: `Bearer ${token}`,
33
+ Accept: "application/vnd.github.raw+json",
34
+ "User-Agent": "rw-design-cli",
35
+ },
36
+ });
37
+ } catch {
38
+ throw new CliError(
39
+ "Unable to connect to the Design Intelligence repository.\n" +
40
+ "Check your internet connection and try again."
41
+ );
42
+ }
43
+
44
+ if (response.status === 401) {
45
+ throw new CliError("Unable to authenticate with GitHub. Check your GITHUB_TOKEN.");
46
+ }
47
+ if (response.status === 404) {
48
+ throw new CliError(`Knowledge file not found in the repository:\n${path}`);
49
+ }
50
+ if (response.status === 403) {
51
+ throw new CliError(
52
+ "Unable to access the Design Intelligence repository (permission denied or rate limited)."
53
+ );
54
+ }
55
+ if (!response.ok) {
56
+ throw new CliError(`Unable to fetch ${path} (HTTP ${response.status}).`);
57
+ }
58
+
59
+ const text = await response.text();
60
+ if (!asJson) return text;
61
+ try {
62
+ return JSON.parse(text);
63
+ } catch {
64
+ throw new CliError(`Malformed JSON received for ${path}.`);
65
+ }
66
+ }
67
+
68
+ /** Fetch a markdown/text file from the KB. */
69
+ function fetchFile(path) {
70
+ return fetchRaw(path, { asJson: false });
71
+ }
72
+
73
+ /** Fetch and parse a JSON file (registry.json, version.json) from the KB. */
74
+ function fetchJSON(path) {
75
+ return fetchRaw(path, { asJson: true });
76
+ }
77
+
78
+ module.exports = { fetchFile, fetchJSON, requireToken };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Builds the interactive questions for `rw-design init` FROM THE KB REGISTRY,
3
+ * so designers can add products, patterns, and skills to the KB without any
4
+ * CLI change:
5
+ * - products ← entries with category "product" (id: product.<value>)
6
+ * - project types ← entries with category "pattern" (id: pattern.<value>)
7
+ * - frameworks ← entries with category "skill" (framework: <value>)
8
+ *
9
+ * Preference questions (density, tone, color mode) drive VARIANT selection —
10
+ * see templates/variant-template.md in the KB.
11
+ */
12
+
13
+ function titleCase(slug) {
14
+ return slug
15
+ .split("-")
16
+ .map((w) => w[0].toUpperCase() + w.slice(1))
17
+ .join(" ");
18
+ }
19
+
20
+ function buildQuestions(registry) {
21
+ const products = registry.files
22
+ .filter((f) => f.category === "product")
23
+ .map((f) => ({ name: f.title.replace(/ Product$/, ""), value: f.id.replace(/^product\./, "") }));
24
+
25
+ const projectTypes = registry.files
26
+ .filter((f) => f.category === "pattern")
27
+ .map((f) => ({ name: f.title.replace(/ Pattern$/, ""), value: f.id.replace(/^pattern\./, "") }));
28
+
29
+ // Project types can also come from applies_to targeting on knowledge files
30
+ // even when no pattern exists yet.
31
+ const fromAppliesTo = new Set(registry.files.flatMap((f) => f.applies_to || []));
32
+ for (const t of fromAppliesTo) {
33
+ if (!projectTypes.some((p) => p.value === t)) projectTypes.push({ name: titleCase(t), value: t });
34
+ }
35
+
36
+ const frameworks = registry.files
37
+ .filter((f) => f.category === "skill" && f.framework)
38
+ .map((f) => ({ name: f.title.replace(/ Implementation$/, ""), value: f.framework }));
39
+
40
+ const platforms = [...new Set(registry.files.flatMap((f) => f.platforms || []))].map((p) => ({
41
+ name: titleCase(p),
42
+ value: p,
43
+ }));
44
+
45
+ return [
46
+ {
47
+ type: "select",
48
+ name: "company",
49
+ message: "Is this project part of RandomWalk?",
50
+ choices: [{ name: "RandomWalk", value: "randomwalk" }],
51
+ default: "randomwalk",
52
+ },
53
+ {
54
+ type: "select",
55
+ name: "product",
56
+ message: "Which product?",
57
+ choices: [...products, { name: "None / general", value: "none" }],
58
+ },
59
+ {
60
+ type: "select",
61
+ name: "projectType",
62
+ message: "What are you building?",
63
+ choices: projectTypes.sort((a, b) => a.name.localeCompare(b.name)),
64
+ },
65
+ {
66
+ type: "select",
67
+ name: "platform",
68
+ message: "Platform?",
69
+ choices: platforms.length ? platforms : [{ name: "Web", value: "web" }],
70
+ default: "web",
71
+ },
72
+ {
73
+ type: "select",
74
+ name: "framework",
75
+ message: "Framework?",
76
+ choices: [...frameworks, { name: "Other / none", value: "none" }],
77
+ },
78
+
79
+ // Design preferences — select knowledge VARIANTS when the KB provides them.
80
+ {
81
+ type: "select",
82
+ name: "density",
83
+ message: "Interface density?",
84
+ choices: [
85
+ { name: "Comfortable (default)", value: "comfortable" },
86
+ { name: "Dense (data-heavy screens)", value: "dense" },
87
+ ],
88
+ default: "comfortable",
89
+ },
90
+ {
91
+ type: "select",
92
+ name: "visualTone",
93
+ message: "Visual tone?",
94
+ choices: [
95
+ { name: "Minimal / professional (default)", value: "minimal" },
96
+ { name: "Expressive / bold", value: "expressive" },
97
+ ],
98
+ default: "minimal",
99
+ },
100
+ {
101
+ type: "select",
102
+ name: "colorMode",
103
+ message: "Default color mode?",
104
+ choices: [
105
+ { name: "Follow system (default)", value: "system" },
106
+ { name: "Light", value: "light" },
107
+ { name: "Dark", value: "dark" },
108
+ ],
109
+ default: "system",
110
+ },
111
+ ];
112
+ }
113
+
114
+ module.exports = { buildQuestions };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Resolver: turns a project profile + the KB registry into the concrete set of
3
+ * knowledge files to install. This is the module a future, smarter resolver
4
+ * replaces — commands depend only on `resolve(profile, registry)`.
5
+ *
6
+ * Selection rules:
7
+ * - Always include: brand.*, core presentation (spacing, typography),
8
+ * content.voice-tone, and accessibility for the chosen platform.
9
+ * - Include the chosen product (product.<product>).
10
+ * - Include the chosen pattern (pattern.<projectType>).
11
+ * - Include the chosen skill (skill.<framework>).
12
+ * - Include any knowledge whose applies_to matches projectType AND whose
13
+ * platforms include the chosen platform (empty platforms = all).
14
+ * - Then close over `dependencies` transitively.
15
+ */
16
+
17
+ function platformMatches(entry, platform) {
18
+ return !entry.platforms || entry.platforms.length === 0 || entry.platforms.includes(platform);
19
+ }
20
+
21
+ function productMatches(entry, product) {
22
+ return !entry.products || entry.products.length === 0 || entry.products.includes(product);
23
+ }
24
+
25
+ function resolve(profile, registry) {
26
+ const byId = new Map(registry.files.map((f) => [f.id, f]));
27
+ const selected = new Set();
28
+
29
+ const add = (id) => {
30
+ if (byId.has(id)) selected.add(id);
31
+ };
32
+
33
+ for (const entry of registry.files) {
34
+ // Variants never enter through base selection — only via substitution below.
35
+ if (entry.variant_of) continue;
36
+ if (!platformMatches(entry, profile.platform)) continue;
37
+ if (!productMatches(entry, profile.product)) continue;
38
+
39
+ // Always-on foundations. Themes and checklists are filtered by the
40
+ // platform/product guards above (e.g. theme.visual-ai only installs for visual-ai).
41
+ if (entry.category === "brand") add(entry.id);
42
+ if (entry.category === "theme") add(entry.id);
43
+ if (entry.category === "checklist") add(entry.id);
44
+
45
+ // Product-scoped knowledge: a file declaring products: [x] is by definition
46
+ // relevant to product x — auto-install when the profile's product matches.
47
+ // (Files with empty products pass productMatches for everyone, so require
48
+ // an explicit non-empty products list here.)
49
+ if (entry.products && entry.products.length > 0) add(entry.id);
50
+ if (entry.id === "presentation.spacing" || entry.id === "presentation.typography") add(entry.id);
51
+ if (entry.id === "content.voice-tone") add(entry.id);
52
+ if (entry.category === "accessibility") add(entry.id);
53
+
54
+ // Profile-driven selections.
55
+ if (entry.id === `product.${profile.product}`) add(entry.id);
56
+ if (entry.id === `pattern.${profile.projectType}`) add(entry.id);
57
+ if (entry.framework && entry.framework === profile.framework) add(entry.id);
58
+
59
+ // Knowledge that declares it applies to this project type.
60
+ if (entry.applies_to && entry.applies_to.includes(profile.projectType)) add(entry.id);
61
+ }
62
+
63
+ // Variant substitution: an entry with `variant_of: <baseId>` plus `when_*`
64
+ // conditions replaces its base when every condition matches the profile
65
+ // (e.g. when_density: dense). Authored via templates/variant-template.md.
66
+ for (const entry of registry.files) {
67
+ if (!entry.variant_of || !selected.has(entry.variant_of)) continue;
68
+ if (!platformMatches(entry, profile.platform)) continue;
69
+ if (!productMatches(entry, profile.product)) continue;
70
+ const conditions = Object.entries(entry).filter(([k]) => k.startsWith("when_"));
71
+ if (conditions.length === 0) continue;
72
+ const allMatch = conditions.every(([k, v]) => profile[k.slice(5)] === v);
73
+ if (allMatch) {
74
+ selected.delete(entry.variant_of);
75
+ selected.add(entry.id);
76
+ }
77
+ }
78
+
79
+ // Transitive dependency closure.
80
+ let changed = true;
81
+ while (changed) {
82
+ changed = false;
83
+ for (const id of [...selected]) {
84
+ const entry = byId.get(id);
85
+ for (const dep of entry.dependencies || []) {
86
+ if (byId.has(dep) && !selected.has(dep)) {
87
+ selected.add(dep);
88
+ changed = true;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // Return registry entries, ordered by priority for readable output/manifest.
95
+ const order = ["brand", "theme", "product", "pattern", "component", "navigation", "interaction", "content", "presentation", "accessibility", "skill", "checklist"];
96
+ return [...selected]
97
+ .map((id) => byId.get(id))
98
+ .sort((a, b) => {
99
+ const oa = order.indexOf(a.category);
100
+ const ob = order.indexOf(b.category);
101
+ if (oa !== ob) return oa - ob;
102
+ return a.id.localeCompare(b.id);
103
+ });
104
+ }
105
+
106
+ module.exports = { resolve };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Centralized error type. Commands throw CliError with a user-friendly message;
3
+ * index.js prints only that message (never stack traces, tokens, or headers).
4
+ */
5
+ class CliError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "CliError";
9
+ this.isCliError = true;
10
+ }
11
+ }
12
+
13
+ module.exports = { CliError };
@@ -0,0 +1,43 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /** Create a directory (and parents) if it does not exist. */
5
+ function ensureDir(dir) {
6
+ fs.mkdirSync(dir, { recursive: true });
7
+ }
8
+
9
+ /** True if a path exists. */
10
+ function exists(target) {
11
+ return fs.existsSync(target);
12
+ }
13
+
14
+ /** Read and parse a JSON file. Returns null if it does not exist. */
15
+ function readJSON(file) {
16
+ if (!fs.existsSync(file)) return null;
17
+ // Strip a UTF-8 BOM if present (PowerShell 5.1's Out-File adds one).
18
+ return JSON.parse(fs.readFileSync(file, "utf8").replace(/^/, ""));
19
+ }
20
+
21
+ /** Write a value as pretty JSON (trailing newline). */
22
+ function writeJSON(file, value) {
23
+ ensureDir(path.dirname(file));
24
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
25
+ }
26
+
27
+ /** Write a text/markdown file, creating parent dirs. */
28
+ function writeText(file, content) {
29
+ ensureDir(path.dirname(file));
30
+ fs.writeFileSync(file, content);
31
+ }
32
+
33
+ /** True if a directory is writable (used to validate cwd before install). */
34
+ function isWritable(dir) {
35
+ try {
36
+ fs.accessSync(dir, fs.constants.W_OK);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ module.exports = { ensureDir, exists, readJSON, writeJSON, writeText, isWritable };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Version comparison. Kept deliberately simple for the POC but structured so a
3
+ * full semver implementation can replace `compareVersions` without touching callers.
4
+ *
5
+ * Returns: -1 if a < b, 0 if equal, 1 if a > b.
6
+ */
7
+ function compareVersions(a, b) {
8
+ const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
9
+ const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
10
+ const len = Math.max(pa.length, pb.length);
11
+ for (let i = 0; i < len; i++) {
12
+ const x = pa[i] || 0;
13
+ const y = pb[i] || 0;
14
+ if (x < y) return -1;
15
+ if (x > y) return 1;
16
+ }
17
+ return 0;
18
+ }
19
+
20
+ module.exports = { compareVersions };