@nuasite/cli 0.16.0 → 0.17.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.
@@ -0,0 +1,2 @@
1
+ export declare function findAstroConfig(cwd?: string): string | null;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAUA,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAM1E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuasite/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": [
@@ -23,7 +23,7 @@
23
23
  "prepack": "bun run ../../scripts/workspace-deps/resolve-deps.ts"
24
24
  },
25
25
  "dependencies": {
26
- "@nuasite/agent-summary": "0.16.0",
26
+ "@nuasite/agent-summary": "0.17.0",
27
27
  "astro": "^6.0.2",
28
28
  "stacktracey": "2.1.8"
29
29
  },
package/src/clean.ts ADDED
@@ -0,0 +1,402 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+ import { findAstroConfig } from './utils'
4
+
5
+ export interface CleanOptions {
6
+ cwd?: string
7
+ dryRun?: boolean
8
+ yes?: boolean
9
+ }
10
+
11
+ export type FeatureKey = 'cms' | 'pageMarkdown' | 'mdx' | 'sitemap' | 'tailwindcss' | 'checks'
12
+ const FEATURE_KEYS: FeatureKey[] = ['cms', 'pageMarkdown', 'mdx', 'sitemap', 'tailwindcss', 'checks']
13
+
14
+ /** Tooling / orchestration packages — removed during clean */
15
+ const NUASITE_TOOLING = [
16
+ '@nuasite/nua',
17
+ '@nuasite/core',
18
+ '@nuasite/cli',
19
+ '@nuasite/cms',
20
+ '@nuasite/llm-enhancements',
21
+ '@nuasite/checks',
22
+ '@nuasite/agent-summary',
23
+ ]
24
+
25
+ const PACKAGES_TO_ADD: Record<string, string> = {
26
+ 'astro': '^6.0.2',
27
+ '@astrojs/check': '^0.9.7',
28
+ '@astrojs/mdx': '^5.0.0',
29
+ '@astrojs/rss': '^4.0.17',
30
+ '@astrojs/sitemap': '^3.7.1',
31
+ '@tailwindcss/vite': '^4.2.1',
32
+ 'tailwindcss': '^4.2.1',
33
+ 'typescript': '^5',
34
+ }
35
+
36
+ export function detectDisabledFeatures(content: string): Set<FeatureKey> {
37
+ const disabled = new Set<FeatureKey>()
38
+ for (const key of FEATURE_KEYS) {
39
+ if (new RegExp(`\\b${key}\\s*:\\s*false\\b`).test(content)) {
40
+ disabled.add(key)
41
+ }
42
+ }
43
+ return disabled
44
+ }
45
+
46
+ /**
47
+ * Find the matching closing brace/bracket/paren, aware of string literals.
48
+ */
49
+ function findMatchingClose(text: string, start: number): number {
50
+ const open = text[start]!
51
+ const close = open === '{' ? '}' : open === '[' ? ']' : ')'
52
+ let depth = 0
53
+ let inString: string | false = false
54
+
55
+ for (let i = start; i < text.length; i++) {
56
+ const ch = text[i]
57
+
58
+ if (inString) {
59
+ if (ch === '\\') {
60
+ i++
61
+ continue
62
+ }
63
+ if (ch === inString) inString = false
64
+ continue
65
+ }
66
+
67
+ if (ch === "'" || ch === '"' || ch === '`') {
68
+ inString = ch
69
+ continue
70
+ }
71
+
72
+ if (ch === open) depth++
73
+ if (ch === close) {
74
+ depth--
75
+ if (depth === 0) return i
76
+ }
77
+ }
78
+
79
+ return -1
80
+ }
81
+
82
+ /**
83
+ * Extract the text between the outermost { } of defineConfig({ ... })
84
+ */
85
+ export function extractConfigBody(content: string): string {
86
+ const match = content.match(/defineConfig\s*\(\s*\{/)
87
+ if (!match || match.index === undefined) return ''
88
+
89
+ const openBrace = content.indexOf('{', match.index + 'defineConfig'.length)
90
+ const closeBrace = findMatchingClose(content, openBrace)
91
+ if (closeBrace === -1) return ''
92
+
93
+ return content.slice(openBrace + 1, closeBrace)
94
+ }
95
+
96
+ /**
97
+ * Remove a top-level property from an object literal body text.
98
+ */
99
+ export function removeProperty(body: string, propName: string): string {
100
+ const regex = new RegExp(`(\\n[ \\t]*)${propName}\\s*:\\s*`)
101
+ const match = regex.exec(body)
102
+ if (!match || match.index === undefined) return body
103
+
104
+ const propLineStart = match.index + 1 // skip the leading \n
105
+ const afterMatch = match.index + match[0].length
106
+
107
+ // Skip whitespace (not newlines) before the value
108
+ let i = afterMatch
109
+ while (i < body.length && (body[i] === ' ' || body[i] === '\t')) i++
110
+
111
+ let valueEnd: number
112
+
113
+ if (body[i] === '{' || body[i] === '[') {
114
+ valueEnd = findMatchingClose(body, i)
115
+ if (valueEnd === -1) return body
116
+ valueEnd++ // include closing brace/bracket
117
+ } else {
118
+ // Simple value (false, true, number, string, variable)
119
+ while (i < body.length && body[i] !== ',' && body[i] !== '\n') i++
120
+ valueEnd = i
121
+ }
122
+
123
+ // Skip trailing comma and whitespace up to newline
124
+ let end = valueEnd
125
+ while (end < body.length && (body[end] === ' ' || body[end] === '\t')) end++
126
+ if (end < body.length && body[end] === ',') end++
127
+ while (end < body.length && (body[end] === ' ' || body[end] === '\t')) end++
128
+ if (end < body.length && body[end] === '\n') end++
129
+
130
+ return body.slice(0, propLineStart) + body.slice(end)
131
+ }
132
+
133
+ /**
134
+ * Prepend items into an existing array property (e.g. `integrations: [` or `plugins: [`).
135
+ * Mutates `lines` in place. Returns true if a merge happened.
136
+ */
137
+ function prependToArrayProperty(lines: string[], property: string, items: string): boolean {
138
+ const pattern = new RegExp(`\\b${property}\\s*:\\s*\\[`)
139
+ for (let i = 0; i < lines.length; i++) {
140
+ if (pattern.test(lines[i]!)) {
141
+ lines[i] = lines[i]!.replace(
142
+ new RegExp(`(\\b${property}\\s*:\\s*\\[)`),
143
+ `$1${items}, `,
144
+ )
145
+ return true
146
+ }
147
+ }
148
+ return false
149
+ }
150
+
151
+ export function transformConfig(content: string, disabled: Set<FeatureKey>): string {
152
+ const userImports = content
153
+ .split('\n')
154
+ .filter(line => /^\s*import\s/.test(line))
155
+ .filter(line => !line.includes('@nuasite/'))
156
+ .filter(line => !line.includes('defineConfig'))
157
+
158
+ let body = extractConfigBody(content)
159
+ body = removeProperty(body, 'nua')
160
+
161
+ if (content.includes('@nuasite/nua/integration')) {
162
+ body = body.replace(/\bnua\s*\([^)]*\)\s*,?\s*/g, '')
163
+ body = body.replace(/\bintegrations\s*:\s*\[\s*,?\s*\]\s*,?/g, '')
164
+ }
165
+
166
+ const imports = [`import { defineConfig } from 'astro/config'`]
167
+ if (!disabled.has('tailwindcss')) imports.push(`import tailwindcss from '@tailwindcss/vite'`)
168
+ if (!disabled.has('mdx')) imports.push(`import mdx from '@astrojs/mdx'`)
169
+ if (!disabled.has('sitemap')) imports.push(`import sitemap from '@astrojs/sitemap'`)
170
+ imports.push(...userImports)
171
+
172
+ const integrationCalls: string[] = []
173
+ if (!disabled.has('mdx')) integrationCalls.push('mdx()')
174
+ if (!disabled.has('sitemap')) integrationCalls.push('sitemap()')
175
+
176
+ const bodyLines = body.split('\n').filter(line => line.trim() !== '')
177
+
178
+ const hasIntegrations = bodyLines.some(line => /^\s*integrations\s*:/.test(line))
179
+ const hasVite = bodyLines.some(line => /^\s*vite\s*:/.test(line))
180
+
181
+ if (hasIntegrations && integrationCalls.length > 0) {
182
+ prependToArrayProperty(bodyLines, 'integrations', integrationCalls.join(', '))
183
+ }
184
+
185
+ if (!disabled.has('tailwindcss') && hasVite) {
186
+ prependToArrayProperty(bodyLines, 'plugins', 'tailwindcss()')
187
+ }
188
+
189
+ const newPropLines: string[] = []
190
+
191
+ if (!disabled.has('tailwindcss') && !hasVite) {
192
+ newPropLines.push(
193
+ '\tvite: {',
194
+ '\t\tbuild: {',
195
+ '\t\t\tsourcemap: true,',
196
+ '\t\t},',
197
+ '\t\tplugins: [tailwindcss()],',
198
+ '\t},',
199
+ )
200
+ }
201
+
202
+ if (!hasIntegrations && integrationCalls.length > 0) {
203
+ newPropLines.push(`\tintegrations: [${integrationCalls.join(', ')}],`)
204
+ }
205
+
206
+ const allLines = [...bodyLines, ...newPropLines]
207
+
208
+ let result = imports.join('\n') + '\n\n'
209
+ result += 'export default defineConfig({\n'
210
+ if (allLines.length > 0) {
211
+ result += allLines.join('\n') + '\n'
212
+ }
213
+ result += '})\n'
214
+
215
+ return result
216
+ }
217
+
218
+ export function transformPackageJson(
219
+ pkg: Record<string, any>,
220
+ disabled: Set<FeatureKey>,
221
+ usedRuntimePackages: string[] = [],
222
+ ): Record<string, any> {
223
+ const result = structuredClone(pkg)
224
+
225
+ const nuaVersion: string | undefined = result.dependencies?.['@nuasite/nua']
226
+ ?? result.devDependencies?.['@nuasite/nua']
227
+
228
+ if (result.scripts) {
229
+ for (const [key, value] of Object.entries(result.scripts)) {
230
+ if (typeof value === 'string') {
231
+ result.scripts[key] = value
232
+ .replace(/\bnua build\b/g, 'astro build')
233
+ .replace(/\bnua dev\b/g, 'astro dev')
234
+ .replace(/\bnua preview\b/g, 'astro preview')
235
+ }
236
+ }
237
+ }
238
+
239
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies'] as const) {
240
+ if (!result[field]) continue
241
+ for (const name of NUASITE_TOOLING) {
242
+ delete result[field][name]
243
+ }
244
+ if (Object.keys(result[field]).length === 0) {
245
+ delete result[field]
246
+ }
247
+ }
248
+
249
+ if (!result.dependencies) result.dependencies = {}
250
+
251
+ for (const [name, version] of Object.entries(PACKAGES_TO_ADD)) {
252
+ if (result.dependencies[name]) continue
253
+ if (name === '@astrojs/mdx' && disabled.has('mdx')) continue
254
+ if (name === '@astrojs/sitemap' && disabled.has('sitemap')) continue
255
+ if (name === '@tailwindcss/vite' && disabled.has('tailwindcss')) continue
256
+ if (name === 'tailwindcss' && disabled.has('tailwindcss')) continue
257
+ result.dependencies[name] = version
258
+ }
259
+
260
+ // Promote runtime packages (e.g. @nuasite/components) — version mirrors @nuasite/nua
261
+ for (const name of usedRuntimePackages) {
262
+ if (!result.dependencies[name]) {
263
+ result.dependencies[name] = nuaVersion ?? '^0.16.0'
264
+ }
265
+ }
266
+
267
+ result.dependencies = Object.fromEntries(
268
+ Object.entries(result.dependencies).sort(([a], [b]) => a.localeCompare(b)),
269
+ )
270
+
271
+ return result
272
+ }
273
+
274
+ function scanForNuasiteUsage(cwd: string): Array<{ file: string; packages: string[] }> {
275
+ const srcDir = join(cwd, 'src')
276
+ if (!existsSync(srcDir)) return []
277
+
278
+ const results: Array<{ file: string; packages: string[] }> = []
279
+
280
+ try {
281
+ const files = readdirSync(srcDir, { recursive: true })
282
+ for (const entry of files) {
283
+ const fileName = String(entry)
284
+ if (!/\.(astro|ts|tsx|js|jsx)$/.test(fileName)) continue
285
+
286
+ try {
287
+ const content = readFileSync(join(srcDir, fileName), 'utf-8')
288
+ const matches = content.match(/@nuasite\/[\w-]+/g)
289
+ if (matches) {
290
+ results.push({
291
+ file: join('src', fileName),
292
+ packages: [...new Set(matches)],
293
+ })
294
+ }
295
+ } catch {
296
+ // skip unreadable files
297
+ }
298
+ }
299
+ } catch {
300
+ // src directory not readable
301
+ }
302
+
303
+ return results
304
+ }
305
+
306
+ export async function clean({ cwd = process.cwd(), dryRun = false, yes = false }: CleanOptions = {}) {
307
+ const configPath = findAstroConfig(cwd)
308
+ if (!configPath) {
309
+ console.error('No Astro config file found.')
310
+ process.exit(1)
311
+ }
312
+
313
+ const configContent = readFileSync(configPath, 'utf-8')
314
+ if (!configContent.includes('@nuasite/nua')) {
315
+ console.log('This project does not use @nuasite/nua. Nothing to clean.')
316
+ return
317
+ }
318
+
319
+ const pkgPath = join(cwd, 'package.json')
320
+ if (!existsSync(pkgPath)) {
321
+ console.error('No package.json found.')
322
+ process.exit(1)
323
+ }
324
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
325
+
326
+ const disabled = detectDisabledFeatures(configContent)
327
+ const nuasiteUsage = scanForNuasiteUsage(cwd)
328
+ const configName = basename(configPath)
329
+
330
+ const usedRuntimePackages = new Set<string>()
331
+ const toolingUsage: Array<{ file: string; packages: string[] }> = []
332
+
333
+ for (const entry of nuasiteUsage) {
334
+ const runtime = entry.packages.filter(p => !NUASITE_TOOLING.includes(p))
335
+ const tooling = entry.packages.filter(p => NUASITE_TOOLING.includes(p))
336
+ for (const p of runtime) usedRuntimePackages.add(p)
337
+ if (tooling.length > 0) {
338
+ toolingUsage.push({ file: entry.file, packages: tooling })
339
+ }
340
+ }
341
+
342
+ console.log('')
343
+ console.log('nua clean — eject to standard Astro project')
344
+ console.log('')
345
+ console.log(` ${configName}`)
346
+ console.log(' - Replace @nuasite/nua with explicit Astro integrations')
347
+ console.log(' - Add mdx, sitemap, tailwindcss imports')
348
+ if (disabled.size > 0) {
349
+ console.log(` - Skipping disabled: ${[...disabled].join(', ')}`)
350
+ }
351
+ console.log('')
352
+ console.log(' package.json')
353
+ console.log(' - Remove @nuasite/* tooling dependencies')
354
+ if (usedRuntimePackages.size > 0) {
355
+ console.log(` - Keep as explicit deps: ${[...usedRuntimePackages].join(', ')}`)
356
+ }
357
+ console.log(' - Add standard Astro packages')
358
+ console.log(' - Update scripts: nua → astro')
359
+
360
+ if (toolingUsage.length > 0) {
361
+ console.log('')
362
+ console.log(' Warning: @nuasite tooling imports found in source files:')
363
+ for (const { file, packages } of toolingUsage) {
364
+ console.log(` ${file} (${packages.join(', ')})`)
365
+ }
366
+ console.log(' These will need manual removal.')
367
+ }
368
+
369
+ if (dryRun) {
370
+ console.log('')
371
+ console.log(' (--dry-run: no changes made)')
372
+ console.log('')
373
+ return
374
+ }
375
+
376
+ if (!yes) {
377
+ console.log('')
378
+ const answer = prompt('Proceed? [y/N] ')
379
+ if (answer?.toLowerCase() !== 'y') {
380
+ console.log('Cancelled.')
381
+ return
382
+ }
383
+ }
384
+
385
+ const newConfig = transformConfig(configContent, disabled)
386
+ writeFileSync(configPath, newConfig)
387
+ console.log(` Updated ${configName}`)
388
+
389
+ const newPkg = transformPackageJson(pkg, disabled, [...usedRuntimePackages])
390
+ writeFileSync(pkgPath, JSON.stringify(newPkg, null, '\t') + '\n')
391
+ console.log(' Updated package.json')
392
+
393
+ console.log('')
394
+ console.log('Next steps:')
395
+ console.log(' 1. bun install')
396
+ console.log(' 2. Review the updated config')
397
+ console.log(' 3. astro dev')
398
+ if (toolingUsage.length > 0) {
399
+ console.log(' 4. Remove @nuasite tooling imports from source files')
400
+ }
401
+ console.log('')
402
+ }
package/src/index.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  import { agentsSummary } from '@nuasite/agent-summary'
3
3
  import { type AstroInlineConfig, build as astroBuild, dev, preview } from 'astro'
4
4
  import { spawn } from 'node:child_process'
5
- import { existsSync, readFileSync } from 'node:fs'
6
- import { join } from 'node:path'
5
+ import { readFileSync } from 'node:fs'
6
+ import { findAstroConfig } from './utils'
7
7
 
8
8
  const [, , command, ...args] = process.argv
9
9
 
@@ -16,23 +16,6 @@ function hasNuaIntegration(configPath: string): boolean {
16
16
  }
17
17
  }
18
18
 
19
- function findAstroConfig(): string | null {
20
- const possibleConfigs = [
21
- 'astro.config.mjs',
22
- 'astro.config.js',
23
- 'astro.config.ts',
24
- 'astro.config.mts',
25
- ]
26
-
27
- for (const config of possibleConfigs) {
28
- const configPath = join(process.cwd(), config)
29
- if (existsSync(configPath)) {
30
- return configPath
31
- }
32
- }
33
- return null
34
- }
35
-
36
19
  function proxyToAstroCLI(command: string, args: string[]) {
37
20
  const astro = spawn('npx', ['astro', command, ...args], {
38
21
  stdio: 'inherit',
@@ -52,10 +35,11 @@ function proxyToAstroCLI(command: string, args: string[]) {
52
35
  function printUsage() {
53
36
  console.log('Usage: nua <command> [options]')
54
37
  console.log('\nCommands:')
55
- console.log(' build Run astro build with the Nua defaults')
56
- console.log(' preview Run astro preview with the Nua defaults')
57
- console.log(' dev Run astro dev with the Nua defaults')
58
- console.log(' help Show this message')
38
+ console.log(' build Run astro build with the Nua defaults')
39
+ console.log(' dev Run astro dev with the Nua defaults')
40
+ console.log(' preview Run astro preview with the Nua defaults')
41
+ console.log(' clean Eject to a standard Astro project (remove @nuasite/* deps)')
42
+ console.log(' help Show this message')
59
43
  console.log('\nAll Astro CLI options are supported.\n')
60
44
  }
61
45
 
@@ -102,6 +86,15 @@ if (canProxyDirectly && command && ['build', 'dev', 'preview'].includes(command)
102
86
  })
103
87
  break
104
88
  }
89
+ case 'clean': {
90
+ const { clean } = await import('./clean')
91
+ await clean({
92
+ cwd: process.cwd(),
93
+ dryRun: args.includes('--dry-run'),
94
+ yes: args.includes('--yes') || args.includes('-y'),
95
+ })
96
+ break
97
+ }
105
98
  case 'help':
106
99
  case '--help':
107
100
  case '-h':
package/src/utils.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ const CONFIG_NAMES = [
5
+ 'astro.config.ts',
6
+ 'astro.config.mts',
7
+ 'astro.config.mjs',
8
+ 'astro.config.js',
9
+ ]
10
+
11
+ export function findAstroConfig(cwd: string = process.cwd()): string | null {
12
+ for (const name of CONFIG_NAMES) {
13
+ const p = join(cwd, name)
14
+ if (existsSync(p)) return p
15
+ }
16
+ return null
17
+ }