@rokkit/cli 1.0.0-next.147 → 1.0.0-next.150

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Load rokkit.config.js using injected adapter or real filesystem.
3
+ * Returns null (not an error) if config is absent or fails to load.
4
+ * @param {{ readConfig?: () => Record<string, unknown>, cwd?: string }} adapters
5
+ * @returns {Promise<Record<string, unknown> | null>}
6
+ */
7
+ export function loadConfig(adapters: {
8
+ readConfig?: () => Record<string, unknown>;
9
+ cwd?: string;
10
+ }): Promise<Record<string, unknown> | null>;
@@ -50,7 +50,7 @@ export function getFolderNames(dir: string): string[];
50
50
  */
51
51
  export function bundleFolders(folders: string[], config: import("./types").IconBuilderOptions, opts: import("./types").FolderOptions): void;
52
52
  /**
53
- * Converts a folder of icons into an icon bundle
53
+ * Converts a folder of icons into individual SVG files
54
54
  * @param {string[]} folders
55
55
  * @param {import('./types').IconBuilderOptions} config
56
56
  * @param {import('./types').FolderOptions} opts
@@ -1,16 +1,18 @@
1
- /**
2
- * Generate a Rokkit config object from user choices.
3
- * @param {{ palette: string, customColors?: Record<string, string>, icons: string, iconPath?: string, themes: string[], switcher: string }} opts
4
- * @returns {Record<string, unknown>}
5
- */
6
- export function generateConfig({ palette, customColors, icons, iconPath, themes, switcher }: {
7
- palette: string;
8
- customColors?: Record<string, string>;
9
- icons: string;
10
- iconPath?: string;
11
- themes: string[];
12
- switcher: string;
13
- }): Record<string, unknown>;
1
+ export function generateConfig({ palette, customColors, icons, iconPath, themes, defaultTheme, switcher }: {
2
+ palette: any;
3
+ customColors: any;
4
+ icons: any;
5
+ iconPath: any;
6
+ themes: any;
7
+ defaultTheme: any;
8
+ switcher: any;
9
+ }): {
10
+ colors: Record<string, unknown>;
11
+ themes: any;
12
+ defaultTheme: any;
13
+ switcher: any;
14
+ storageKey: string;
15
+ };
14
16
  /**
15
17
  * Generate the content for uno.config.js.
16
18
  * @returns {string}
@@ -29,7 +31,7 @@ export function generateAppCssImports(themes: string[]): string[];
29
31
  * @param {string} [storageKey='rokkit-theme']
30
32
  * @returns {string | null}
31
33
  */
32
- export function generateInitScript(switcher: string, storageKey?: string): string | null;
34
+ export function generateInitScript(switcher: string, storageKey?: string, defaultStyle?: string): string | null;
33
35
  /**
34
36
  * Interactive init command — prompts the user, writes config files.
35
37
  */
@@ -16,31 +16,8 @@ export function addSkinToConfig(config: Record<string, unknown>, name: string):
16
16
  * @returns {string}
17
17
  */
18
18
  export function serializeConfig(config: Record<string, unknown>): string;
19
- /**
20
- * List all skins defined in rokkit.config.js.
21
- * @param {{
22
- * readConfig?: () => Record<string, unknown>,
23
- * cwd?: string
24
- * }} adapters
25
- */
26
- export function runSkinList(adapters?: {
27
- readConfig?: () => Record<string, unknown>;
28
- cwd?: string;
29
- }): Promise<void>;
30
- /**
31
- * Create a new skin entry in rokkit.config.js.
32
- * @param {string} name
33
- * @param {{
34
- * readConfig?: () => Record<string, unknown>,
35
- * writeConfig?: (config: Record<string, unknown>) => void,
36
- * cwd?: string
37
- * }} adapters
38
- */
39
- export function runSkinCreate(name: string, adapters?: {
40
- readConfig?: () => Record<string, unknown>;
41
- writeConfig?: (config: Record<string, unknown>) => void;
42
- cwd?: string;
43
- }): Promise<void>;
19
+ export function runSkinList(adapters?: {}): Promise<void>;
20
+ export function runSkinCreate(name: any, adapters?: {}): Promise<void>;
44
21
  /**
45
22
  * CLI entry point for `rokkit skin` subcommands.
46
23
  * @param {string} sub — 'list' | 'create'
@@ -5,35 +5,8 @@
5
5
  * @returns {string}
6
6
  */
7
7
  export function generateThemeStub(name: string): string;
8
- /**
9
- * List all theme styles defined in rokkit.config.js plus any custom CSS files in src/themes/.
10
- * @param {{
11
- * readConfig?: () => Record<string, unknown>,
12
- * listFiles?: (dir: string) => string[],
13
- * cwd?: string
14
- * }} adapters
15
- */
16
- export function runThemeList(adapters?: {
17
- readConfig?: () => Record<string, unknown>;
18
- listFiles?: (dir: string) => string[];
19
- cwd?: string;
20
- }): Promise<void>;
21
- /**
22
- * Create a new custom theme CSS stub at src/themes/<name>.css.
23
- * @param {string} name
24
- * @param {{
25
- * writeFile?: (path: string, content: string) => void,
26
- * exists?: (path: string) => boolean,
27
- * mkdir?: (dir: string) => void,
28
- * cwd?: string
29
- * }} adapters
30
- */
31
- export function runThemeCreate(name: string, adapters?: {
32
- writeFile?: (path: string, content: string) => void;
33
- exists?: (path: string) => boolean;
34
- mkdir?: (dir: string) => void;
35
- cwd?: string;
36
- }): Promise<void>;
8
+ export function runThemeList(adapters?: {}): Promise<void>;
9
+ export function runThemeCreate(name: any, adapters?: {}): void;
37
10
  /**
38
11
  * CLI entry point for `rokkit theme` subcommands.
39
12
  * @param {string} sub — 'list' | 'create'
@@ -23,25 +23,7 @@ export function buildInstallCommand(pm: "bun" | "pnpm" | "yarn" | "npm", package
23
23
  bin: string;
24
24
  args: string[];
25
25
  };
26
- /**
27
- * Run upgrade check and optionally apply it.
28
- *
29
- * @param {{ apply?: boolean }} opts
30
- * @param {{
31
- * readFile?: (p: string) => string,
32
- * exists?: (p: string) => boolean,
33
- * fetchVersion?: (name: string) => string | null,
34
- * runInstall?: (bin: string, args: string[]) => void
35
- * }} adapters — injectable for testing
36
- */
37
- export function runUpgrade(opts?: {
38
- apply?: boolean;
39
- }, adapters?: {
40
- readFile?: (p: string) => string;
41
- exists?: (p: string) => boolean;
42
- fetchVersion?: (name: string) => string | null;
43
- runInstall?: (bin: string, args: string[]) => void;
44
- }): void;
26
+ export function runUpgrade(opts?: {}, adapters?: {}): void;
45
27
  /**
46
28
  * CLI entry point for `rokkit upgrade`
47
29
  * @param {{ apply?: boolean }} opts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/cli",
3
- "version": "1.0.0-next.147",
3
+ "version": "1.0.0-next.150",
4
4
  "description": "Command line utilities for icon bundling and conversion.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/config.js ADDED
@@ -0,0 +1,23 @@
1
+ import { existsSync } from 'fs'
2
+ import { resolve } from 'path'
3
+
4
+ /**
5
+ * Load rokkit.config.js using injected adapter or real filesystem.
6
+ * Returns null (not an error) if config is absent or fails to load.
7
+ * @param {{ readConfig?: () => Record<string, unknown>, cwd?: string }} adapters
8
+ * @returns {Promise<Record<string, unknown> | null>}
9
+ */
10
+ export async function loadConfig(adapters) {
11
+ if (adapters.readConfig) return adapters.readConfig()
12
+
13
+ const cwd = adapters.cwd ?? process.cwd()
14
+ const configPath = resolve(cwd, 'rokkit.config.js')
15
+ if (!existsSync(configPath)) return null
16
+
17
+ try {
18
+ const { default: config } = await import(`file://${configPath}?t=${Date.now()}`)
19
+ return config
20
+ } catch {
21
+ return null
22
+ }
23
+ }
package/src/convert.js CHANGED
@@ -172,40 +172,39 @@ export function getFolderNames(dir) {
172
172
  }
173
173
  }
174
174
 
175
+ function folderOpts(folder, config, opts, extra) {
176
+ const folderConfig = config.bundles?.[folder] || {}
177
+ return { target: opts.output, color: folderConfig.color || false, prefix: folder, ...extra }
178
+ }
179
+
175
180
  /**
176
- * Converts a folder of icons into an icon bundle
177
181
  * @param {string[]} folders
178
182
  * @param {import('./types').IconBuilderOptions} config
179
- * @param {import('./types').FolderOptions} opts
183
+ * @param {import('./types').FolderOptions} opts
184
+ * @param {{ fn: Function, extra?: Record<string, unknown> }} processor
180
185
  */
181
- export function bundleFolders(folders, config, opts) {
186
+ function processFolders(folders, config, opts, { fn, extra = {} }) {
182
187
  for (const folder of folders) {
183
- const folderPath = path.join(opts.input, folder)
184
- const folderConfig = config.bundles?.[folder] || {}
185
-
186
- bundle(folderPath, {
187
- target: opts.output,
188
- color: folderConfig.color || false,
189
- prefix: folder
190
- })
188
+ fn(path.join(opts.input, folder), folderOpts(folder, config, opts, extra))
191
189
  }
192
190
  }
191
+
193
192
  /**
194
193
  * Converts a folder of icons into an icon bundle
194
+ * @param {string[]} folders
195
+ * @param {import('./types').IconBuilderOptions} config
196
+ * @param {import('./types').FolderOptions} opts
197
+ */
198
+ export function bundleFolders(folders, config, opts) {
199
+ processFolders(folders, config, opts, { fn: bundle })
200
+ }
201
+
202
+ /**
203
+ * Converts a folder of icons into individual SVG files
195
204
  * @param {string[]} folders
196
205
  * @param {import('./types').IconBuilderOptions} config
197
206
  * @param {import('./types').FolderOptions} opts
198
207
  */
199
208
  export function convertFolders(folders, config, opts) {
200
- for (const folder of folders) {
201
- const folderPath = path.join(opts.input, folder)
202
- const folderConfig = config.bundles[folder] || {}
203
-
204
- convert(folderPath, {
205
- target: opts.output,
206
- color: folderConfig.color || false,
207
- prefix: folder,
208
- package: config.package || {}
209
- })
210
- }
209
+ processFolders(folders, config, opts, { fn: convert, extra: { package: config.package || {} } })
211
210
  }
package/src/doctor.js CHANGED
@@ -3,6 +3,11 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'
3
3
  import { resolve } from 'path'
4
4
  import { generateUnoConfig, generateAppCssImports, generateInitScript } from './init.js'
5
5
 
6
+ const KNOWN_THEMES = [
7
+ 'rokkit', 'minimal', 'material', 'glass', 'grada-ui',
8
+ 'shadcn', 'daisy-ui', 'bits-ui', 'carbon', 'ant-design'
9
+ ]
10
+
6
11
  /**
7
12
  * Check config file existence
8
13
  * @param {Object} fs
@@ -28,10 +33,11 @@ function checkConfig(fs) {
28
33
  function checkUnoPreset(fs) {
29
34
  const unoPath = fs.resolve('uno.config.js')
30
35
  const unoExists = fs.exists(unoPath)
31
- const unoUsesPreset = unoExists && fs.read(unoPath).includes('presetRokkit')
36
+ const unoContent = unoExists ? fs.read(unoPath) : ''
37
+ const unoUsesPreset = unoContent.includes('presetRokkit') && unoContent.includes('rokkit.config')
32
38
  return {
33
39
  id: 'uno-uses-preset',
34
- label: 'uno.config.js uses presetRokkit()',
40
+ label: 'uno.config.js uses presetRokkit(config)',
35
41
  status: unoUsesPreset ? 'pass' : 'fail',
36
42
  fixable: false,
37
43
  fix: unoExists
@@ -48,17 +54,36 @@ function checkUnoPreset(fs) {
48
54
  function checkCssImports(fs) {
49
55
  const cssPath = fs.resolve('src/app.css')
50
56
  const cssExists = fs.exists(cssPath)
51
- const cssHasBase = cssExists && fs.read(cssPath).includes('@rokkit/themes/dist/base')
57
+ const cssHasBase = cssExists && fs.read(cssPath).includes('@rokkit/themes/base.css')
52
58
  return {
53
59
  id: 'css-imports',
54
- label: 'app.css has theme imports',
60
+ label: 'app.css imports @rokkit/themes/base.css',
55
61
  status: cssHasBase ? 'pass' : 'fail',
56
62
  fixable: true,
57
- fix: 'Append theme imports to src/app.css',
63
+ fix: 'Append base theme import to src/app.css',
58
64
  autoFix: 'patch-css'
59
65
  }
60
66
  }
61
67
 
68
+ /**
69
+ * Check app.css has at least one theme style
70
+ * @param {Object} fs
71
+ * @returns {Object}
72
+ */
73
+ function checkCssTheme(fs) {
74
+ const cssPath = fs.resolve('src/app.css')
75
+ const cssExists = fs.exists(cssPath)
76
+ const css = cssExists ? fs.read(cssPath) : ''
77
+ const hasTheme = KNOWN_THEMES.some((t) => css.includes(`@rokkit/themes/${t}.css`))
78
+ return {
79
+ id: 'css-theme',
80
+ label: 'app.css has a theme style',
81
+ status: hasTheme ? 'pass' : 'warn',
82
+ fixable: false,
83
+ fix: `Add a theme import to src/app.css, e.g. @import '@rokkit/themes/rokkit.css'\n Available: ${KNOWN_THEMES.join(', ')}, or use a custom theme`
84
+ }
85
+ }
86
+
62
87
  /**
63
88
  * Check app.html has init script
64
89
  * @param {Object} fs
@@ -86,7 +111,7 @@ function checkHtmlScript(fs) {
86
111
  * @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
87
112
  */
88
113
  export function runChecks(fs) {
89
- return [checkConfig(fs), checkUnoPreset(fs), checkCssImports(fs), checkHtmlScript(fs)]
114
+ return [checkConfig(fs), checkUnoPreset(fs), checkCssImports(fs), checkCssTheme(fs), checkHtmlScript(fs)]
90
115
  }
91
116
 
92
117
  /**
@@ -119,7 +144,7 @@ function applyGenerateConfig(cwd, label) {
119
144
  */
120
145
  function applyPatchCss(cwd, label) {
121
146
  const cssPath = resolve(cwd, 'src/app.css')
122
- const imports = generateAppCssImports(['rokkit'])
147
+ const imports = generateAppCssImports([])
123
148
  if (existsSync(cssPath)) {
124
149
  const existing = readFileSync(cssPath, 'utf-8')
125
150
  const missing = imports.filter((line) => !existing.includes(line))
@@ -189,7 +214,7 @@ function autoFix(checks, cwd) {
189
214
  */
190
215
  function printFixHints(checks) {
191
216
  for (const check of checks) {
192
- if (check.status === 'fail') {
217
+ if (check.status === 'fail' || check.status === 'warn') {
193
218
  console.info(` ${check.fixable ? '(auto-fixable) ' : ''}${check.fix}`)
194
219
  }
195
220
  }
@@ -215,7 +240,7 @@ function printManualItems(checks) {
215
240
  function printChecks(checks) {
216
241
  let failures = 0
217
242
  for (const check of checks) {
218
- const icon = check.status === 'pass' ? 'PASS' : 'FAIL'
243
+ const icon = check.status === 'pass' ? 'PASS' : check.status === 'warn' ? 'WARN' : 'FAIL'
219
244
  console.info(` ${icon} ${check.label}`)
220
245
  if (check.status === 'fail') failures++
221
246
  }
package/src/init.js CHANGED
@@ -36,13 +36,19 @@ const DEFAULT_COLORS = {
36
36
  * @param {{ palette: string, customColors?: Record<string, string>, icons: string, iconPath?: string, themes: string[], switcher: string }} opts
37
37
  * @returns {Record<string, unknown>}
38
38
  */
39
- export function generateConfig({ palette, customColors, icons, iconPath, themes, switcher }) {
40
- const colors =
41
- palette === 'custom'
42
- ? { ...DEFAULT_COLORS, ...customColors }
43
- : { ...DEFAULT_COLORS, ...(SKIN_PRESETS[palette] || {}) }
39
+ function resolveColors(palette, customColors) {
40
+ if (palette === 'custom') return { ...DEFAULT_COLORS, ...customColors }
41
+ return { ...DEFAULT_COLORS, ...(SKIN_PRESETS[palette] || {}) }
42
+ }
44
43
 
45
- const config = { colors, themes, switcher, storageKey: 'rokkit-theme' }
44
+ export function generateConfig({ palette, customColors, icons, iconPath, themes, defaultTheme, switcher }) {
45
+ const config = {
46
+ colors: resolveColors(palette, customColors),
47
+ themes,
48
+ defaultTheme: defaultTheme || themes[0],
49
+ switcher,
50
+ storageKey: 'rokkit-theme'
51
+ }
46
52
 
47
53
  if (icons === 'custom' && iconPath) {
48
54
  config.icons = { custom: iconPath }
@@ -58,9 +64,10 @@ export function generateConfig({ palette, customColors, icons, iconPath, themes,
58
64
  export function generateUnoConfig() {
59
65
  return `import { defineConfig } from 'unocss'
60
66
  import { presetRokkit } from '@rokkit/unocss'
67
+ import config from './rokkit.config.js'
61
68
 
62
69
  export default defineConfig({
63
- presets: [presetRokkit()]
70
+ presets: [presetRokkit(config)]
64
71
  })
65
72
  `
66
73
  }
@@ -71,9 +78,9 @@ export default defineConfig({
71
78
  * @returns {string[]}
72
79
  */
73
80
  export function generateAppCssImports(themes) {
74
- const lines = ["@import '@unocss/reset/tailwind.css';", "@import '@rokkit/themes/dist/base';"]
81
+ const lines = ["@import '@unocss/reset/tailwind.css';", "@import '@rokkit/themes/base.css';"]
75
82
  for (const theme of themes) {
76
- lines.push(`@import '@rokkit/themes/dist/${theme}';`)
83
+ lines.push(`@import '@rokkit/themes/${theme}.css';`)
77
84
  }
78
85
  return lines
79
86
  }
@@ -85,10 +92,10 @@ export function generateAppCssImports(themes) {
85
92
  * @param {string} [storageKey='rokkit-theme']
86
93
  * @returns {string | null}
87
94
  */
88
- export function generateInitScript(switcher, storageKey = 'rokkit-theme') {
95
+ export function generateInitScript(switcher, storageKey = 'rokkit-theme', defaultStyle = 'rokkit') {
89
96
  if (switcher === 'system') return null
90
97
 
91
- const setStyle = switcher === 'full' ? `b.dataset.style = t.style || 'rokkit'\n ` : ''
98
+ const setStyle = switcher === 'full' ? `b.dataset.style = t.style || '${defaultStyle}'\n ` : ''
92
99
 
93
100
  return ` <script>
94
101
  (function () {
@@ -159,10 +166,23 @@ const PROMPTS_CONFIG = [
159
166
  choices: [
160
167
  { title: 'Rokkit', value: 'rokkit', selected: true },
161
168
  { title: 'Minimal', value: 'minimal' },
162
- { title: 'Material', value: 'material' }
169
+ { title: 'Material', value: 'material' },
170
+ { title: 'Glass', value: 'glass' },
171
+ { title: 'Grada UI', value: 'grada-ui' },
172
+ { value: 'shadcn', title: 'shadcn', hint: 'flat borders + ring focus' },
173
+ { value: 'daisy-ui', title: 'daisy-ui', hint: 'rounded-full + bold fills' },
174
+ { value: 'bits-ui', title: 'bits-ui', hint: 'rounded-lg + polished' },
175
+ { value: 'carbon', title: 'carbon', hint: 'square edges + enterprise' },
176
+ { value: 'ant-design', title: 'ant-design', hint: 'thin borders + dense' }
163
177
  ],
164
178
  min: 1
165
179
  },
180
+ {
181
+ type: (prev) => (prev?.length > 1 ? 'select' : null),
182
+ name: 'defaultTheme',
183
+ message: 'Default theme style',
184
+ choices: (prev) => prev.map((t) => ({ title: t.charAt(0).toUpperCase() + t.slice(1), value: t }))
185
+ },
166
186
  {
167
187
  type: 'select',
168
188
  name: 'switcher',
@@ -269,7 +289,7 @@ export async function init() {
269
289
  writeUnoConfig(cwd)
270
290
  writeAppCss(cwd, generateAppCssImports(config.themes))
271
291
 
272
- const initScript = generateInitScript(config.switcher, config.storageKey)
292
+ const initScript = generateInitScript(config.switcher, config.storageKey, config.defaultTheme)
273
293
  if (initScript) writeAppHtml(cwd, initScript, config.storageKey)
274
294
 
275
295
  console.info('\nDone! Run `rokkit doctor` to verify your setup.')
package/src/skin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-console */
2
- import { writeFileSync, existsSync } from 'fs'
2
+ import { writeFileSync } from 'fs'
3
3
  import { resolve } from 'path'
4
+ import { loadConfig } from './config.js'
4
5
 
5
6
  const SKIN_TOKEN_DEFAULTS = {
6
7
  primary: 'orange',
@@ -52,10 +53,7 @@ export function serializeConfig(config) {
52
53
  * cwd?: string
53
54
  * }} adapters
54
55
  */
55
- export async function runSkinList(adapters = {}) {
56
- const config = await loadConfig(adapters)
57
- if (!config) return
58
- const skins = config.skins ?? {}
56
+ function printSkinNames(skins) {
59
57
  const names = Object.keys(skins)
60
58
  if (names.length === 0) {
61
59
  console.info('No skins defined in rokkit.config.js.')
@@ -71,6 +69,15 @@ export async function runSkinList(adapters = {}) {
71
69
  }
72
70
  }
73
71
 
72
+ export async function runSkinList(adapters = {}) {
73
+ const config = await loadConfig(adapters)
74
+ if (!config) {
75
+ console.error('rokkit.config.js not found. Run `rokkit init` first.')
76
+ return
77
+ }
78
+ printSkinNames(config.skins ?? {})
79
+ }
80
+
74
81
  /**
75
82
  * Create a new skin entry in rokkit.config.js.
76
83
  * @param {string} name
@@ -80,22 +87,27 @@ export async function runSkinList(adapters = {}) {
80
87
  * cwd?: string
81
88
  * }} adapters
82
89
  */
83
- export async function runSkinCreate(name, adapters = {}) {
84
- if (!name) {
85
- console.error('Usage: rokkit skin create --name <name>')
86
- return
87
- }
90
+ function validateSkinCreate(name, config) {
91
+ if (!name) return 'Usage: rokkit skin create --name <name>'
92
+ if (!config) return 'rokkit.config.js not found. Run `rokkit init` first.'
93
+ if (config.skins?.[name]) return `Skin "${name}" already exists in rokkit.config.js.`
94
+ return null
95
+ }
88
96
 
97
+ export async function runSkinCreate(name, adapters = {}) {
89
98
  const config = await loadConfig(adapters)
90
- if (!config) return
91
-
92
- if (config.skins?.[name]) {
93
- console.warn(`Skin "${name}" already exists in rokkit.config.js.`)
99
+ const error = validateSkinCreate(name, config)
100
+ if (error) {
101
+ if (error.includes('already exists')) {
102
+ console.warn(error)
103
+ } else {
104
+ console.error(error)
105
+ }
94
106
  return
95
107
  }
96
108
 
97
109
  const updated = addSkinToConfig(config, name)
98
- await saveConfig(updated, adapters)
110
+ saveConfig(updated, adapters)
99
111
 
100
112
  console.info(`Added skin "${name}" to rokkit.config.js`)
101
113
  console.info(
@@ -104,31 +116,12 @@ export async function runSkinCreate(name, adapters = {}) {
104
116
  )
105
117
  }
106
118
 
107
- /**
108
- * Load rokkit.config.js using injected adapter or real filesystem.
109
- * @param {{ readConfig?: () => Record<string, unknown>, cwd?: string }} adapters
110
- * @returns {Promise<Record<string, unknown> | null>}
111
- */
112
- async function loadConfig(adapters) {
113
- if (adapters.readConfig) return adapters.readConfig()
114
-
115
- const cwd = adapters.cwd ?? process.cwd()
116
- const configPath = resolve(cwd, 'rokkit.config.js')
117
- if (!existsSync(configPath)) {
118
- console.error('rokkit.config.js not found. Run `rokkit init` first.')
119
- return null
120
- }
121
-
122
- const { default: config } = await import(`file://${configPath}?t=${Date.now()}`)
123
- return config
124
- }
125
-
126
119
  /**
127
120
  * Write updated config using injected adapter or real filesystem.
128
121
  * @param {Record<string, unknown>} config
129
122
  * @param {{ writeConfig?: (config: Record<string, unknown>) => void, cwd?: string }} adapters
130
123
  */
131
- async function saveConfig(config, adapters) {
124
+ function saveConfig(config, adapters) {
132
125
  if (adapters.writeConfig) {
133
126
  adapters.writeConfig(config)
134
127
  return
package/src/theme.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import { writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
3
3
  import { resolve, join } from 'path'
4
+ import { loadConfig } from './config.js'
4
5
 
5
6
  /**
6
7
  * All component names that ship with Rokkit — one CSS block will be generated for each.
@@ -62,33 +63,38 @@ export function generateThemeStub(name) {
62
63
  * cwd?: string
63
64
  * }} adapters
64
65
  */
65
- export async function runThemeList(adapters = {}) {
66
- const config = await loadConfig(adapters)
67
- const builtIn = config?.themes ?? []
68
-
66
+ function getCustomThemes(adapters) {
69
67
  const cwd = adapters.cwd ?? process.cwd()
70
68
  const themesDir = resolve(cwd, 'src/themes')
71
69
  const listFiles = adapters.listFiles ?? ((dir) => (existsSync(dir) ? readdirSync(dir) : []))
72
- const custom = listFiles(themesDir)
70
+ return listFiles(themesDir)
73
71
  .filter((f) => f.endsWith('.css'))
74
72
  .map((f) => f.replace(/\.css$/, ''))
73
+ }
75
74
 
76
- console.info('Rokkit Themes\n')
75
+ function printThemeSection(label, items) {
76
+ if (items.length === 0) return
77
+ console.info(label)
78
+ for (const t of items) console.info(` ${t}`)
79
+ }
77
80
 
78
- if (builtIn.length > 0) {
79
- console.info(' Built-in (from @rokkit/themes):')
80
- for (const t of builtIn) console.info(` ${t}`)
81
- }
82
- if (custom.length > 0) {
83
- console.info('\n Custom (src/themes/):')
84
- for (const t of custom) console.info(` ${t}`)
85
- }
81
+ function printThemeList(builtIn, custom) {
82
+ console.info('Rokkit Themes\n')
83
+ printThemeSection(' Built-in (from @rokkit/themes):', builtIn)
84
+ printThemeSection('\n Custom (src/themes/):', custom)
86
85
  if (builtIn.length === 0 && custom.length === 0) {
87
86
  console.info(' No themes found.')
88
87
  console.info(' Run `rokkit theme create --name <name>` to scaffold a custom theme.')
89
88
  }
90
89
  }
91
90
 
91
+ export async function runThemeList(adapters = {}) {
92
+ const config = await loadConfig(adapters)
93
+ const builtIn = config?.themes ?? []
94
+ const custom = getCustomThemes(adapters)
95
+ printThemeList(builtIn, custom)
96
+ }
97
+
92
98
  /**
93
99
  * Create a new custom theme CSS stub at src/themes/<name>.css.
94
100
  * @param {string} name
@@ -99,17 +105,22 @@ export async function runThemeList(adapters = {}) {
99
105
  * cwd?: string
100
106
  * }} adapters
101
107
  */
102
- export async function runThemeCreate(name, adapters = {}) {
108
+ function resolveThemeAdapters(adapters) {
109
+ return {
110
+ cwd: adapters.cwd ?? process.cwd(),
111
+ writeFile: adapters.writeFile ?? writeFileSync,
112
+ exists: adapters.exists ?? existsSync,
113
+ mkdir: adapters.mkdir ?? ((dir) => mkdirSync(dir, { recursive: true }))
114
+ }
115
+ }
116
+
117
+ export function runThemeCreate(name, adapters = {}) {
103
118
  if (!name) {
104
119
  console.error('Usage: rokkit theme create --name <name>')
105
120
  return
106
121
  }
107
122
 
108
- const cwd = adapters.cwd ?? process.cwd()
109
- const writeFile = adapters.writeFile ?? writeFileSync
110
- const exists = adapters.exists ?? existsSync
111
- const mkdir = adapters.mkdir ?? ((dir) => mkdirSync(dir, { recursive: true }))
112
-
123
+ const { cwd, writeFile, exists, mkdir } = resolveThemeAdapters(adapters)
113
124
  const themesDir = resolve(cwd, 'src/themes')
114
125
  const filePath = join(themesDir, `${name}.css`)
115
126
 
@@ -128,26 +139,6 @@ export async function runThemeCreate(name, adapters = {}) {
128
139
  )
129
140
  }
130
141
 
131
- /**
132
- * Load rokkit.config.js using injected adapter or real filesystem.
133
- * Returns null (not an error) if config is absent — theme list can still show custom files.
134
- * @param {{ readConfig?: () => Record<string, unknown>, cwd?: string }} adapters
135
- */
136
- async function loadConfig(adapters) {
137
- if (adapters.readConfig) return adapters.readConfig()
138
-
139
- const cwd = adapters.cwd ?? process.cwd()
140
- const configPath = resolve(cwd, 'rokkit.config.js')
141
- if (!existsSync(configPath)) return null
142
-
143
- try {
144
- const { default: config } = await import(`file://${configPath}?t=${Date.now()}`)
145
- return config
146
- } catch {
147
- return null
148
- }
149
- }
150
-
151
142
  /**
152
143
  * CLI entry point for `rokkit theme` subcommands.
153
144
  * @param {string} sub — 'list' | 'create'
@@ -155,7 +146,7 @@ async function loadConfig(adapters) {
155
146
  */
156
147
  export async function theme(sub, opts = {}) {
157
148
  if (sub === 'create') {
158
- await runThemeCreate(opts.name)
149
+ runThemeCreate(opts.name)
159
150
  } else {
160
151
  await runThemeList()
161
152
  }
package/src/upgrade.js CHANGED
@@ -51,69 +51,92 @@ export function buildInstallCommand(pm, packages) {
51
51
  * runInstall?: (bin: string, args: string[]) => void
52
52
  * }} adapters — injectable for testing
53
53
  */
54
- export function runUpgrade(opts = {}, adapters = {}) {
55
- const cwd = process.cwd()
56
- const readFile = adapters.readFile ?? ((p) => readFileSync(p, 'utf-8'))
57
- const exists = adapters.exists ?? ((p) => existsSync(p))
58
- const fetchVersion =
59
- adapters.fetchVersion ??
60
- ((name) => {
61
- try {
62
- return execFileSync('npm', ['view', name, 'version'], { encoding: 'utf-8' }).trim()
63
- } catch {
64
- return null
65
- }
66
- })
67
- const runInstall =
68
- adapters.runInstall ??
69
- ((bin, args) => execFileSync(bin, args, { stdio: 'inherit' }))
54
+ function resolveUpgradeAdapters(adapters) {
55
+ return {
56
+ readFile: adapters.readFile ?? ((p) => readFileSync(p, 'utf-8')),
57
+ exists: adapters.exists ?? ((p) => existsSync(p)),
58
+ fetchVersion:
59
+ adapters.fetchVersion ??
60
+ ((name) => {
61
+ try {
62
+ return execFileSync('npm', ['view', name, 'version'], { encoding: 'utf-8' }).trim()
63
+ } catch {
64
+ return null
65
+ }
66
+ }),
67
+ runInstall: adapters.runInstall ?? ((bin, args) => execFileSync(bin, args, { stdio: 'inherit' }))
68
+ }
69
+ }
70
+
71
+ function formatPackageStatus(pkg, latest) {
72
+ if (!latest) return `OK ${pkg.name} ${pkg.current}`
73
+ if (latest === pkg.current) return `OK ${pkg.name} ${pkg.current}`
74
+ return `OUT ${pkg.name} ${pkg.current} → ${latest}`
75
+ }
76
+
77
+ function checkPackageVersion(pkg, fetchVersion, upgrades) {
78
+ const latest = fetchVersion(pkg.name)
79
+ console.info(` ${formatPackageStatus(pkg, latest)}`)
80
+ if (latest && latest !== pkg.current) upgrades.push(`${pkg.name}@${latest}`)
81
+ }
82
+
83
+ function checkUpgrades(packages, fetchVersion) {
84
+ const upgrades = []
85
+ for (const pkg of packages) {
86
+ checkPackageVersion(pkg, fetchVersion, upgrades)
87
+ }
88
+ return upgrades
89
+ }
90
+
91
+ function applyUpgrades(upgrades, pm, apply, runInstall) {
92
+ const { bin, args } = buildInstallCommand(pm, upgrades)
93
+ const cmdStr = `${bin} ${args.join(' ')}`
94
+ if (apply) {
95
+ console.info(`\nRunning: ${cmdStr}\n`)
96
+ runInstall(bin, args)
97
+ console.info('\nUpgrade complete.')
98
+ } else {
99
+ console.info(`\n${upgrades.length} package(s) can be upgraded.`)
100
+ console.info(`Run with --apply to install, or manually:\n ${cmdStr}`)
101
+ }
102
+ }
70
103
 
104
+ function loadPackages(cwd, readFile, exists) {
71
105
  const pkgPath = resolve(cwd, 'package.json')
72
106
  if (!exists(pkgPath)) {
73
107
  console.error('No package.json found in current directory.')
74
- return
108
+ return null
75
109
  }
76
-
77
- const pkgJson = JSON.parse(readFile(pkgPath))
78
- const packages = getRokkitPackages(pkgJson)
79
-
110
+ const packages = getRokkitPackages(JSON.parse(readFile(pkgPath)))
80
111
  if (packages.length === 0) {
81
112
  console.info('No @rokkit/* packages found in package.json.')
82
- return
113
+ return null
83
114
  }
115
+ return packages
116
+ }
84
117
 
118
+ function detectPm(cwd, exists) {
85
119
  const LOCKFILES = ['bun.lock', 'bun.lockb', 'pnpm-lock.yaml', 'yarn.lock', 'package-lock.json']
86
- const presentLockfiles = LOCKFILES.filter((f) => exists(resolve(cwd, f)))
87
- const pm = detectPackageManager(presentLockfiles)
120
+ return detectPackageManager(LOCKFILES.filter((f) => exists(resolve(cwd, f))))
121
+ }
88
122
 
89
- console.info('Rokkit Upgrade\n')
123
+ export function runUpgrade(opts = {}, adapters = {}) {
124
+ const cwd = process.cwd()
125
+ const { readFile, exists, fetchVersion, runInstall } = resolveUpgradeAdapters(adapters)
90
126
 
91
- const upgrades = []
92
- for (const pkg of packages) {
93
- const latest = fetchVersion(pkg.name)
94
- const upToDate = latest && latest === pkg.current
95
- const status = !latest ? '?' : upToDate ? 'up-to-date' : 'outdated'
96
- const arrow = latest ? `${pkg.current} → ${latest}` : pkg.current
97
- console.info(` ${status === 'outdated' ? 'OUT' : 'OK '} ${pkg.name} ${arrow}`)
98
- if (!upToDate && latest) upgrades.push(`${pkg.name}@${latest}`)
99
- }
127
+ const packages = loadPackages(cwd, readFile, exists)
128
+ if (!packages) return
100
129
 
130
+ const pm = detectPm(cwd, exists)
131
+ console.info('Rokkit Upgrade\n')
132
+
133
+ const upgrades = checkUpgrades(packages, fetchVersion)
101
134
  if (upgrades.length === 0) {
102
135
  console.info('\nAll packages are up to date.')
103
136
  return
104
137
  }
105
138
 
106
- const { bin, args } = buildInstallCommand(pm, upgrades)
107
- const cmdStr = `${bin} ${args.join(' ')}`
108
-
109
- if (opts.apply) {
110
- console.info(`\nRunning: ${cmdStr}\n`)
111
- runInstall(bin, args)
112
- console.info('\nUpgrade complete.')
113
- } else {
114
- console.info(`\n${upgrades.length} package(s) can be upgraded.`)
115
- console.info(`Run with --apply to install, or manually:\n ${cmdStr}`)
116
- }
139
+ applyUpgrades(upgrades, pm, opts.apply, runInstall)
117
140
  }
118
141
 
119
142
  /**