@archetypeai/ds-cli 0.3.17 → 0.3.18

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/README.md CHANGED
@@ -26,13 +26,19 @@ Sets up a SvelteKit + Tailwind v4 + shadcn-svelte project with design tokens, co
26
26
  |------|--------|---------|
27
27
  | `--framework` | `svelte` | prompt |
28
28
  | `--pm` | `npm`, `pnpm`, `bun`, `yarn` | prompt |
29
+ | `--fonts <path>` | path to PP Neue Montreal fonts folder | prompt |
30
+ | `--no-fonts` | skip font installation (use system fallback) | — |
29
31
  | `--no-components` | skip component install | install all |
30
32
  | `--codeagent` | `cursor`, `claude`, `none` | prompt |
33
+ | `--defaults` | force non-interactive mode | — |
31
34
 
32
35
  ```bash
33
- npx @archetypeai/ds-cli create my-app --framework svelte --pm pnpm --codeagent none
36
+ npx @archetypeai/ds-cli create my-app --pm pnpm --no-fonts --codeagent claude
37
+ npx @archetypeai/ds-cli create my-app --pm npm --fonts /path/to/fonts --codeagent claude
34
38
  ```
35
39
 
40
+ **Non-interactive mode** (no TTY or `--defaults`): all required flags must be specified. The CLI lists missing options and exits so agents can ask the user before proceeding.
41
+
36
42
  ---
37
43
 
38
44
  ### `init` → Add DS to an existing project
@@ -47,11 +53,15 @@ Run from a SvelteKit project root. Auto-detects your package manager, installs t
47
53
  | Flag | Values | Default |
48
54
  |------|--------|---------|
49
55
  | `--pm` | `npm`, `pnpm`, `bun`, `yarn` | auto-detect |
56
+ | `--fonts <path>` | path to PP Neue Montreal fonts folder | prompt |
57
+ | `--no-fonts` | skip font installation (use system fallback) | — |
50
58
  | `--no-components` | skip component install | install all |
51
59
  | `--codeagent` | `cursor`, `claude`, `none` | prompt |
60
+ | `--defaults` | force non-interactive mode | — |
52
61
 
53
62
  ```bash
54
- npx @archetypeai/ds-cli init --pm npm --codeagent none
63
+ npx @archetypeai/ds-cli init --pm npm --no-fonts --codeagent claude
64
+ npx @archetypeai/ds-cli init --pm npm --fonts /path/to/fonts --codeagent claude
55
65
  ```
56
66
 
57
67
  ---
package/bin.js CHANGED
@@ -28,19 +28,22 @@ Usage:
28
28
  Create flags:
29
29
  --framework <svelte> Framework to use (default: prompt)
30
30
  --pm <npm|pnpm|bun|yarn> Package manager (default: prompt)
31
- --fonts / --no-fonts Install internal fonts (default: prompt)
31
+ --fonts <path> Install fonts from local folder
32
+ --no-fonts Skip font installation
32
33
  --no-components Skip component installation (default: install all)
33
34
  --codeagent <cursor|claude|none> Agent configuration (default: prompt)
34
35
  --defaults Skip prompts, use sensible defaults
35
36
 
36
37
  Init flags:
37
38
  --pm <npm|pnpm|bun|yarn> Package manager (default: auto-detect)
38
- --fonts / --no-fonts Install internal fonts (default: prompt)
39
+ --fonts <path> Install fonts from local folder
40
+ --no-fonts Skip font installation
39
41
  --no-components Skip component installation (default: install all)
40
42
  --codeagent <cursor|claude|none> Agent configuration (default: prompt)
41
- --defaults Skip prompts, use sensible defaults
43
+ --defaults Force non-interactive mode
42
44
 
43
- Non-interactive shells (no TTY) automatically use defaults.
45
+ Non-interactive mode (no TTY or --defaults):
46
+ All required options must be specified via flags. Missing options are listed and the CLI exits.
44
47
 
45
48
  Add targets:
46
49
  ds-ui-svelte Install all design system components
@@ -14,7 +14,8 @@ import {
14
14
  createDemoPage,
15
15
  installAgentConfig
16
16
  } from '../lib/scaffold-ds-svelte-project.js';
17
- import { isInteractive, DEFAULTS, logDefaultsNotice } from '../lib/is-interactive.js';
17
+ import { validateFontsPath, installLocalFonts } from '../lib/install-fonts-local.js';
18
+ import { isInteractive, DEFAULTS } from '../lib/is-interactive.js';
18
19
 
19
20
  // parse flags
20
21
  export function parseFlags(args) {
@@ -22,6 +23,7 @@ export function parseFlags(args) {
22
23
  name: null,
23
24
  framework: null,
24
25
  pm: null,
26
+ fonts: null,
25
27
  components: true,
26
28
  agent: null,
27
29
  defaults: false
@@ -33,6 +35,10 @@ export function parseFlags(args) {
33
35
  flags.framework = args[++i];
34
36
  } else if (arg === '--pm' && args[i + 1]) {
35
37
  flags.pm = args[++i];
38
+ } else if (arg === '--fonts' && args[i + 1]) {
39
+ flags.fonts = args[++i];
40
+ } else if (arg === '--no-fonts') {
41
+ flags.fonts = false;
36
42
  } else if (arg === '--no-components') {
37
43
  flags.components = false;
38
44
  } else if (arg === '--codeagent' && args[i + 1]) {
@@ -52,6 +58,30 @@ export async function create(args) {
52
58
  const flags = parseFlags(args);
53
59
  const interactive = isInteractive(flags);
54
60
 
61
+ // preflight: in non-interactive mode, list all unspecified options and exit
62
+ if (!interactive) {
63
+ const missing = [];
64
+ if (!flags.name)
65
+ missing.push(' <project-name> Project name (positional argument)');
66
+ if (!flags.pm) missing.push(' --pm <npm (default)|pnpm|bun|yarn> Package manager');
67
+ if (flags.fonts === null)
68
+ missing.push(' --fonts <path> | --no-fonts Internal fonts (PP Neue Montreal)');
69
+ if (!flags.agent) missing.push(' --codeagent <cursor|claude|none> AI agent configuration');
70
+
71
+ if (missing.length > 0) {
72
+ console.log(
73
+ '[CONFIGURATION REQUIRED] Do not assume defaults. Ask the user to choose each option:\n\n' +
74
+ missing.join('\n') +
75
+ '\n\nOptional:\n' +
76
+ ' --no-components Skip component installation\n\n' +
77
+ 'Once the user has answered, re-run with their choices:\n' +
78
+ ' ds create my-app --pm <choice> --fonts <path> --codeagent <choice>\n' +
79
+ ' ds create my-app --pm <choice> --no-fonts --codeagent <choice>\n'
80
+ );
81
+ process.exit(1);
82
+ }
83
+ }
84
+
55
85
  p.intro('Create a new project using Archetype AI Design System.');
56
86
 
57
87
  // specify project name
@@ -95,44 +125,87 @@ export async function create(args) {
95
125
  process.exit(1);
96
126
  }
97
127
 
98
- // specify framework
99
- let framework = flags.framework;
100
- if (!framework) {
101
- if (!interactive) {
102
- framework = DEFAULTS.framework;
103
- } else {
104
- framework = await p.select({
105
- message: 'Which framework?',
106
- options: [{ value: 'svelte', label: 'SvelteKit', hint: 'Svelte 5 + Tailwind v4, default' }]
107
- });
108
- if (p.isCancel(framework)) {
109
- p.cancel('Setup cancelled.');
110
- process.exit(0);
111
- }
128
+ // specify framework (only svelte supported, default silently)
129
+ let framework = flags.framework || 'svelte';
130
+ if (!flags.framework && interactive) {
131
+ framework = await p.select({
132
+ message: 'Which framework?',
133
+ options: [{ value: 'svelte', label: 'SvelteKit', hint: 'Svelte 5 + Tailwind v4, default' }]
134
+ });
135
+ if (p.isCancel(framework)) {
136
+ p.cancel('Setup cancelled.');
137
+ process.exit(0);
112
138
  }
113
139
  }
114
140
 
115
141
  // specify package manager
116
142
  let pmName = flags.pm;
117
143
  if (!pmName) {
118
- if (!interactive) {
119
- pmName = DEFAULTS.pm;
144
+ pmName = await p.select({
145
+ message: 'Which package manager?',
146
+ options: pmNames.map((n) => ({
147
+ value: n,
148
+ label: n,
149
+ ...(n === DEFAULTS.pm && { hint: 'default' })
150
+ }))
151
+ });
152
+ if (p.isCancel(pmName)) {
153
+ p.cancel('Setup cancelled.');
154
+ process.exit(0);
155
+ }
156
+ }
157
+ const pm = getPm(pmName);
158
+
159
+ // specify fonts
160
+ let fontsPath = null;
161
+ let includeFonts = false;
162
+ if (flags.fonts === false) {
163
+ // --no-fonts: skip
164
+ } else if (typeof flags.fonts === 'string') {
165
+ fontsPath = flags.fonts;
166
+ const result = validateFontsPath(fontsPath);
167
+ if (!result.valid) {
168
+ if (!interactive) {
169
+ p.log.error(result.error);
170
+ process.exit(1);
171
+ }
172
+ p.log.warn(result.error);
173
+ p.log.warn('Continuing without fonts.');
174
+ fontsPath = null;
120
175
  } else {
121
- pmName = await p.select({
122
- message: 'Which package manager?',
123
- options: pmNames.map((n) => ({
124
- value: n,
125
- label: n,
126
- ...(n === DEFAULTS.pm && { hint: 'default' })
127
- }))
176
+ includeFonts = true;
177
+ }
178
+ } else {
179
+ const useFonts = await p.confirm({
180
+ message: 'Do you want to use internal fonts?',
181
+ initialValue: false
182
+ });
183
+ if (p.isCancel(useFonts)) {
184
+ p.cancel('Setup cancelled.');
185
+ process.exit(0);
186
+ }
187
+ if (useFonts) {
188
+ const fontInput = await p.text({
189
+ message: 'Drop your fonts folder here:',
190
+ placeholder: '/path/to/fonts'
128
191
  });
129
- if (p.isCancel(pmName)) {
192
+ if (p.isCancel(fontInput)) {
130
193
  p.cancel('Setup cancelled.');
131
194
  process.exit(0);
132
195
  }
196
+ if (fontInput && fontInput.trim()) {
197
+ fontsPath = fontInput.trim();
198
+ const result = validateFontsPath(fontsPath);
199
+ if (!result.valid) {
200
+ p.log.warn(result.error);
201
+ p.log.warn('Continuing without fonts.');
202
+ fontsPath = null;
203
+ } else {
204
+ includeFonts = true;
205
+ }
206
+ }
133
207
  }
134
208
  }
135
- const pm = getPm(pmName);
136
209
 
137
210
  // verify package manager is installed
138
211
  if (!isPmInstalled(pmName)) {
@@ -177,8 +250,22 @@ export async function create(args) {
177
250
  // init shadcn-svelte
178
251
  await initShadcn(pm, projectPath);
179
252
 
253
+ // install local fonts
254
+ if (includeFonts && fontsPath) {
255
+ const s = p.spinner();
256
+ s.start('Installing fonts');
257
+ const fontResult = installLocalFonts(fontsPath, projectPath);
258
+ if (fontResult.success) {
259
+ s.stop(`${fontResult.fileCount} font files installed`);
260
+ } else {
261
+ s.stop('Failed to install fonts');
262
+ p.log.warn(fontResult.error);
263
+ p.log.warn('Continuing without fonts.');
264
+ includeFonts = false;
265
+ }
266
+ }
267
+
180
268
  // configure CSS
181
- const includeFonts = false;
182
269
  configureCss(projectPath, includeFonts);
183
270
 
184
271
  // set up linting and formatting
@@ -202,33 +289,20 @@ export async function create(args) {
202
289
  // specify agent config
203
290
  let agent = flags.agent;
204
291
  if (!agent) {
205
- if (!interactive) {
206
- agent = DEFAULTS.agent;
207
- } else {
208
- agent = await p.select({
209
- message: 'Install AI agent configuration?',
210
- options: [
211
- { value: 'none', label: 'None' },
212
- { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
213
- { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
214
- ]
215
- });
216
- if (p.isCancel(agent)) {
217
- p.cancel('Setup cancelled.');
218
- process.exit(0);
219
- }
292
+ agent = await p.select({
293
+ message: 'Install AI agent configuration?',
294
+ options: [
295
+ { value: 'none', label: 'None' },
296
+ { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
297
+ { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
298
+ ]
299
+ });
300
+ if (p.isCancel(agent)) {
301
+ p.cancel('Setup cancelled.');
302
+ process.exit(0);
220
303
  }
221
304
  }
222
305
 
223
- // log defaults notice
224
- if (!interactive) {
225
- const applied = {};
226
- if (!flags.framework) applied.framework = framework;
227
- if (!flags.pm) applied.pm = pmName;
228
- if (!flags.agent) applied.codeagent = agent;
229
- logDefaultsNotice(applied);
230
- }
231
-
232
306
  const validAgents = ['cursor', 'claude', 'none'];
233
307
  if (!validAgents.includes(agent)) {
234
308
  p.log.error(`Invalid --codeagent value "${agent}". Must be one of: ${validAgents.join(', ')}`);
package/commands/init.js CHANGED
@@ -13,12 +13,14 @@ import {
13
13
  installComponents,
14
14
  installAgentConfig
15
15
  } from '../lib/scaffold-ds-svelte-project.js';
16
- import { isInteractive, DEFAULTS, logDefaultsNotice } from '../lib/is-interactive.js';
16
+ import { validateFontsPath, installLocalFonts } from '../lib/install-fonts-local.js';
17
+ import { isInteractive, DEFAULTS } from '../lib/is-interactive.js';
17
18
 
18
19
  // parse flags
19
20
  export function parseFlags(args) {
20
21
  const flags = {
21
22
  pm: null,
23
+ fonts: null,
22
24
  components: true,
23
25
  agent: null,
24
26
  defaults: false
@@ -28,6 +30,10 @@ export function parseFlags(args) {
28
30
  const arg = args[i];
29
31
  if (arg === '--pm' && args[i + 1]) {
30
32
  flags.pm = args[++i];
33
+ } else if (arg === '--fonts' && args[i + 1]) {
34
+ flags.fonts = args[++i];
35
+ } else if (arg === '--no-fonts') {
36
+ flags.fonts = false;
31
37
  } else if (arg === '--no-components') {
32
38
  flags.components = false;
33
39
  } else if (arg === '--codeagent' && args[i + 1]) {
@@ -57,6 +63,29 @@ export async function init(args) {
57
63
  const interactive = isInteractive(flags);
58
64
  const projectPath = process.cwd();
59
65
 
66
+ // preflight: in non-interactive mode, list all unspecified options and exit
67
+ if (!interactive) {
68
+ const missing = [];
69
+ if (!flags.pm && !detectPm(projectPath))
70
+ missing.push(' --pm <npm (default)|pnpm|bun|yarn> Package manager');
71
+ if (flags.fonts === null)
72
+ missing.push(' --fonts <path> | --no-fonts Internal fonts (PP Neue Montreal)');
73
+ if (!flags.agent) missing.push(' --codeagent <cursor|claude|none> AI agent configuration');
74
+
75
+ if (missing.length > 0) {
76
+ console.log(
77
+ '[CONFIGURATION REQUIRED] Do not assume defaults. Ask the user to choose each option:\n\n' +
78
+ missing.join('\n') +
79
+ '\n\nOptional:\n' +
80
+ ' --no-components Skip component installation\n\n' +
81
+ 'Once the user has answered, re-run with their choices:\n' +
82
+ ' ds init --pm <choice> --fonts <path> --codeagent <choice>\n' +
83
+ ' ds init --pm <choice> --no-fonts --codeagent <choice>\n'
84
+ );
85
+ process.exit(1);
86
+ }
87
+ }
88
+
60
89
  // validate --pm flag
61
90
  if (flags.pm && !pmNames.includes(flags.pm)) {
62
91
  p.log.error(`Invalid --pm value "${flags.pm}". Must be one of: ${pmNames.join(', ')}`);
@@ -91,32 +120,71 @@ export async function init(args) {
91
120
  }
92
121
  }
93
122
  if (!pmName) {
94
- if (!interactive) {
95
- pmName = DEFAULTS.pm;
123
+ pmName = await p.select({
124
+ message: 'Which package manager?',
125
+ options: pmNames.map((n) => ({
126
+ value: n,
127
+ label: n,
128
+ ...(n === DEFAULTS.pm && { hint: 'default' })
129
+ }))
130
+ });
131
+ if (p.isCancel(pmName)) {
132
+ p.cancel('Setup cancelled.');
133
+ process.exit(0);
134
+ }
135
+ }
136
+ const pm = getPm(pmName);
137
+
138
+ // specify fonts
139
+ let fontsPath = null;
140
+ let includeFonts = false;
141
+ if (flags.fonts === false) {
142
+ // --no-fonts: skip
143
+ } else if (typeof flags.fonts === 'string') {
144
+ fontsPath = flags.fonts;
145
+ const result = validateFontsPath(fontsPath);
146
+ if (!result.valid) {
147
+ if (!interactive) {
148
+ p.log.error(result.error);
149
+ process.exit(1);
150
+ }
151
+ p.log.warn(result.error);
152
+ p.log.warn('Continuing without fonts.');
153
+ fontsPath = null;
96
154
  } else {
97
- pmName = await p.select({
98
- message: 'Which package manager?',
99
- options: pmNames.map((n) => ({
100
- value: n,
101
- label: n,
102
- ...(n === DEFAULTS.pm && { hint: 'default' })
103
- }))
155
+ includeFonts = true;
156
+ }
157
+ } else {
158
+ const useFonts = await p.confirm({
159
+ message: 'Do you want to use internal fonts?',
160
+ initialValue: false
161
+ });
162
+ if (p.isCancel(useFonts)) {
163
+ p.cancel('Setup cancelled.');
164
+ process.exit(0);
165
+ }
166
+ if (useFonts) {
167
+ const fontInput = await p.text({
168
+ message: 'Drop your fonts folder here:',
169
+ placeholder: '/path/to/fonts'
104
170
  });
105
- if (p.isCancel(pmName)) {
171
+ if (p.isCancel(fontInput)) {
106
172
  p.cancel('Setup cancelled.');
107
173
  process.exit(0);
108
174
  }
175
+ if (fontInput && fontInput.trim()) {
176
+ fontsPath = fontInput.trim();
177
+ const result = validateFontsPath(fontsPath);
178
+ if (!result.valid) {
179
+ p.log.warn(result.error);
180
+ p.log.warn('Continuing without fonts.');
181
+ fontsPath = null;
182
+ } else {
183
+ includeFonts = true;
184
+ }
185
+ }
109
186
  }
110
187
  }
111
- const pm = getPm(pmName);
112
-
113
- // log defaults notice
114
- if (!interactive) {
115
- const applied = {};
116
- if (!flags.pm) applied.pm = pmName;
117
- if (!flags.agent) applied.codeagent = DEFAULTS.agent;
118
- logDefaultsNotice(applied);
119
- }
120
188
 
121
189
  // verify package manager is installed
122
190
  if (!isPmInstalled(pmName)) {
@@ -142,8 +210,22 @@ export async function init(args) {
142
210
  // init shadcn-svelte
143
211
  await initShadcn(pm, projectPath);
144
212
 
213
+ // install local fonts
214
+ if (includeFonts && fontsPath) {
215
+ const s = p.spinner();
216
+ s.start('Installing fonts');
217
+ const fontResult = installLocalFonts(fontsPath, projectPath);
218
+ if (fontResult.success) {
219
+ s.stop(`${fontResult.fileCount} font files installed`);
220
+ } else {
221
+ s.stop('Failed to install fonts');
222
+ p.log.warn(fontResult.error);
223
+ p.log.warn('Continuing without fonts.');
224
+ includeFonts = false;
225
+ }
226
+ }
227
+
145
228
  // configure CSS (prepend to existing)
146
- const includeFonts = false;
147
229
  prependCss(projectPath, includeFonts);
148
230
 
149
231
  // set up linting and formatting
@@ -167,21 +249,17 @@ export async function init(args) {
167
249
  // specify agent config
168
250
  let agent = flags.agent;
169
251
  if (!agent) {
170
- if (!interactive) {
171
- agent = DEFAULTS.agent;
172
- } else {
173
- agent = await p.select({
174
- message: 'Install AI agent configuration?',
175
- options: [
176
- { value: 'none', label: 'None' },
177
- { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
178
- { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
179
- ]
180
- });
181
- if (p.isCancel(agent)) {
182
- p.cancel('Setup cancelled.');
183
- process.exit(0);
184
- }
252
+ agent = await p.select({
253
+ message: 'Install AI agent configuration?',
254
+ options: [
255
+ { value: 'none', label: 'None' },
256
+ { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
257
+ { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
258
+ ]
259
+ });
260
+ if (p.isCancel(agent)) {
261
+ p.cancel('Setup cancelled.');
262
+ process.exit(0);
185
263
  }
186
264
  }
187
265
 
package/files/AGENTS.md CHANGED
@@ -49,7 +49,9 @@ Standard Tailwind is fine for:
49
49
  Before building a new component, fetch the pattern catalog from:
50
50
  `https://design-system.archetypeai.workers.dev/r/patterns.json`
51
51
 
52
- Each entry includes a `name`, `title`, `description`, and `registryDependencies`. Reuse or extend existing patterns instead of rebuilding from scratch. To install a pattern:
52
+ Each entry includes a `name`, `title`, `description`, and `registryDependencies`. Reuse or extend existing patterns instead of rebuilding from scratch.
53
+
54
+ Before installing, check if the pattern already exists locally in `$lib/components/ui/patterns/{name}/`. Only install if it is missing:
53
55
  `npx shadcn-svelte@latest add https://design-system.archetypeai.workers.dev/r/{name}.json`
54
56
 
55
57
  ## Skills
package/files/CLAUDE.md CHANGED
@@ -49,7 +49,9 @@ Standard Tailwind is fine for:
49
49
  Before building a new component, fetch the pattern catalog from:
50
50
  `https://design-system.archetypeai.workers.dev/r/patterns.json`
51
51
 
52
- Each entry includes a `name`, `title`, `description`, and `registryDependencies`. Reuse or extend existing patterns instead of rebuilding from scratch. To install a pattern:
52
+ Each entry includes a `name`, `title`, `description`, and `registryDependencies`. Reuse or extend existing patterns instead of rebuilding from scratch.
53
+
54
+ Before installing, check if the pattern already exists locally in `$lib/components/ui/patterns/{name}/`. Only install if it is missing:
53
55
  `npx shadcn-svelte@latest add https://design-system.archetypeai.workers.dev/r/{name}.json`
54
56
 
55
57
  ## Skills
@@ -31,6 +31,17 @@ The project includes pattern components that demonstrate these conventions. Stud
31
31
 
32
32
  Check if an existing pattern fits before building a new one. Use `@skills/build-pattern` to create new patterns that follow these same conventions.
33
33
 
34
+ ## Directory structure
35
+
36
+ ```
37
+ $lib/components/ui/
38
+ primitives/ — DS registry primitives (Card, Badge, Button, etc.)
39
+ patterns/ — DS registry patterns (BackgroundCard, FlatLogItem, etc.)
40
+ custom/ — Project-specific components not from the registry
41
+ ```
42
+
43
+ Place custom components (ones you create for this project that are not installed from the registry) in `$lib/components/ui/custom/`. Never put custom components directly in `ui/` next to `primitives/` and `patterns/`.
44
+
34
45
  ## Component conventions
35
46
 
36
47
  Follow `@rules/components` for the full conventions. The essentials:
@@ -90,6 +90,16 @@ In your global CSS file:
90
90
 
91
91
  Order matters - tokens must come before Tailwind.
92
92
 
93
+ ### Hide Scrollbars
94
+
95
+ Always add this rule to the root layout CSS (`src/app.css` or `src/routes/layout.css`) to hide visible scrollbars in ScrollArea components:
96
+
97
+ ```css
98
+ [data-slot='scroll-area-scrollbar'] {
99
+ display: none !important;
100
+ }
101
+ ```
102
+
93
103
  ## Tailwind v4 Specifics
94
104
 
95
105
  ### @theme Directive
@@ -33,7 +33,7 @@ Dashboards fill the entire viewport with no scrolling:
33
33
  Unless the user explicitly asks for a different layout:
34
34
 
35
35
  - **Equal-width columns** — use `grid-cols-2`, `grid-cols-3`, etc. Never mix fixed and fluid widths for card grids (e.g., don't use `grid-cols-[300px_1fr]` for cards — that's for sidebar layouts only)
36
- - **Equal-height cards** — CSS grid rows distribute height evenly. Add `max-h-full` to each Card so content stays constrained to its grid cell
36
+ - **Cards fill available height** — cards must stretch to fill their grid cell or flex container. Add `max-h-full` to constrain content. For asymmetric layouts (stacked cards in one column, full-height card in another), use a flex column with `flex-1` on the card that should grow
37
37
  - **Consistent spacing** — always use `gap-4` between cards and `p-4` padding around the grid
38
38
 
39
39
  ## Menubar
@@ -123,7 +123,7 @@ For co-branding, pass a `partnerLogo` snippet to replace the default placeholder
123
123
  </main>
124
124
  ```
125
125
 
126
- ### Grid of Cards
126
+ ### Grid of Cards (Equal)
127
127
 
128
128
  ```svelte
129
129
  <main class="grid grid-cols-2 grid-rows-2 gap-4 overflow-hidden p-4">
@@ -134,6 +134,25 @@ For co-branding, pass a `partnerLogo` snippet to replace the default placeholder
134
134
  </main>
135
135
  ```
136
136
 
137
+ ### Asymmetric Cards (Stacked Left + Full-Height Right)
138
+
139
+ When one column has multiple cards and the other has a single tall card, use `grid-rows-subgrid` or nested flex columns. Cards must stretch to fill available height — never leave empty space at the bottom.
140
+
141
+ ```svelte
142
+ <main class="grid grid-cols-[1fr_2fr] gap-4 overflow-hidden p-4">
143
+ <!-- Left column: stacked cards that fill height -->
144
+ <div class="flex flex-col gap-4 overflow-hidden">
145
+ <Card class="max-h-full"><!-- input --></Card>
146
+ <Card class="max-h-full"><!-- status --></Card>
147
+ <Card class="max-h-full flex-1"><!-- summary (grows to fill remaining space) --></Card>
148
+ </div>
149
+ <!-- Right column: single card spanning full height -->
150
+ <Card class="max-h-full overflow-hidden"><!-- log / results --></Card>
151
+ </main>
152
+ ```
153
+
154
+ The `flex-1` on the last left-column card makes it grow to fill remaining vertical space. Adjust column ratio (`1fr_2fr`, `1fr_1fr`, etc.) to match content needs.
155
+
137
156
  ## Key Tailwind Classes
138
157
 
139
158
  | Class | Purpose |
@@ -0,0 +1,212 @@
1
+ import { existsSync, readdirSync, copyFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+
4
+ // all 15 required font files
5
+ export const REQUIRED_FONTS = [
6
+ // PP Neue Montreal Sans (.ttf)
7
+ 'PPNeueMontreal-Thin.ttf',
8
+ 'PPNeueMontreal-ThinItalic.ttf',
9
+ 'PPNeueMontreal-Light.ttf',
10
+ 'PPNeueMontreal-Regular.ttf',
11
+ 'PPNeueMontreal-Italic.ttf',
12
+ 'PPNeueMontreal-Book.ttf',
13
+ 'PPNeueMontreal-Medium.ttf',
14
+ 'PPNeueMontreal-Bold.ttf',
15
+ 'PPNeueMontreal-BoldItalic.ttf',
16
+ // PP Neue Montreal Mono (.otf)
17
+ 'PPNeueMontrealMono-Thin.otf',
18
+ 'PPNeueMontrealMono-Regular.otf',
19
+ 'PPNeueMontrealMono-RegularItalic.otf',
20
+ 'PPNeueMontrealMono-Book.otf',
21
+ 'PPNeueMontrealMono-Medium.otf',
22
+ 'PPNeueMontrealMono-Bold.otf'
23
+ ];
24
+
25
+ // hardcoded fonts.css template
26
+ export const FONTS_CSS = `/* PP Neue Montreal Sans*/
27
+
28
+ @font-face {
29
+ font-family: 'PP Neue Montreal';
30
+ src: url('/fonts/PPNeueMontreal-Thin.ttf') format('truetype');
31
+ font-weight: 100;
32
+ font-style: normal;
33
+ font-display: swap;
34
+ }
35
+
36
+ @font-face {
37
+ font-family: 'PP Neue Montreal';
38
+ src: url('/fonts/PPNeueMontreal-ThinItalic.ttf') format('truetype');
39
+ font-weight: 100;
40
+ font-style: italic;
41
+ font-display: swap;
42
+ }
43
+
44
+ @font-face {
45
+ font-family: 'PP Neue Montreal';
46
+ src: url('/fonts/PPNeueMontreal-Light.ttf') format('truetype');
47
+ font-weight: 300;
48
+ font-style: normal;
49
+ font-display: swap;
50
+ }
51
+
52
+ @font-face {
53
+ font-family: 'PP Neue Montreal';
54
+ src: url('/fonts/PPNeueMontreal-Regular.ttf') format('truetype');
55
+ font-weight: 400;
56
+ font-style: normal;
57
+ font-display: swap;
58
+ }
59
+
60
+ @font-face {
61
+ font-family: 'PP Neue Montreal';
62
+ src: url('/fonts/PPNeueMontreal-Italic.ttf') format('truetype');
63
+ font-weight: 400;
64
+ font-style: italic;
65
+ font-display: swap;
66
+ }
67
+
68
+ @font-face {
69
+ font-family: 'PP Neue Montreal';
70
+ src: url('/fonts/PPNeueMontreal-Book.ttf') format('truetype');
71
+ font-weight: 400;
72
+ font-style: normal;
73
+ font-display: swap;
74
+ }
75
+
76
+ @font-face {
77
+ font-family: 'PP Neue Montreal';
78
+ src: url('/fonts/PPNeueMontreal-Medium.ttf') format('truetype');
79
+ font-weight: 500;
80
+ font-style: normal;
81
+ font-display: swap;
82
+ }
83
+
84
+ @font-face {
85
+ font-family: 'PP Neue Montreal';
86
+ src: url('/fonts/PPNeueMontreal-Bold.ttf') format('truetype');
87
+ font-weight: 700;
88
+ font-style: normal;
89
+ font-display: swap;
90
+ }
91
+
92
+ @font-face {
93
+ font-family: 'PP Neue Montreal';
94
+ src: url('/fonts/PPNeueMontreal-BoldItalic.ttf') format('truetype');
95
+ font-weight: 700;
96
+ font-style: italic;
97
+ font-display: swap;
98
+ }
99
+
100
+ /* PP Neue Montreal Mono */
101
+
102
+ @font-face {
103
+ font-family: 'PP Neue Montreal Mono';
104
+ src: url('/fonts/PPNeueMontrealMono-Thin.otf') format('opentype');
105
+ font-weight: 100;
106
+ font-style: normal;
107
+ font-display: swap;
108
+ }
109
+
110
+ @font-face {
111
+ font-family: 'PP Neue Montreal Mono';
112
+ src: url('/fonts/PPNeueMontrealMono-Regular.otf') format('opentype');
113
+ font-weight: 400;
114
+ font-style: normal;
115
+ font-display: swap;
116
+ }
117
+
118
+ @font-face {
119
+ font-family: 'PP Neue Montreal Mono';
120
+ src: url('/fonts/PPNeueMontrealMono-RegularItalic.otf') format('opentype');
121
+ font-weight: 400;
122
+ font-style: italic;
123
+ font-display: swap;
124
+ }
125
+
126
+ @font-face {
127
+ font-family: 'PP Neue Montreal Mono';
128
+ src: url('/fonts/PPNeueMontrealMono-Book.otf') format('opentype');
129
+ font-weight: 400;
130
+ font-style: normal;
131
+ font-display: swap;
132
+ }
133
+
134
+ @font-face {
135
+ font-family: 'PP Neue Montreal Mono';
136
+ src: url('/fonts/PPNeueMontrealMono-Medium.otf') format('opentype');
137
+ font-weight: 500;
138
+ font-style: normal;
139
+ font-display: swap;
140
+ }
141
+
142
+ @font-face {
143
+ font-family: 'PP Neue Montreal Mono';
144
+ src: url('/fonts/PPNeueMontrealMono-Bold.otf') format('opentype');
145
+ font-weight: 700;
146
+ font-style: normal;
147
+ font-display: swap;
148
+ }
149
+ `;
150
+
151
+ // validate that a directory contains all required font files
152
+ export function validateFontsPath(fontsPath) {
153
+ const resolved = resolve(fontsPath);
154
+
155
+ if (!existsSync(resolved)) {
156
+ return { valid: false, missing: REQUIRED_FONTS, error: `Path does not exist: ${resolved}` };
157
+ }
158
+
159
+ let entries;
160
+ try {
161
+ entries = readdirSync(resolved);
162
+ } catch {
163
+ return { valid: false, missing: REQUIRED_FONTS, error: `Cannot read directory: ${resolved}` };
164
+ }
165
+
166
+ const missing = REQUIRED_FONTS.filter((f) => !entries.includes(f));
167
+ if (missing.length > 0) {
168
+ return {
169
+ valid: false,
170
+ missing,
171
+ error: `Missing ${missing.length} font file(s): ${missing.join(', ')}`
172
+ };
173
+ }
174
+
175
+ return { valid: true, missing: [] };
176
+ }
177
+
178
+ // copy required font files to <project>/static/fonts/
179
+ export function copyFontsToStatic(fontsPath, projectPath) {
180
+ const src = resolve(fontsPath);
181
+ const dest = join(projectPath, 'static', 'fonts');
182
+ mkdirSync(dest, { recursive: true });
183
+
184
+ for (const file of REQUIRED_FONTS) {
185
+ copyFileSync(join(src, file), join(dest, file));
186
+ }
187
+
188
+ return REQUIRED_FONTS.length;
189
+ }
190
+
191
+ // write fonts.css to src/routes/fonts.css
192
+ function writeFontsCss(projectPath) {
193
+ const cssPath = join(projectPath, 'src', 'routes', 'fonts.css');
194
+ const dir = join(projectPath, 'src', 'routes');
195
+ if (!existsSync(dir)) {
196
+ mkdirSync(dir, { recursive: true });
197
+ }
198
+ writeFileSync(cssPath, FONTS_CSS);
199
+ }
200
+
201
+ // orchestrate: validate, copy fonts, write fonts.css
202
+ export function installLocalFonts(fontsPath, projectPath) {
203
+ const validation = validateFontsPath(fontsPath);
204
+ if (!validation.valid) {
205
+ return { success: false, fileCount: 0, error: validation.error };
206
+ }
207
+
208
+ const fileCount = copyFontsToStatic(fontsPath, projectPath);
209
+ writeFontsCss(projectPath);
210
+
211
+ return { success: true, fileCount };
212
+ }
@@ -1,5 +1,3 @@
1
- import * as p from '@clack/prompts';
2
-
3
1
  export const DEFAULTS = {
4
2
  framework: 'svelte',
5
3
  pm: 'npm',
@@ -10,10 +8,3 @@ export function isInteractive(flags) {
10
8
  if (flags.defaults) return false;
11
9
  return !!process.stdin.isTTY;
12
10
  }
13
-
14
- export function logDefaultsNotice(applied) {
15
- const parts = Object.entries(applied).map(([k, v]) => `${k}=${v}`);
16
- if (parts.length > 0) {
17
- p.log.info(`Non-interactive mode: using defaults (${parts.join(', ')})`);
18
- }
19
- }
@@ -88,23 +88,6 @@ export async function installTokens(pm, projectPath) {
88
88
  }
89
89
  }
90
90
 
91
- // install internal fonts package
92
- export async function installFonts(pm, projectPath) {
93
- const [cmd, ...baseArgs] = splitCmd(pm.install);
94
- const args = [...baseArgs, '@archetypeai/ds-lib-fonts-internal'];
95
- const s = p.spinner();
96
- s.start('Installing internal fonts');
97
- try {
98
- await run(cmd, args, { cwd: projectPath });
99
- s.stop('Internal fonts installed');
100
- return true;
101
- } catch {
102
- s.stop('Failed to install internal fonts');
103
- p.log.warn('Make sure you are authenticated with npm (run: npm login)');
104
- return false;
105
- }
106
- }
107
-
108
91
  // init shadcn-svelte
109
92
  export async function initShadcn(pm, projectPath) {
110
93
  const s = p.spinner();
@@ -166,7 +149,7 @@ export function cn(...inputs) {
166
149
  export function configureCss(projectPath, includeFonts) {
167
150
  let css = '';
168
151
  if (includeFonts) {
169
- css += '@import "@archetypeai/ds-lib-fonts-internal";\n';
152
+ css += '@import "./fonts.css";\n';
170
153
  }
171
154
  css += '@import "@archetypeai/ds-lib-tokens/theme.css";\n';
172
155
  css += '@import "tailwindcss";\n';
@@ -211,7 +194,7 @@ export function prependCss(projectPath, includeFonts) {
211
194
 
212
195
  let imports = '';
213
196
  if (includeFonts) {
214
- imports += '@import "@archetypeai/ds-lib-fonts-internal";\n';
197
+ imports += '@import "./fonts.css";\n';
215
198
  }
216
199
  imports += '@import "@archetypeai/ds-lib-tokens/theme.css";\n';
217
200
  imports += '@import "tailwindcss";\n';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archetypeai/ds-cli",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Archetype AI Design System CLI Tool",
5
5
  "type": "module",
6
6
  "bin": {