@readme/cli 0.0.26 → 0.0.27

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/src/utils/lint.js CHANGED
@@ -1,6 +1,6 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
4
 
5
5
  const TARGET_PATTERNS = [
6
6
  { dir: 'custom_blocks', ext: ['.mdx', '.md'] },
@@ -8,28 +8,28 @@ const TARGET_PATTERNS = [
8
8
  { dir: 'reference', ext: '.md' },
9
9
  { dir: 'custom_pages', ext: '.md' },
10
10
  { dir: 'recipes', ext: '.md' },
11
- ];
11
+ ]
12
12
 
13
13
  /**
14
14
  * Collect all target files from the repo root.
15
15
  */
16
16
  export function collectFiles(gitRoot) {
17
- const files = [];
17
+ const files = []
18
18
 
19
19
  for (const { dir, ext } of TARGET_PATTERNS) {
20
- const dirPath = path.join(gitRoot, dir);
21
- if (!fs.existsSync(dirPath)) continue;
20
+ const dirPath = path.join(gitRoot, dir)
21
+ if (!fs.existsSync(dirPath)) continue
22
22
 
23
- const exts = Array.isArray(ext) ? ext : [ext];
24
- const entries = fs.readdirSync(dirPath, { recursive: true });
23
+ const exts = Array.isArray(ext) ? ext : [ext]
24
+ const entries = fs.readdirSync(dirPath, { recursive: true })
25
25
  for (const entry of entries) {
26
26
  if (exts.some((e) => entry.endsWith(e))) {
27
- files.push(path.join(dir, entry));
27
+ files.push(path.join(dir, entry))
28
28
  }
29
29
  }
30
30
  }
31
31
 
32
- return files.sort();
32
+ return files.sort()
33
33
  }
34
34
 
35
35
  /**
@@ -38,18 +38,18 @@ export function collectFiles(gitRoot) {
38
38
  * `validateAll()` for cross-file checks (like ordering).
39
39
  */
40
40
  async function loadValidators() {
41
- const validatorsDir = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'validators');
42
- const files = fs.readdirSync(validatorsDir).filter((f) => f.endsWith('.js'));
43
- const validators = [];
41
+ const validatorsDir = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'validators')
42
+ const files = fs.readdirSync(validatorsDir).filter((f) => f.endsWith('.js'))
43
+ const validators = []
44
44
 
45
45
  for (const file of files) {
46
- const mod = await import(pathToFileURL(path.join(validatorsDir, file)).href);
46
+ const mod = await import(pathToFileURL(path.join(validatorsDir, file)).href)
47
47
  if (mod.name && (mod.validate || mod.validateAll)) {
48
- validators.push(mod);
48
+ validators.push(mod)
49
49
  }
50
50
  }
51
51
 
52
- return validators;
52
+ return validators
53
53
  }
54
54
 
55
55
  /**
@@ -57,43 +57,43 @@ async function loadValidators() {
57
57
  * Each result has { file, rule, message, severity? } where severity defaults to 'error'.
58
58
  * Calls `onFile(relativePath)` before processing each file (for progress reporting).
59
59
  */
60
- export async function runValidators(files, gitRoot, { onFile, onBeforeCrossFile, fix } = {}) {
61
- const validators = await loadValidators();
62
- const results = [];
60
+ export async function runValidators(files, gitRoot, { onFile, onBeforeCrossFile, fix, nonInteractive } = {}) {
61
+ const validators = await loadValidators()
62
+ const results = []
63
63
 
64
64
  // Per-file validators.
65
65
  for (const relativePath of files) {
66
- if (onFile) onFile(relativePath);
66
+ if (onFile) onFile(relativePath)
67
67
 
68
- const filePath = path.join(gitRoot, relativePath);
69
- const content = fs.readFileSync(filePath, 'utf-8');
68
+ const filePath = path.join(gitRoot, relativePath)
69
+ const content = fs.readFileSync(filePath, 'utf-8')
70
70
 
71
71
  for (const validator of validators) {
72
- if (!validator.validate) continue;
73
- const result = validator.validate({ filePath, content, relativePath, fix });
72
+ if (!validator.validate) continue
73
+ const result = validator.validate({ filePath, content, relativePath, fix })
74
74
  if (result) {
75
75
  if (Array.isArray(result)) {
76
- results.push(...result);
76
+ results.push(...result)
77
77
  } else {
78
- results.push(result);
78
+ results.push(result)
79
79
  }
80
80
  }
81
81
  }
82
82
  }
83
83
 
84
84
  // Cross-file validators — stop the spinner first so interactive prompts work.
85
- if (onBeforeCrossFile) onBeforeCrossFile();
85
+ if (onBeforeCrossFile) onBeforeCrossFile()
86
86
  for (const validator of validators) {
87
- if (!validator.validateAll) continue;
88
- const result = await validator.validateAll(files, gitRoot, { fix });
87
+ if (!validator.validateAll) continue
88
+ const result = await validator.validateAll(files, gitRoot, { fix, nonInteractive })
89
89
  if (result) {
90
90
  if (Array.isArray(result)) {
91
- results.push(...result);
91
+ results.push(...result)
92
92
  } else {
93
- results.push(result);
93
+ results.push(result)
94
94
  }
95
95
  }
96
96
  }
97
97
 
98
- return results;
98
+ return results
99
99
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Talks to the readmeio/marketplace repo: lists the available MDX components,
3
+ * fetches a single component's files (the .mdx source + its readme.md), and
4
+ * caches everything on disk so we're not hitting GitHub on every command.
5
+ *
6
+ * Components are read at https://github.com/readmeio/marketplace/tree/main/components
7
+ * and copied into the user's local `components/` folder. The marketplace
8
+ * `.mdx` files are the canonical source — they hold the JSX export plus an
9
+ * example invocation at the bottom.
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+
16
+ const REPO = 'readmeio/marketplace';
17
+ const BRANCH = 'main';
18
+ const COMPONENTS_PATH = 'components';
19
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
20
+
21
+ const CACHE_DIR = path.join(os.homedir(), '.cache', 'readme-cli', 'marketplace');
22
+
23
+ function ensureCacheDir() {
24
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
25
+ }
26
+
27
+ function cachePath(name) {
28
+ return path.join(CACHE_DIR, name);
29
+ }
30
+
31
+ function readCache(name, maxAgeMs = CACHE_TTL_MS) {
32
+ const file = cachePath(name);
33
+ try {
34
+ const stat = fs.statSync(file);
35
+ if (Date.now() - stat.mtimeMs > maxAgeMs) return null;
36
+ return fs.readFileSync(file, 'utf-8');
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function writeCache(name, contents) {
43
+ ensureCacheDir();
44
+ fs.writeFileSync(cachePath(name), contents, 'utf-8');
45
+ }
46
+
47
+ async function fetchText(url) {
48
+ const res = await fetch(url, {
49
+ headers: { 'User-Agent': '@readme/cli', Accept: 'application/vnd.github.v3+json' },
50
+ });
51
+ if (!res.ok) {
52
+ throw new Error(`GET ${url} → ${res.status} ${res.statusText}`);
53
+ }
54
+ return res.text();
55
+ }
56
+
57
+ /**
58
+ * Returns the list of marketplace components as an array of:
59
+ * { name, mdxUrl, readmeUrl, htmlUrl }
60
+ * Hidden entries (e.g. `.ExampleComponent`) are filtered out.
61
+ */
62
+ export async function listMarketplaceComponents({ force = false } = {}) {
63
+ const cacheKey = 'index.json';
64
+ if (!force) {
65
+ const cached = readCache(cacheKey);
66
+ if (cached) {
67
+ try { return JSON.parse(cached); } catch { /* fall through */ }
68
+ }
69
+ }
70
+
71
+ const url = `https://api.github.com/repos/${REPO}/contents/${COMPONENTS_PATH}?ref=${BRANCH}`;
72
+ const body = await fetchText(url);
73
+ const entries = JSON.parse(body);
74
+
75
+ const components = entries
76
+ .filter((e) => e.type === 'dir' && !e.name.startsWith('.'))
77
+ .map((e) => ({
78
+ name: e.name,
79
+ htmlUrl: `https://github.com/${REPO}/tree/${BRANCH}/${COMPONENTS_PATH}/${e.name}`,
80
+ mdxUrl: `https://raw.githubusercontent.com/${REPO}/${BRANCH}/${COMPONENTS_PATH}/${e.name}/${e.name}.mdx`,
81
+ readmeUrl: `https://raw.githubusercontent.com/${REPO}/${BRANCH}/${COMPONENTS_PATH}/${e.name}/readme.md`,
82
+ }));
83
+
84
+ writeCache(cacheKey, JSON.stringify(components, null, 2));
85
+ return components;
86
+ }
87
+
88
+ /**
89
+ * Fetch the .mdx + readme.md for a single component. Returns:
90
+ * { name, mdx, readme, htmlUrl }
91
+ * `readme` may be null if the component has no readme.md.
92
+ */
93
+ export async function fetchMarketplaceComponent(name, { force = false } = {}) {
94
+ const components = await listMarketplaceComponents({ force });
95
+ const match = components.find((c) => c.name.toLowerCase() === name.toLowerCase());
96
+ if (!match) {
97
+ throw new Error(`Component "${name}" not found in marketplace.`);
98
+ }
99
+
100
+ const mdxCacheKey = `${match.name}.mdx`;
101
+ const readmeCacheKey = `${match.name}.readme.md`;
102
+
103
+ let mdx = !force && readCache(mdxCacheKey);
104
+ if (!mdx) {
105
+ mdx = await fetchText(match.mdxUrl);
106
+ writeCache(mdxCacheKey, mdx);
107
+ }
108
+
109
+ let readme = !force && readCache(readmeCacheKey);
110
+ if (!readme) {
111
+ try {
112
+ readme = await fetchText(match.readmeUrl);
113
+ writeCache(readmeCacheKey, readme);
114
+ } catch {
115
+ readme = null;
116
+ }
117
+ }
118
+
119
+ return { name: match.name, mdx, readme, htmlUrl: match.htmlUrl };
120
+ }
121
+
122
+ /**
123
+ * Pull the one-line summary out of a component's readme.md. We look for the
124
+ * first non-empty paragraph after the `## Overview` heading, falling back to
125
+ * the first non-heading line. Returns "" if nothing usable is found.
126
+ */
127
+ export function summarizeReadme(readme) {
128
+ if (!readme) return '';
129
+ const lines = readme.split('\n');
130
+ let inOverview = false;
131
+ for (let i = 0; i < lines.length; i++) {
132
+ const line = lines[i].trim();
133
+ if (/^##\s+overview/i.test(line)) { inOverview = true; continue; }
134
+ if (inOverview && line && !line.startsWith('#') && !line.startsWith('![')) {
135
+ return line.replace(/[*_`]/g, '').slice(0, 140);
136
+ }
137
+ }
138
+ // Fallback: first non-heading, non-image line.
139
+ for (const line of lines) {
140
+ const t = line.trim();
141
+ if (t && !t.startsWith('#') && !t.startsWith('![') && !t.startsWith('<')) {
142
+ return t.replace(/[*_`]/g, '').slice(0, 140);
143
+ }
144
+ }
145
+ return '';
146
+ }
147
+
148
+ /**
149
+ * Pull the first example invocation block out of a component's mdx. Components
150
+ * conventionally end with a bare `<ComponentName ... />` line below the React
151
+ * export — return that. Returns the full mdx as a fallback.
152
+ */
153
+ export function extractExample(mdx, name) {
154
+ const re = new RegExp(`^<${name}[\\s\\S]*?(?:/>|</${name}>)\\s*$`, 'm');
155
+ const match = mdx.match(re);
156
+ return match ? match[0] : '';
157
+ }
158
+
159
+ /**
160
+ * Returns the absolute path to the local `components/` folder for a repo
161
+ * (relative to gitRoot). Does not create it.
162
+ */
163
+ export function localComponentsDir(gitRoot) {
164
+ return path.join(gitRoot, 'components');
165
+ }
166
+
167
+ /**
168
+ * Read a previously-cached readme for a component without hitting the network.
169
+ * Returns null if there's nothing cached. Used so we can show short
170
+ * descriptions in prompts/lists without slowing down the command on a cold
171
+ * cache.
172
+ */
173
+ export function readCachedReadme(name) {
174
+ return readCache(`${name}.readme.md`, Infinity);
175
+ }
176
+
177
+ /**
178
+ * Install a marketplace component into the user's local components/ folder.
179
+ * Used by both the `components add` subcommand and the auto-install path in
180
+ * `pretty`. Returns the destination path on success.
181
+ */
182
+ export async function installComponent(gitRoot, name, { force = false } = {}) {
183
+ const comp = await fetchMarketplaceComponent(name, { force });
184
+ const dest = localComponentsDir(gitRoot);
185
+ fs.mkdirSync(dest, { recursive: true });
186
+ const file = path.join(dest, `${comp.name}.mdx`);
187
+ fs.writeFileSync(file, comp.mdx, 'utf-8');
188
+ return { name: comp.name, file, mdx: comp.mdx };
189
+ }
190
+
191
+ /**
192
+ * List components that have been copied into the user's local components/
193
+ * folder. Returns an array of { name, file, source } where `source` is the
194
+ * raw mdx contents.
195
+ */
196
+ export function listLocalComponents(gitRoot) {
197
+ const dir = localComponentsDir(gitRoot);
198
+ if (!fs.existsSync(dir)) return [];
199
+
200
+ const out = [];
201
+ for (const entry of fs.readdirSync(dir)) {
202
+ if (!entry.endsWith('.mdx')) continue;
203
+ const file = path.join(dir, entry);
204
+ const source = fs.readFileSync(file, 'utf-8');
205
+ out.push({ name: entry.replace(/\.mdx$/, ''), file, source });
206
+ }
207
+ return out.sort((a, b) => a.name.localeCompare(b.name));
208
+ }
@@ -0,0 +1,71 @@
1
+ import { createLogUpdate } from 'log-update';
2
+ import cliSpinners from 'cli-spinners';
3
+ import chalk from 'chalk';
4
+
5
+ /**
6
+ * Multi-row spinner. Renders `rows` labeled lines, repainted in unison at the
7
+ * spinner's tick interval. Cleared rows render as blank lines (the live region
8
+ * keeps a fixed height). Falls back to no-op render + line-buffered `log()`
9
+ * in non-TTY environments.
10
+ */
11
+ export class MultiSpinner {
12
+ constructor({ rows, color = 'blue', stream = process.stderr } = {}) {
13
+ this.rows = Math.max(0, rows);
14
+ this.color = color;
15
+ this.stream = stream;
16
+ this.isTTY = !!stream.isTTY;
17
+ this.lines = new Array(this.rows).fill('');
18
+ this.frame = 0;
19
+ this.spinner = cliSpinners.dots;
20
+ this.timer = null;
21
+ this.update = null;
22
+ }
23
+
24
+ start() {
25
+ if (!this.isTTY) return;
26
+ this.update = createLogUpdate(this.stream);
27
+ this._render();
28
+ this.timer = setInterval(() => this._render(), this.spinner.interval);
29
+ }
30
+
31
+ setLine(rowIdx, text) {
32
+ if (!this.isTTY) return;
33
+ if (rowIdx < 0 || rowIdx >= this.rows) return;
34
+ this.lines[rowIdx] = text;
35
+ }
36
+
37
+ clearLine(rowIdx) {
38
+ this.setLine(rowIdx, '');
39
+ }
40
+
41
+ log(text) {
42
+ if (this.isTTY && this.update) {
43
+ this.update.persist(text);
44
+ } else {
45
+ this.stream.write(text + '\n');
46
+ }
47
+ }
48
+
49
+ stop() {
50
+ if (this.timer) {
51
+ clearInterval(this.timer);
52
+ this.timer = null;
53
+ }
54
+ if (this.update) {
55
+ this.update.clear();
56
+ this.update.done();
57
+ this.update = null;
58
+ }
59
+ }
60
+
61
+ _render() {
62
+ if (!this.update) return;
63
+ const glyph = this.spinner.frames[this.frame % this.spinner.frames.length];
64
+ const colorize = chalk[this.color] ?? ((s) => s);
65
+ const out = this.lines
66
+ .map((text) => (text ? `${colorize(glyph)} ${text}` : ''))
67
+ .join('\n');
68
+ this.update(out);
69
+ this.frame++;
70
+ }
71
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Run `worker` over `items` with at most `concurrency` calls in flight.
3
+ * Each worker pulls the next item from a shared cursor; finishes when none left.
4
+ * `worker(item, index, workerIdx)` is responsible for catching its own errors —
5
+ * an uncaught rejection will propagate up and abort other in-flight workers.
6
+ */
7
+ export async function runPool(items, concurrency, worker) {
8
+ let cursor = 0;
9
+ const next = () => (cursor < items.length ? cursor++ : -1);
10
+ const lanes = Math.max(1, Math.min(concurrency, items.length));
11
+ const workers = Array.from({ length: lanes }, (_, workerIdx) => (async () => {
12
+ for (let i = next(); i !== -1; i = next()) {
13
+ await worker(items[i], i, workerIdx);
14
+ }
15
+ })());
16
+ await Promise.all(workers);
17
+ }
@@ -1,68 +1,65 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import readline from 'node:readline';
4
- import * as styles from '../utils/styles.js';
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import readline from 'node:readline'
4
+ import * as styles from '../utils/styles.js'
5
5
 
6
- export const name = 'numbering';
6
+ export const name = 'numbering'
7
7
 
8
- const SUFFIX_RE = /-(\d+)$/;
8
+ const SUFFIX_RE = /-(\d+)$/
9
9
 
10
10
  function prompt(question) {
11
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
12
12
  return new Promise((resolve) => {
13
13
  rl.question(question, (answer) => {
14
- rl.close();
15
- resolve(answer.trim().toLowerCase());
16
- });
17
- });
14
+ rl.close()
15
+ resolve(answer.trim().toLowerCase())
16
+ })
17
+ })
18
18
  }
19
19
 
20
20
  function updateOrderYaml(fromPath, toPath) {
21
- const dir = path.dirname(fromPath);
22
- const orderFile = path.join(dir, '_order.yaml');
23
- if (!fs.existsSync(orderFile)) return;
21
+ const dir = path.dirname(fromPath)
22
+ const orderFile = path.join(dir, '_order.yaml')
23
+ if (!fs.existsSync(orderFile)) return
24
24
 
25
- const oldSlug = path.basename(fromPath).replace(/\.(md|mdx)$/, '');
26
- const newSlug = path.basename(toPath).replace(/\.(md|mdx)$/, '');
25
+ const oldSlug = path.basename(fromPath).replace(/\.(md|mdx)$/, '')
26
+ const newSlug = path.basename(toPath).replace(/\.(md|mdx)$/, '')
27
27
 
28
- const content = fs.readFileSync(orderFile, 'utf-8');
29
- const updated = content.replace(
30
- new RegExp(`^(- )${oldSlug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'),
31
- `$1${newSlug}`,
32
- );
28
+ const content = fs.readFileSync(orderFile, 'utf-8')
29
+ const updated = content.replace(new RegExp(`^(- )${oldSlug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'), `$1${newSlug}`)
33
30
 
34
31
  if (updated !== content) {
35
- fs.writeFileSync(orderFile, updated);
32
+ fs.writeFileSync(orderFile, updated)
36
33
  }
37
34
  }
38
35
 
39
- export async function validateAll(files, gitRoot, { fix } = {}) {
40
- const results = [];
41
- const renames = [];
36
+ export async function validateAll(files, gitRoot, { fix, nonInteractive } = {}) {
37
+ const results = []
38
+ const renames = []
42
39
 
43
40
  // Collect all slugs (filenames without ext) and directory names across the repo.
44
- const allSlugs = new Set();
45
- const allDirs = new Set();
41
+ const allSlugs = new Set()
42
+ const allDirs = new Set()
46
43
  for (const relPath of files) {
47
- allSlugs.add(path.basename(relPath).replace(/\.(md|mdx)$/, ''));
48
- const parts = relPath.split('/');
44
+ allSlugs.add(path.basename(relPath).replace(/\.(md|mdx)$/, ''))
45
+ const parts = relPath.split('/')
49
46
  for (let i = 0; i < parts.length - 1; i++) {
50
- allDirs.add(parts[i]);
47
+ allDirs.add(parts[i])
51
48
  }
52
49
  }
53
50
 
54
51
  // Check files: if slug ends with -N and the base slug doesn't exist anywhere, warn.
55
52
  for (const relPath of files) {
56
- const slug = path.basename(relPath).replace(/\.(md|mdx)$/, '');
57
- const match = slug.match(SUFFIX_RE);
58
- if (!match) continue;
53
+ const slug = path.basename(relPath).replace(/\.(md|mdx)$/, '')
54
+ const match = slug.match(SUFFIX_RE)
55
+ if (!match) continue
59
56
 
60
- const baseSlug = slug.slice(0, -match[0].length);
57
+ const baseSlug = slug.slice(0, -match[0].length)
61
58
  if (!allSlugs.has(baseSlug)) {
62
- const ext = path.extname(relPath);
63
- const from = path.join(gitRoot, relPath);
64
- const toRel = path.join(path.dirname(relPath), `${baseSlug}${ext}`);
65
- const to = path.join(gitRoot, toRel);
59
+ const ext = path.extname(relPath)
60
+ const from = path.join(gitRoot, relPath)
61
+ const toRel = path.join(path.dirname(relPath), `${baseSlug}${ext}`)
62
+ const to = path.join(gitRoot, toRel)
66
63
 
67
64
  results.push({
68
65
  file: relPath,
@@ -70,29 +67,29 @@ export async function validateAll(files, gitRoot, { fix } = {}) {
70
67
  severity: 'warning',
71
68
  fixable: true,
72
69
  message: `Unnecessary suffix: "${slug}${ext}" should be renamed to "${baseSlug}${ext}"`,
73
- });
74
- renames.push({ from, to, label: `${relPath} → ${toRel}` });
70
+ })
71
+ renames.push({ from, to, label: `${relPath} → ${toRel}` })
75
72
  }
76
73
  }
77
74
 
78
75
  // Check directories: if a dir name ends with -N and the base dir doesn't exist, warn.
79
- const warnedDirs = new Set();
76
+ const warnedDirs = new Set()
80
77
  for (const relPath of files) {
81
- const parts = relPath.split('/');
78
+ const parts = relPath.split('/')
82
79
  for (let i = 0; i < parts.length - 1; i++) {
83
- const dirName = parts[i];
84
- if (warnedDirs.has(dirName)) continue;
80
+ const dirName = parts[i]
81
+ if (warnedDirs.has(dirName)) continue
85
82
 
86
- const match = dirName.match(SUFFIX_RE);
87
- if (!match) continue;
83
+ const match = dirName.match(SUFFIX_RE)
84
+ if (!match) continue
88
85
 
89
- const baseName = dirName.slice(0, -match[0].length);
86
+ const baseName = dirName.slice(0, -match[0].length)
90
87
  if (!allDirs.has(baseName)) {
91
- warnedDirs.add(dirName);
92
- const dirPath = parts.slice(0, i + 1).join('/');
93
- const baseDirPath = [...parts.slice(0, i), baseName].join('/');
94
- const from = path.join(gitRoot, dirPath);
95
- const to = path.join(gitRoot, baseDirPath);
88
+ warnedDirs.add(dirName)
89
+ const dirPath = parts.slice(0, i + 1).join('/')
90
+ const baseDirPath = [...parts.slice(0, i), baseName].join('/')
91
+ const from = path.join(gitRoot, dirPath)
92
+ const to = path.join(gitRoot, baseDirPath)
96
93
 
97
94
  results.push({
98
95
  file: dirPath,
@@ -100,37 +97,38 @@ export async function validateAll(files, gitRoot, { fix } = {}) {
100
97
  severity: 'warning',
101
98
  fixable: true,
102
99
  message: `Unnecessary suffix: "${dirName}" folder should be renamed to "${baseName}"`,
103
- });
104
- renames.push({ from, to, label: `${dirPath}/ → ${baseDirPath}/` });
100
+ })
101
+ renames.push({ from, to, label: `${dirPath}/ → ${baseDirPath}/` })
105
102
  }
106
103
  }
107
104
  }
108
105
 
109
106
  // Interactive rename when --fix is passed.
110
107
  if (fix && renames.length > 0) {
111
- console.log();
112
- console.log(` The following will be renamed:`);
108
+ console.log()
109
+ console.log(` The following will be renamed:`)
113
110
  for (const r of renames) {
114
- console.log(` ${styles.dim(r.label)}`);
111
+ console.log(` ${styles.dim(r.label)}`)
115
112
  }
116
- console.log();
117
- console.log(` ${styles.warn('Note:')} Renaming changes slugs, which could break existing URLs.`);
118
- console.log();
113
+ console.log()
114
+ console.log(` ${styles.warn('Note:')} Renaming changes slugs, which could break existing URLs.`)
115
+ console.log()
119
116
 
120
- const answer = await prompt(` Rename ${renames.length} ${renames.length === 1 ? 'path' : 'paths'}? (y/N) `);
117
+ // assume yes if non-interactive, otherwise prompt for confirmation
118
+ const answer = nonInteractive ? 'yes' : await prompt(` Rename ${renames.length} ${renames.length === 1 ? 'path' : 'paths'}? (y/N) `)
121
119
 
122
120
  if (answer === 'y' || answer === 'yes') {
123
121
  // Sort longest path first so nested dirs get renamed before parents.
124
- renames.sort((a, b) => b.from.length - a.from.length);
122
+ renames.sort((a, b) => b.from.length - a.from.length)
125
123
  for (const r of renames) {
126
- fs.renameSync(r.from, r.to);
127
- updateOrderYaml(r.from, r.to);
124
+ fs.renameSync(r.from, r.to)
125
+ updateOrderYaml(r.from, r.to)
128
126
  }
129
127
  for (const r of results) {
130
- r.message += ' (fixed)';
128
+ r.message += ' (fixed)'
131
129
  }
132
130
  }
133
131
  }
134
132
 
135
- return results.length > 0 ? results : null;
133
+ return results.length > 0 ? results : null
136
134
  }