@rokkit/cli 1.0.0-next.144 → 1.0.0-next.146

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,51 @@
1
+ /**
2
+ * Generate a skin scaffold object with all token keys set to default color names.
3
+ * @returns {Record<string, string>}
4
+ */
5
+ export function generateSkinScaffold(): Record<string, string>;
6
+ /**
7
+ * Add a new skin entry to a config object (pure, no side effects).
8
+ * @param {Record<string, unknown>} config
9
+ * @param {string} name
10
+ * @returns {Record<string, unknown>}
11
+ */
12
+ export function addSkinToConfig(config: Record<string, unknown>, name: string): Record<string, unknown>;
13
+ /**
14
+ * Serialize a rokkit config back to a JS module string.
15
+ * @param {Record<string, unknown>} config
16
+ * @returns {string}
17
+ */
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>;
44
+ /**
45
+ * CLI entry point for `rokkit skin` subcommands.
46
+ * @param {string} sub — 'list' | 'create'
47
+ * @param {{ name?: string }} opts
48
+ */
49
+ export function skin(sub: string, opts?: {
50
+ name?: string;
51
+ }): Promise<void>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Generate a full CSS theme stub for the given style name.
3
+ * Produces one `[data-style='<name>']` block per component.
4
+ * @param {string} name
5
+ * @returns {string}
6
+ */
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>;
37
+ /**
38
+ * CLI entry point for `rokkit theme` subcommands.
39
+ * @param {string} sub — 'list' | 'create'
40
+ * @param {{ name?: string }} opts
41
+ */
42
+ export function theme(sub: string, opts?: {
43
+ name?: string;
44
+ }): Promise<void>;
45
+ /**
46
+ * All component names that ship with Rokkit — one CSS block will be generated for each.
47
+ */
48
+ export const THEME_COMPONENTS: string[];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Extract @rokkit/* packages from a parsed package.json object.
3
+ * @param {Record<string, unknown>} pkgJson
4
+ * @returns {{ name: string, current: string }[]}
5
+ */
6
+ export function getRokkitPackages(pkgJson: Record<string, unknown>): {
7
+ name: string;
8
+ current: string;
9
+ }[];
10
+ /**
11
+ * Detect the package manager from lockfile presence.
12
+ * @param {string[]} lockfiles — list of lockfile names that exist in cwd
13
+ * @returns {'bun' | 'pnpm' | 'yarn' | 'npm'}
14
+ */
15
+ export function detectPackageManager(lockfiles: string[]): "bun" | "pnpm" | "yarn" | "npm";
16
+ /**
17
+ * Build the install command for the given package manager.
18
+ * @param {'bun' | 'pnpm' | 'yarn' | 'npm'} pm
19
+ * @param {string[]} packages — package specs like `@rokkit/ui@1.2.3`
20
+ * @returns {{ bin: string, args: string[] }}
21
+ */
22
+ export function buildInstallCommand(pm: "bun" | "pnpm" | "yarn" | "npm", packages: string[]): {
23
+ bin: string;
24
+ args: string[];
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;
45
+ /**
46
+ * CLI entry point for `rokkit upgrade`
47
+ * @param {{ apply?: boolean }} opts
48
+ */
49
+ export function upgrade(opts?: {
50
+ apply?: boolean;
51
+ }): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/cli",
3
- "version": "1.0.0-next.144",
3
+ "version": "1.0.0-next.146",
4
4
  "description": "Command line utilities for icon bundling and conversion.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/doctor.js CHANGED
@@ -150,8 +150,14 @@ function applyPatchHtml(cwd, label) {
150
150
 
151
151
  /** @type {Record<string, (cwd: string, label: string) => boolean|void>} */
152
152
  const FIX_HANDLERS = {
153
- 'generate-config': (cwd, label) => { applyGenerateConfig(cwd, label); return true },
154
- 'patch-css': (cwd, label) => { applyPatchCss(cwd, label); return true },
153
+ 'generate-config': (cwd, label) => {
154
+ applyGenerateConfig(cwd, label)
155
+ return true
156
+ },
157
+ 'patch-css': (cwd, label) => {
158
+ applyPatchCss(cwd, label)
159
+ return true
160
+ },
155
161
  'patch-html': (cwd, label) => applyPatchHtml(cwd, label)
156
162
  }
157
163
 
package/src/index.js CHANGED
@@ -70,4 +70,47 @@ prog
70
70
  await doctor(opts)
71
71
  })
72
72
 
73
+ prog
74
+ .command('upgrade')
75
+ .describe('Check for @rokkit/* package updates and optionally install them')
76
+ .option('--apply', 'Install available upgrades')
77
+ .action(async (opts) => {
78
+ const { upgrade } = await import('./upgrade.js')
79
+ upgrade(opts)
80
+ })
81
+
82
+ prog
83
+ .command('skin list')
84
+ .describe('List all skins defined in rokkit.config.js')
85
+ .action(async () => {
86
+ const { skin } = await import('./skin.js')
87
+ await skin('list')
88
+ })
89
+
90
+ prog
91
+ .command('skin create')
92
+ .describe('Scaffold a new skin entry in rokkit.config.js')
93
+ .option('--name', 'Skin name')
94
+ .action(async (opts) => {
95
+ const { skin } = await import('./skin.js')
96
+ await skin('create', opts)
97
+ })
98
+
99
+ prog
100
+ .command('theme list')
101
+ .describe('List built-in and custom themes')
102
+ .action(async () => {
103
+ const { theme } = await import('./theme.js')
104
+ await theme('list')
105
+ })
106
+
107
+ prog
108
+ .command('theme create')
109
+ .describe('Scaffold a new custom theme CSS file')
110
+ .option('--name', 'Theme name')
111
+ .action(async (opts) => {
112
+ const { theme } = await import('./theme.js')
113
+ await theme('create', opts)
114
+ })
115
+
73
116
  prog.parse(process.argv)
package/src/skin.js ADDED
@@ -0,0 +1,151 @@
1
+ /* eslint-disable no-console */
2
+ import { writeFileSync, existsSync } from 'fs'
3
+ import { resolve } from 'path'
4
+
5
+ const SKIN_TOKEN_DEFAULTS = {
6
+ primary: 'orange',
7
+ secondary: 'pink',
8
+ accent: 'sky',
9
+ surface: 'slate',
10
+ success: 'green',
11
+ warning: 'yellow',
12
+ danger: 'red',
13
+ error: 'red',
14
+ info: 'cyan'
15
+ }
16
+
17
+ /**
18
+ * Generate a skin scaffold object with all token keys set to default color names.
19
+ * @returns {Record<string, string>}
20
+ */
21
+ export function generateSkinScaffold() {
22
+ return { ...SKIN_TOKEN_DEFAULTS }
23
+ }
24
+
25
+ /**
26
+ * Add a new skin entry to a config object (pure, no side effects).
27
+ * @param {Record<string, unknown>} config
28
+ * @param {string} name
29
+ * @returns {Record<string, unknown>}
30
+ */
31
+ export function addSkinToConfig(config, name) {
32
+ const skins = config.skins ?? {}
33
+ return {
34
+ ...config,
35
+ skins: { ...skins, [name]: generateSkinScaffold() }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Serialize a rokkit config back to a JS module string.
41
+ * @param {Record<string, unknown>} config
42
+ * @returns {string}
43
+ */
44
+ export function serializeConfig(config) {
45
+ return `export default ${JSON.stringify(config, null, 2)}\n`
46
+ }
47
+
48
+ /**
49
+ * List all skins defined in rokkit.config.js.
50
+ * @param {{
51
+ * readConfig?: () => Record<string, unknown>,
52
+ * cwd?: string
53
+ * }} adapters
54
+ */
55
+ export async function runSkinList(adapters = {}) {
56
+ const config = await loadConfig(adapters)
57
+ if (!config) return
58
+ const skins = config.skins ?? {}
59
+ const names = Object.keys(skins)
60
+ if (names.length === 0) {
61
+ console.info('No skins defined in rokkit.config.js.')
62
+ console.info('Run `rokkit skin create --name <name>` to add one.')
63
+ return
64
+ }
65
+ console.info('Defined skins:\n')
66
+ for (const name of names) {
67
+ const tokens = Object.entries(skins[name])
68
+ .map(([k, v]) => `${k}: ${v}`)
69
+ .join(', ')
70
+ console.info(` ${name} — ${tokens}`)
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Create a new skin entry in rokkit.config.js.
76
+ * @param {string} name
77
+ * @param {{
78
+ * readConfig?: () => Record<string, unknown>,
79
+ * writeConfig?: (config: Record<string, unknown>) => void,
80
+ * cwd?: string
81
+ * }} adapters
82
+ */
83
+ export async function runSkinCreate(name, adapters = {}) {
84
+ if (!name) {
85
+ console.error('Usage: rokkit skin create --name <name>')
86
+ return
87
+ }
88
+
89
+ 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.`)
94
+ return
95
+ }
96
+
97
+ const updated = addSkinToConfig(config, name)
98
+ await saveConfig(updated, adapters)
99
+
100
+ console.info(`Added skin "${name}" to rokkit.config.js`)
101
+ console.info(
102
+ `\nCustomize the color tokens in rokkit.config.js → skins.${name}\n` +
103
+ `Then apply with: <body data-skin="${name}">`
104
+ )
105
+ }
106
+
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
+ /**
127
+ * Write updated config using injected adapter or real filesystem.
128
+ * @param {Record<string, unknown>} config
129
+ * @param {{ writeConfig?: (config: Record<string, unknown>) => void, cwd?: string }} adapters
130
+ */
131
+ async function saveConfig(config, adapters) {
132
+ if (adapters.writeConfig) {
133
+ adapters.writeConfig(config)
134
+ return
135
+ }
136
+ const cwd = adapters.cwd ?? process.cwd()
137
+ writeFileSync(resolve(cwd, 'rokkit.config.js'), serializeConfig(config))
138
+ }
139
+
140
+ /**
141
+ * CLI entry point for `rokkit skin` subcommands.
142
+ * @param {string} sub — 'list' | 'create'
143
+ * @param {{ name?: string }} opts
144
+ */
145
+ export async function skin(sub, opts = {}) {
146
+ if (sub === 'create') {
147
+ await runSkinCreate(opts.name)
148
+ } else {
149
+ await runSkinList()
150
+ }
151
+ }
package/src/theme.js ADDED
@@ -0,0 +1,162 @@
1
+ /* eslint-disable no-console */
2
+ import { writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
3
+ import { resolve, join } from 'path'
4
+
5
+ /**
6
+ * All component names that ship with Rokkit — one CSS block will be generated for each.
7
+ */
8
+ export const THEME_COMPONENTS = [
9
+ 'button',
10
+ 'card',
11
+ 'connector',
12
+ 'dropdown',
13
+ 'floating-action',
14
+ 'floating-navigation',
15
+ 'grid',
16
+ 'input',
17
+ 'list',
18
+ 'menu',
19
+ 'message',
20
+ 'range',
21
+ 'search-filter',
22
+ 'select',
23
+ 'status-list',
24
+ 'switch',
25
+ 'table',
26
+ 'tabs',
27
+ 'timeline',
28
+ 'toc',
29
+ 'toggle',
30
+ 'toolbar',
31
+ 'tree',
32
+ 'upload-progress',
33
+ 'upload-target'
34
+ ]
35
+
36
+ /**
37
+ * Generate a full CSS theme stub for the given style name.
38
+ * Produces one `[data-style='<name>']` block per component.
39
+ * @param {string} name
40
+ * @returns {string}
41
+ */
42
+ export function generateThemeStub(name) {
43
+ const header = `/* Custom theme: ${name} */
44
+ /* Apply this theme with: <body data-style="${name}"> */
45
+ /* Customize each component block below using CSS custom properties and UnoCSS utilities. */
46
+ /* Reference: https://rokkit.dev/docs/theming */
47
+
48
+ `
49
+ const blocks = THEME_COMPONENTS.map((component) => {
50
+ const selector = `[data-style='${name}'] [data-${component}]`
51
+ return `${selector} {\n /* ${component} styles */\n}\n`
52
+ })
53
+
54
+ return header + blocks.join('\n')
55
+ }
56
+
57
+ /**
58
+ * List all theme styles defined in rokkit.config.js plus any custom CSS files in src/themes/.
59
+ * @param {{
60
+ * readConfig?: () => Record<string, unknown>,
61
+ * listFiles?: (dir: string) => string[],
62
+ * cwd?: string
63
+ * }} adapters
64
+ */
65
+ export async function runThemeList(adapters = {}) {
66
+ const config = await loadConfig(adapters)
67
+ const builtIn = config?.themes ?? []
68
+
69
+ const cwd = adapters.cwd ?? process.cwd()
70
+ const themesDir = resolve(cwd, 'src/themes')
71
+ const listFiles = adapters.listFiles ?? ((dir) => (existsSync(dir) ? readdirSync(dir) : []))
72
+ const custom = listFiles(themesDir)
73
+ .filter((f) => f.endsWith('.css'))
74
+ .map((f) => f.replace(/\.css$/, ''))
75
+
76
+ console.info('Rokkit Themes\n')
77
+
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
+ }
86
+ if (builtIn.length === 0 && custom.length === 0) {
87
+ console.info(' No themes found.')
88
+ console.info(' Run `rokkit theme create --name <name>` to scaffold a custom theme.')
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Create a new custom theme CSS stub at src/themes/<name>.css.
94
+ * @param {string} name
95
+ * @param {{
96
+ * writeFile?: (path: string, content: string) => void,
97
+ * exists?: (path: string) => boolean,
98
+ * mkdir?: (dir: string) => void,
99
+ * cwd?: string
100
+ * }} adapters
101
+ */
102
+ export async function runThemeCreate(name, adapters = {}) {
103
+ if (!name) {
104
+ console.error('Usage: rokkit theme create --name <name>')
105
+ return
106
+ }
107
+
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
+
113
+ const themesDir = resolve(cwd, 'src/themes')
114
+ const filePath = join(themesDir, `${name}.css`)
115
+
116
+ if (exists(filePath)) {
117
+ console.warn(`Theme file already exists: src/themes/${name}.css`)
118
+ return
119
+ }
120
+
121
+ if (!exists(themesDir)) mkdir(themesDir)
122
+
123
+ writeFile(filePath, generateThemeStub(name))
124
+ console.info(`Created src/themes/${name}.css`)
125
+ console.info(
126
+ `\nImport it in src/app.css:\n @import './themes/${name}.css';\n` +
127
+ `\nApply it: <body data-style="${name}">`
128
+ )
129
+ }
130
+
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
+ /**
152
+ * CLI entry point for `rokkit theme` subcommands.
153
+ * @param {string} sub — 'list' | 'create'
154
+ * @param {{ name?: string }} opts
155
+ */
156
+ export async function theme(sub, opts = {}) {
157
+ if (sub === 'create') {
158
+ await runThemeCreate(opts.name)
159
+ } else {
160
+ await runThemeList()
161
+ }
162
+ }
package/src/upgrade.js ADDED
@@ -0,0 +1,125 @@
1
+ /* eslint-disable no-console */
2
+ import { readFileSync, existsSync } from 'fs'
3
+ import { resolve } from 'path'
4
+ import { execFileSync } from 'child_process'
5
+
6
+ /**
7
+ * Extract @rokkit/* packages from a parsed package.json object.
8
+ * @param {Record<string, unknown>} pkgJson
9
+ * @returns {{ name: string, current: string }[]}
10
+ */
11
+ export function getRokkitPackages(pkgJson) {
12
+ const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) }
13
+ return Object.entries(deps)
14
+ .filter(([name]) => name.startsWith('@rokkit/'))
15
+ .map(([name, version]) => ({ name, current: String(version).replace(/^[\^~>=<]+/, '') }))
16
+ }
17
+
18
+ /**
19
+ * Detect the package manager from lockfile presence.
20
+ * @param {string[]} lockfiles — list of lockfile names that exist in cwd
21
+ * @returns {'bun' | 'pnpm' | 'yarn' | 'npm'}
22
+ */
23
+ export function detectPackageManager(lockfiles) {
24
+ if (lockfiles.includes('bun.lock') || lockfiles.includes('bun.lockb')) return 'bun'
25
+ if (lockfiles.includes('pnpm-lock.yaml')) return 'pnpm'
26
+ if (lockfiles.includes('yarn.lock')) return 'yarn'
27
+ return 'npm'
28
+ }
29
+
30
+ /**
31
+ * Build the install command for the given package manager.
32
+ * @param {'bun' | 'pnpm' | 'yarn' | 'npm'} pm
33
+ * @param {string[]} packages — package specs like `@rokkit/ui@1.2.3`
34
+ * @returns {{ bin: string, args: string[] }}
35
+ */
36
+ export function buildInstallCommand(pm, packages) {
37
+ if (pm === 'bun') return { bin: 'bun', args: ['add', ...packages] }
38
+ if (pm === 'pnpm') return { bin: 'pnpm', args: ['add', ...packages] }
39
+ if (pm === 'yarn') return { bin: 'yarn', args: ['add', ...packages] }
40
+ return { bin: 'npm', args: ['install', ...packages] }
41
+ }
42
+
43
+ /**
44
+ * Run upgrade check and optionally apply it.
45
+ *
46
+ * @param {{ apply?: boolean }} opts
47
+ * @param {{
48
+ * readFile?: (p: string) => string,
49
+ * exists?: (p: string) => boolean,
50
+ * fetchVersion?: (name: string) => string | null,
51
+ * runInstall?: (bin: string, args: string[]) => void
52
+ * }} adapters — injectable for testing
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' }))
70
+
71
+ const pkgPath = resolve(cwd, 'package.json')
72
+ if (!exists(pkgPath)) {
73
+ console.error('No package.json found in current directory.')
74
+ return
75
+ }
76
+
77
+ const pkgJson = JSON.parse(readFile(pkgPath))
78
+ const packages = getRokkitPackages(pkgJson)
79
+
80
+ if (packages.length === 0) {
81
+ console.info('No @rokkit/* packages found in package.json.')
82
+ return
83
+ }
84
+
85
+ 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)
88
+
89
+ console.info('Rokkit Upgrade\n')
90
+
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
+ }
100
+
101
+ if (upgrades.length === 0) {
102
+ console.info('\nAll packages are up to date.')
103
+ return
104
+ }
105
+
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
+ }
117
+ }
118
+
119
+ /**
120
+ * CLI entry point for `rokkit upgrade`
121
+ * @param {{ apply?: boolean }} opts
122
+ */
123
+ export function upgrade(opts = {}) {
124
+ runUpgrade(opts)
125
+ }