uniweb 0.8.3 → 0.8.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,9 +41,9 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/runtime": "0.6.4",
45
- "@uniweb/build": "0.8.3",
44
+ "@uniweb/build": "0.8.4",
46
45
  "@uniweb/core": "0.5.4",
46
+ "@uniweb/runtime": "0.6.4",
47
47
  "@uniweb/kit": "0.7.3"
48
48
  }
49
49
  }
@@ -25,6 +25,7 @@ import {
25
25
  import { validatePackageName, getExistingPackageNames, resolveUniqueName } from '../utils/names.js'
26
26
  import { findWorkspaceRoot } from '../utils/workspace.js'
27
27
  import { detectPackageManager, filterCmd, installCmd } from '../utils/pm.js'
28
+ import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from '../utils/interactive.js'
28
29
  import { resolveTemplate } from '../templates/index.js'
29
30
  import { validateTemplate } from '../templates/validator.js'
30
31
  import { getVersionsForTemplates } from '../versions.js'
@@ -91,13 +92,17 @@ function parseArgs(args) {
91
92
  /**
92
93
  * Main add command handler
93
94
  */
94
- export async function add(args) {
95
+ export async function add(rawArgs) {
96
+ const nonInteractive = isNonInteractive(rawArgs)
97
+ const args = stripNonInteractiveFlag(rawArgs)
98
+
95
99
  if (args[0] === '--help' || args[0] === '-h') {
96
100
  showAddHelp()
97
101
  return
98
102
  }
99
103
 
100
104
  const pm = detectPackageManager()
105
+ const prefix = getCliPrefix()
101
106
 
102
107
  // Find workspace root
103
108
  const rootDir = findWorkspaceRoot()
@@ -110,6 +115,18 @@ export async function add(args) {
110
115
  // Interactive subcommand chooser when no args given
111
116
  let parsed
112
117
  if (!args.length || (args[0] && args[0].startsWith('--'))) {
118
+ if (nonInteractive) {
119
+ error(`Missing subcommand.\n`)
120
+ log(formatOptions([
121
+ { label: 'foundation', description: 'Component library' },
122
+ { label: 'site', description: 'Content site' },
123
+ { label: 'extension', description: 'Additional component package' },
124
+ ]))
125
+ log('')
126
+ log(`Usage: ${prefix} add <foundation|site|extension> [name]`)
127
+ process.exit(1)
128
+ }
129
+
113
130
  const response = await prompts({
114
131
  type: 'select',
115
132
  name: 'subcommand',
@@ -171,6 +188,12 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
171
188
 
172
189
  // Interactive name prompt when name not provided and no --path
173
190
  if (!name && !opts.path) {
191
+ if (isNonInteractive(process.argv)) {
192
+ error(`Missing foundation name.\n`)
193
+ log(`Usage: ${getCliPrefix()} add foundation <name>`)
194
+ process.exit(1)
195
+ }
196
+
174
197
  const foundations = await discoverFoundations(rootDir)
175
198
  const hasDefault = foundations.length === 0 && !existsSync(join(rootDir, 'foundation'))
176
199
  const response = await prompts({
@@ -251,6 +274,12 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
251
274
 
252
275
  // Interactive name prompt when name not provided and no --path
253
276
  if (!name && !opts.path) {
277
+ if (isNonInteractive(process.argv)) {
278
+ error(`Missing site name.\n`)
279
+ log(`Usage: ${getCliPrefix()} add site <name>`)
280
+ process.exit(1)
281
+ }
282
+
254
283
  const existingSites = await discoverSites(rootDir)
255
284
  const hasDefault = existingSites.length === 0 && !existsSync(join(rootDir, 'site'))
256
285
  const response = await prompts({
@@ -371,6 +400,12 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
371
400
 
372
401
  // Interactive name prompt when name not provided
373
402
  if (!name) {
403
+ if (isNonInteractive(process.argv)) {
404
+ error(`Missing extension name.\n`)
405
+ log(`Usage: ${getCliPrefix()} add extension <name>`)
406
+ process.exit(1)
407
+ }
408
+
374
409
  const response = await prompts({
375
410
  type: 'text',
376
411
  name: 'name',
@@ -529,7 +564,18 @@ async function resolveFoundation(rootDir, foundationFlag) {
529
564
  return foundations[0]
530
565
  }
531
566
 
532
- // Multiple foundations — prompt
567
+ // Multiple foundations — prompt (or fail in non-interactive mode)
568
+ if (isNonInteractive(process.argv)) {
569
+ error(`Multiple foundations found. Specify which to use:\n`)
570
+ log(formatOptions(foundations.map(f => ({
571
+ label: f.name,
572
+ description: f.path,
573
+ }))))
574
+ log('')
575
+ log(`Usage: ${getCliPrefix()} add site <name> --foundation <name>`)
576
+ process.exit(1)
577
+ }
578
+
533
579
  const response = await prompts({
534
580
  type: 'select',
535
581
  name: 'foundation',
@@ -29,6 +29,7 @@ import {
29
29
  findFoundations,
30
30
  promptSelect,
31
31
  } from '../utils/workspace.js'
32
+ import { isNonInteractive, getCliPrefix, formatOptions } from '../utils/interactive.js'
32
33
 
33
34
  // Colors for terminal output
34
35
  const colors = {
@@ -394,6 +395,12 @@ async function generateComponentDocs(args) {
394
395
  if (foundations.length === 1) {
395
396
  targetFoundation = foundations[0]
396
397
  info(`Found foundation: ${targetFoundation}`)
398
+ } else if (isNonInteractive(process.argv)) {
399
+ error(`Multiple foundations found. Specify which to target:\n`)
400
+ log(formatOptions(foundations.map(f => ({ label: f, description: '' }))))
401
+ log('')
402
+ log(`Usage: ${getCliPrefix()} docs --target <path>`)
403
+ process.exit(1)
397
404
  } else {
398
405
  log(`${colors.dim}Multiple foundations found in workspace.${colors.reset}\n`)
399
406
  targetFoundation = await promptSelect('Select foundation:', foundations)
package/src/index.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  import { validateTemplate } from './templates/validator.js'
30
30
  import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
31
31
  import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
32
+ import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
32
33
 
33
34
  // Colors for terminal output
34
35
  const colors = {
@@ -288,7 +289,9 @@ function computeFoundationFilePath(sitePath, foundationPath) {
288
289
  }
289
290
 
290
291
  async function main() {
291
- const args = process.argv.slice(2)
292
+ const rawArgs = process.argv.slice(2)
293
+ const nonInteractive = isNonInteractive(rawArgs)
294
+ const args = stripNonInteractiveFlag(rawArgs)
292
295
  const command = args[0]
293
296
  const pm = detectPackageManager()
294
297
 
@@ -369,6 +372,26 @@ async function main() {
369
372
  projectName = null
370
373
  }
371
374
 
375
+ const prefix = getCliPrefix()
376
+
377
+ // Non-interactive: fail with actionable message instead of prompting
378
+ if (nonInteractive && !projectName) {
379
+ error(`Missing project name.\n`)
380
+ log(`Usage: ${prefix} create <project-name> [--template <name>]`)
381
+ process.exit(1)
382
+ }
383
+
384
+ if (nonInteractive && !templateType) {
385
+ error(`Missing --template flag. Available templates:\n`)
386
+ log(formatOptions(TEMPLATE_CHOICES.map(c => ({
387
+ label: c.value,
388
+ description: c.description,
389
+ }))))
390
+ log('')
391
+ log(`Usage: ${prefix} create ${projectName || '<project-name>'} --template <name>`)
392
+ process.exit(1)
393
+ }
394
+
372
395
  // Interactive prompts
373
396
  const response = await prompts([
374
397
  {
@@ -539,6 +562,10 @@ ${colors.bright}Add Subcommands:${colors.reset}
539
562
  add site [name] Add a site (--from, --foundation, --path, --project)
540
563
  add extension <name> Add an extension (--from, --site, --path)
541
564
 
565
+ ${colors.bright}Global Options:${colors.reset}
566
+ --non-interactive Fail with usage info instead of prompting
567
+ Auto-detected when CI=true or no TTY (pipes, agents)
568
+
542
569
  ${colors.bright}Build Options:${colors.reset}
543
570
  --target <type> Build target (foundation, site) - auto-detected if not specified
544
571
  --prerender Force pre-rendering (overrides site.yml)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Non-Interactive Mode Detection
3
+ *
4
+ * Detects when the CLI is running without a TTY (AI agents, CI, piped input)
5
+ * and provides helpers for printing actionable error messages.
6
+ */
7
+
8
+ /**
9
+ * Detect if the CLI is running in non-interactive mode.
10
+ * True when --non-interactive flag, CI env var, or no TTY.
11
+ * @param {string[]} args - Command line arguments
12
+ * @returns {boolean}
13
+ */
14
+ export function isNonInteractive(args) {
15
+ if (args.includes('--non-interactive')) return true
16
+ if (process.env.CI) return true
17
+ if (!process.stdin.isTTY) return true
18
+ return false
19
+ }
20
+
21
+ /**
22
+ * Get the CLI invocation prefix to use in suggested commands.
23
+ * Mirrors however the user actually ran the CLI.
24
+ * @returns {string}
25
+ */
26
+ export function getCliPrefix() {
27
+ const ua = process.env.npm_config_user_agent || ''
28
+ if (ua.startsWith('pnpm/')) return 'pnpm uniweb'
29
+ if (ua.startsWith('npm/')) return 'npx uniweb'
30
+ return 'uniweb'
31
+ }
32
+
33
+ /**
34
+ * Strip --non-interactive from an args array so it doesn't interfere
35
+ * with positional argument parsing.
36
+ * @param {string[]} args
37
+ * @returns {string[]}
38
+ */
39
+ export function stripNonInteractiveFlag(args) {
40
+ return args.filter(a => a !== '--non-interactive')
41
+ }
42
+
43
+ /**
44
+ * Format a list of options with aligned descriptions for terminal output.
45
+ * @param {{ label: string, description: string }[]} options
46
+ * @returns {string}
47
+ */
48
+ export function formatOptions(options) {
49
+ const maxLen = Math.max(...options.map(o => o.label.length))
50
+ return options
51
+ .map(o => ` ${o.label.padEnd(maxLen + 3)}${o.description}`)
52
+ .join('\n')
53
+ }
@@ -1 +1,99 @@
1
- primary: '#3b82f6'
1
+ # Theme Configuration
2
+ # Controls colors, typography, and visual style for the site.
3
+ # Only set what you want to change — everything has sensible defaults.
4
+
5
+ # ─── Colors ────────────────────────────────────────────────────────────────────
6
+ # Palette colors generate shades (50–950) used by semantic tokens.
7
+ # Values: any CSS color (hex, rgb, hsl, oklch).
8
+
9
+ colors:
10
+ primary: '#3b82f6' # Brand color — buttons, links, focus rings
11
+ # secondary: '#64748b' # Secondary actions
12
+ # accent: '#8b5cf6' # Highlights, decorative elements
13
+ # neutral: stone # Gray family — stone, zinc, gray, slate, neutral
14
+
15
+ # ─── Fonts ─────────────────────────────────────────────────────────────────────
16
+ # Font families for text, headings, and code blocks.
17
+ # Default: system-ui stack. Uncomment to use custom fonts.
18
+
19
+ # fonts:
20
+ # heading: '"Inter", system-ui, sans-serif'
21
+ # body: '"Inter", system-ui, sans-serif'
22
+ # mono: '"JetBrains Mono", ui-monospace, monospace'
23
+ # import:
24
+ # - url: https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap
25
+ # - url: https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap
26
+
27
+ # ─── Page Background ──────────────────────────────────────────────────────────
28
+ # Background applied to the page body. Any CSS background value.
29
+
30
+ # background: '#fafaf9'
31
+ # background: 'linear-gradient(to bottom, #fafaf9, white)'
32
+
33
+ # ─── Appearance ────────────────────────────────────────────────────────────────
34
+ # Color scheme support. Simple value: light, dark, or system.
35
+ # Or use the expanded form for more control.
36
+
37
+ # appearance: light
38
+ # appearance:
39
+ # default: light
40
+ # schemes: [light, dark]
41
+ # allowToggle: true
42
+ # respectSystemPreference: true
43
+
44
+ # ─── Context Overrides ─────────────────────────────────────────────────────────
45
+ # Sections declare a context (light, medium, dark) in frontmatter.
46
+ # Each context maps semantic tokens to palette values.
47
+ # Override individual tokens here — unlisted tokens keep their defaults.
48
+ #
49
+ # Available tokens:
50
+ # Surfaces: section, card, muted
51
+ # Text: body, heading, subtle
52
+ # Border: border, ring
53
+ # Links: link, link-hover
54
+ # Actions: primary, primary-foreground, primary-hover
55
+ # secondary, secondary-foreground, secondary-hover
56
+ # Status: success, warning, error, info (+ -subtle variants)
57
+
58
+ # contexts:
59
+ # light:
60
+ # section: white
61
+ # medium:
62
+ # section: 'var(--neutral-100)'
63
+ # dark:
64
+ # heading: white
65
+
66
+ # ─── Inline Text Styles ───────────────────────────────────────────────────────
67
+ # Named styles for inline text in markdown: [text]{emphasis}
68
+ # Each name maps to CSS properties.
69
+ # Defaults: emphasis (colored + bold), muted (subtle).
70
+
71
+ # inline:
72
+ # emphasis:
73
+ # color: 'var(--link)'
74
+ # font-weight: '600'
75
+ # muted:
76
+ # color: 'var(--subtle)'
77
+
78
+ # ─── Code Blocks ───────────────────────────────────────────────────────────────
79
+ # Syntax highlighting colors for code blocks (uses Shiki).
80
+
81
+ # code:
82
+ # background: '#1e1e2e'
83
+ # foreground: '#cdd6f4'
84
+ # keyword: '#cba6f7'
85
+ # string: '#a6e3a1'
86
+ # number: '#fab387'
87
+ # comment: '#6c7086'
88
+ # function: '#89b4fa'
89
+ # variable: '#f5e0dc'
90
+ # type: '#f9e2af'
91
+ # property: '#94e2d5'
92
+ # tag: '#89b4fa'
93
+
94
+ # ─── Foundation Variables ──────────────────────────────────────────────────────
95
+ # Override variables declared by the foundation (e.g., header-height, max-width).
96
+ # Available variables depend on the foundation — check its foundation.js.
97
+
98
+ # vars:
99
+ # header-height: 5rem