@archetypeai/ds-cli 0.3.7
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/LICENSE +21 -0
- package/README.md +123 -0
- package/bin.js +77 -0
- package/commands/add.js +42 -0
- package/commands/create.js +238 -0
- package/commands/init.js +199 -0
- package/files/AGENTS.md +63 -0
- package/files/CLAUDE.md +63 -0
- package/files/LICENSE +21 -0
- package/files/rules/accessibility.md +219 -0
- package/files/rules/charts.md +352 -0
- package/files/rules/components.md +267 -0
- package/files/rules/design-principles.md +56 -0
- package/files/rules/linting.md +31 -0
- package/files/rules/state.md +405 -0
- package/files/rules/styling.md +245 -0
- package/files/skills/apply-ds/SKILL.md +117 -0
- package/files/skills/apply-ds/scripts/setup.sh +271 -0
- package/files/skills/build-pattern/SKILL.md +202 -0
- package/files/skills/create-dashboard/SKILL.md +189 -0
- package/files/skills/deploy-worker/SKILL.md +231 -0
- package/files/skills/deploy-worker/references/wrangler-commands.md +327 -0
- package/files/skills/fix-accessibility/SKILL.md +184 -0
- package/files/skills/fix-metadata/SKILL.md +118 -0
- package/files/skills/fix-metadata/assets/favicon.ico +0 -0
- package/files/skills/setup-chart/SKILL.md +225 -0
- package/files/skills/setup-chart/data/embedding.csv +42 -0
- package/files/skills/setup-chart/data/timeseries.csv +173 -0
- package/files/skills/setup-chart/references/scatter-chart.md +229 -0
- package/files/skills/setup-chart/references/sensor-chart.md +156 -0
- package/lib/add-ds-config-codeagent.js +154 -0
- package/lib/add-ds-ui-svelte.js +93 -0
- package/lib/scaffold-ds-svelte-project.js +272 -0
- package/lib/use-package-manager.js +65 -0
- package/lib/use-shadcn-svelte-registry.js +26 -0
- package/lib/validate-url.js +31 -0
- package/package.json +34 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# SensorChart Pattern Reference
|
|
2
|
+
|
|
3
|
+
Line charts for time series data, sensor readings, and streaming signals.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx shadcn-svelte@latest add --registry https://design-system.archetypeai.workers.dev/r sensor-chart
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This installs `SensorChart.svelte` and its dependencies (`card`, `chart`, `utils`, `layerchart`, `d3-scale`, `d3-shape`).
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
| ----------- | -------------------------------------- | ------------------ | ------------------------------------------------- |
|
|
17
|
+
| `title` | string | `"UNKNOWN SENSOR"` | Card header label identifying the sensor |
|
|
18
|
+
| `icon` | Component | undefined | Lucide icon component for header |
|
|
19
|
+
| `data` | Array<Record<string, any>> | `[]` | Array of data points to chart |
|
|
20
|
+
| `signals` | Record<string, string> | `{}` | Map of data key to display label (defines series) |
|
|
21
|
+
| `xKey` | string | `"timestamp"` | Data key for x-axis values |
|
|
22
|
+
| `maxPoints` | number | undefined | Enables streaming mode with sliding window |
|
|
23
|
+
| `yMin` | number | **required** | Minimum y-axis value |
|
|
24
|
+
| `yMax` | number | **required** | Maximum y-axis value |
|
|
25
|
+
| `yTicks` | number[] | undefined | Explicit y-axis tick values |
|
|
26
|
+
| `axis` | `"both"` \| `"x"` \| `"y"` \| `"none"` | `"both"` | Which axes to display |
|
|
27
|
+
| `class` | string | undefined | Additional CSS classes |
|
|
28
|
+
|
|
29
|
+
## Key Behaviors
|
|
30
|
+
|
|
31
|
+
- **Static mode** (no `maxPoints`): Uses `scaleUtc()` for x-axis with timestamp-based positioning. X-axis shows MM:SS relative to first data point.
|
|
32
|
+
- **Streaming mode** (`maxPoints` set): Uses `scaleLinear()` with index-based x positioning (`_index`). Data window slides as new points arrive.
|
|
33
|
+
- **Series**: Derived from `signals` prop. Each entry maps a data key to a display label and auto-assigns theme colors (`--chart-1` through `--chart-5`).
|
|
34
|
+
- **Curve**: Uses `curveNatural` from d3-shape for smooth lines.
|
|
35
|
+
- **Chart type**: Uses `LineChart` from layerchart — NOT `AreaChart` (which renders with unwanted fill).
|
|
36
|
+
|
|
37
|
+
## Complete Example — Static Data
|
|
38
|
+
|
|
39
|
+
```svelte
|
|
40
|
+
<script>
|
|
41
|
+
import SensorChart from '$lib/components/ui/SensorChart.svelte';
|
|
42
|
+
import AudioWaveformIcon from '@lucide/svelte/icons/audio-waveform';
|
|
43
|
+
import timeseriesCsv from '$lib/data/timeseries.csv?raw';
|
|
44
|
+
|
|
45
|
+
function parseCsv(csvText) {
|
|
46
|
+
const lines = csvText
|
|
47
|
+
.trim()
|
|
48
|
+
.split('\n')
|
|
49
|
+
.filter((line) => line.trim());
|
|
50
|
+
if (lines.length === 0) return [];
|
|
51
|
+
const headers = lines[0].split(',').map((h) => h.trim());
|
|
52
|
+
return lines.slice(1).map((line) => {
|
|
53
|
+
const values = line.split(',');
|
|
54
|
+
const row = {};
|
|
55
|
+
headers.forEach((header, index) => {
|
|
56
|
+
row[header] = values[index]?.trim() || '';
|
|
57
|
+
});
|
|
58
|
+
return row;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rawRows = parseCsv(timeseriesCsv);
|
|
63
|
+
const data = rawRows.map((row) => ({
|
|
64
|
+
timestamp: new Date(row.timestamp),
|
|
65
|
+
accel_x: parseFloat(row.accel_x),
|
|
66
|
+
accel_y: parseFloat(row.accel_y),
|
|
67
|
+
accel_z: parseFloat(row.accel_z)
|
|
68
|
+
}));
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<SensorChart
|
|
72
|
+
title="ACCELERATION SENSOR"
|
|
73
|
+
icon={AudioWaveformIcon}
|
|
74
|
+
{data}
|
|
75
|
+
signals={{ accel_x: 'Accel X', accel_y: 'Accel Y', accel_z: 'Accel Z' }}
|
|
76
|
+
yMin={-30}
|
|
77
|
+
yMax={30}
|
|
78
|
+
yTicks={[-30, -15, 0, 15, 30]}
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Complete Example — Streaming Data
|
|
83
|
+
|
|
84
|
+
For real-time data with a sliding window, use the `maxPoints` prop and feed data incrementally:
|
|
85
|
+
|
|
86
|
+
```svelte
|
|
87
|
+
<script>
|
|
88
|
+
import { onMount, onDestroy } from 'svelte';
|
|
89
|
+
import SensorChart from '$lib/components/ui/SensorChart.svelte';
|
|
90
|
+
import ThermometerIcon from '@lucide/svelte/icons/thermometer';
|
|
91
|
+
|
|
92
|
+
let streamingData = $state([]);
|
|
93
|
+
let interval;
|
|
94
|
+
|
|
95
|
+
onMount(() => {
|
|
96
|
+
interval = setInterval(() => {
|
|
97
|
+
streamingData = [
|
|
98
|
+
...streamingData,
|
|
99
|
+
{
|
|
100
|
+
timestamp: new Date(),
|
|
101
|
+
gyro_x: (Math.random() - 0.5) * 4
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
}, 120);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
onDestroy(() => clearInterval(interval));
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<SensorChart
|
|
111
|
+
title="GYROSCOPE SENSOR"
|
|
112
|
+
icon={ThermometerIcon}
|
|
113
|
+
data={streamingData}
|
|
114
|
+
signals={{ gyro_x: 'Gyro X' }}
|
|
115
|
+
maxPoints={50}
|
|
116
|
+
yMin={-2}
|
|
117
|
+
yMax={2}
|
|
118
|
+
yTicks={[-2, -1, 0, 1, 2]}
|
|
119
|
+
axis="y"
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Building Inline (Without Pattern)
|
|
124
|
+
|
|
125
|
+
If the `sensor-chart` pattern is not installed, build the equivalent using `card` + `chart` primitives with `LineChart` from layerchart:
|
|
126
|
+
|
|
127
|
+
```svelte
|
|
128
|
+
<script>
|
|
129
|
+
import * as Chart from '$lib/components/ui/chart/index.js';
|
|
130
|
+
import { LineChart } from 'layerchart';
|
|
131
|
+
import { scaleUtc, scaleLinear } from 'd3-scale';
|
|
132
|
+
import { curveNatural } from 'd3-shape';
|
|
133
|
+
|
|
134
|
+
let { data } = $props();
|
|
135
|
+
|
|
136
|
+
const chartConfig = {
|
|
137
|
+
value: { label: 'Value', color: 'var(--chart-1)' }
|
|
138
|
+
};
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<Chart.Container config={chartConfig} class="h-[220px] w-full">
|
|
142
|
+
<LineChart
|
|
143
|
+
{data}
|
|
144
|
+
x="timestamp"
|
|
145
|
+
xScale={scaleUtc()}
|
|
146
|
+
yScale={scaleLinear()}
|
|
147
|
+
series={[{ key: 'value', color: 'var(--chart-1)' }]}
|
|
148
|
+
tooltip={false}
|
|
149
|
+
props={{
|
|
150
|
+
spline: { curve: curveNatural, strokeWidth: 1.5 },
|
|
151
|
+
grid: { y: true, x: false },
|
|
152
|
+
highlight: { lines: false, points: false }
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
</Chart.Container>
|
|
156
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const FILES_DIR = join(__dirname, '..', 'files');
|
|
8
|
+
|
|
9
|
+
function copyDir(src, dest) {
|
|
10
|
+
if (!existsSync(src)) return;
|
|
11
|
+
|
|
12
|
+
mkdirSync(dest, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const srcPath = join(src, entry.name);
|
|
17
|
+
const destPath = join(dest, entry.name);
|
|
18
|
+
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
copyDir(srcPath, destPath);
|
|
21
|
+
} else {
|
|
22
|
+
if (existsSync(destPath)) {
|
|
23
|
+
p.log.warn(`Skipped (exists): ${destPath}`);
|
|
24
|
+
} else {
|
|
25
|
+
copyFileSync(srcPath, destPath);
|
|
26
|
+
p.log.success(`Copied: ${destPath}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyFile(src, dest) {
|
|
33
|
+
if (!existsSync(src)) {
|
|
34
|
+
p.log.warn(`Source not found: ${src}`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (existsSync(dest)) {
|
|
39
|
+
p.log.warn(`Skipped (exists): ${dest}`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
copyFileSync(src, dest);
|
|
44
|
+
p.log.success(`Copied: ${dest}`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function setupCursor(projectDir) {
|
|
49
|
+
p.log.step('Setting up for Cursor IDE');
|
|
50
|
+
|
|
51
|
+
// copy AGENTS.md to root
|
|
52
|
+
copyFile(join(FILES_DIR, 'AGENTS.md'), join(projectDir, 'AGENTS.md'));
|
|
53
|
+
|
|
54
|
+
// copy skills to .cursor/skills/
|
|
55
|
+
const skillsSrc = join(FILES_DIR, 'skills');
|
|
56
|
+
if (existsSync(skillsSrc)) {
|
|
57
|
+
copyDir(skillsSrc, join(projectDir, '.cursor', 'skills'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// copy rules to .cursor/rules/
|
|
61
|
+
const rulesSrc = join(FILES_DIR, 'rules');
|
|
62
|
+
if (existsSync(rulesSrc)) {
|
|
63
|
+
copyDir(rulesSrc, join(projectDir, '.cursor', 'rules'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// copy LICENSE to .cursor/
|
|
67
|
+
copyFile(join(FILES_DIR, 'LICENSE'), join(projectDir, '.cursor', 'LICENSE'));
|
|
68
|
+
|
|
69
|
+
p.log.success('Cursor setup complete');
|
|
70
|
+
p.log.info('Tip: Reload Cursor window (Cmd+Shift+P > "Reload Window")');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setupClaude(projectDir) {
|
|
74
|
+
p.log.step('Setting up for Claude Code');
|
|
75
|
+
|
|
76
|
+
// copy CLAUDE.md to root
|
|
77
|
+
copyFile(join(FILES_DIR, 'CLAUDE.md'), join(projectDir, 'CLAUDE.md'));
|
|
78
|
+
|
|
79
|
+
// copy skills to .claude/skills/
|
|
80
|
+
const skillsSrc = join(FILES_DIR, 'skills');
|
|
81
|
+
if (existsSync(skillsSrc)) {
|
|
82
|
+
copyDir(skillsSrc, join(projectDir, '.claude', 'skills'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// copy rules to .claude/rules/
|
|
86
|
+
const rulesSrc = join(FILES_DIR, 'rules');
|
|
87
|
+
if (existsSync(rulesSrc)) {
|
|
88
|
+
copyDir(rulesSrc, join(projectDir, '.claude', 'rules'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// copy LICENSE to .claude/
|
|
92
|
+
copyFile(join(FILES_DIR, 'LICENSE'), join(projectDir, '.claude', 'LICENSE'));
|
|
93
|
+
|
|
94
|
+
p.log.success('Claude Code setup complete');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function addDsConfigCodeagent(flags) {
|
|
98
|
+
const isCursor = flags.includes('--cursor');
|
|
99
|
+
const isClaude = flags.includes('--claude');
|
|
100
|
+
const isHelp = flags.includes('--help') || flags.includes('-h');
|
|
101
|
+
|
|
102
|
+
if (isHelp) {
|
|
103
|
+
console.log(`
|
|
104
|
+
ds add ds-config-codeagent - Install agent configurations
|
|
105
|
+
|
|
106
|
+
Usage:
|
|
107
|
+
npx @archetypeai/ds-cli add ds-config-codeagent --cursor Setup for Cursor IDE
|
|
108
|
+
npx @archetypeai/ds-cli add ds-config-codeagent --claude Setup for Claude Code
|
|
109
|
+
`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let agent;
|
|
114
|
+
|
|
115
|
+
if (isCursor && isClaude) {
|
|
116
|
+
p.log.error('Please specify only one: --cursor or --claude');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
} else if (isCursor) {
|
|
119
|
+
agent = 'cursor';
|
|
120
|
+
} else if (isClaude) {
|
|
121
|
+
agent = 'claude';
|
|
122
|
+
} else {
|
|
123
|
+
// interactive prompt when no flag is passed
|
|
124
|
+
agent = await p.select({
|
|
125
|
+
message: 'Which agent configuration?',
|
|
126
|
+
options: [
|
|
127
|
+
{ value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
|
|
128
|
+
{ value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules' }
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
if (p.isCancel(agent)) {
|
|
132
|
+
p.cancel('Setup cancelled.');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// verify files directory exists
|
|
138
|
+
if (!existsSync(FILES_DIR)) {
|
|
139
|
+
p.log.error('Package files not found. This may be a corrupted installation.');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
p.intro('Installing Archetype AI agent configurations.');
|
|
144
|
+
|
|
145
|
+
const projectDir = process.cwd();
|
|
146
|
+
|
|
147
|
+
if (agent === 'cursor') {
|
|
148
|
+
setupCursor(projectDir);
|
|
149
|
+
} else {
|
|
150
|
+
setupClaude(projectDir);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
p.outro('Agent configuration installed.');
|
|
154
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
6
|
+
import { validateRegistryUrl } from './validate-url.js';
|
|
7
|
+
import { detectPm, getPm } from './use-package-manager.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const REGISTRY_URL = process.env.REGISTRY_URL || 'https://design-system.archetypeai.workers.dev';
|
|
11
|
+
const ALL_COMPONENTS_URL = `${REGISTRY_URL}/r/all.json`;
|
|
12
|
+
|
|
13
|
+
export async function addDsUiSvelte() {
|
|
14
|
+
p.intro('Install all components from Archetype AI Design System.');
|
|
15
|
+
|
|
16
|
+
const projectDir = process.cwd();
|
|
17
|
+
|
|
18
|
+
// check if shadcn-svelte is initialized
|
|
19
|
+
if (!existsSync(join(projectDir, 'components.json'))) {
|
|
20
|
+
p.log.error('shadcn-svelte is not initialized in this project.');
|
|
21
|
+
p.log.info('Please run: npx shadcn-svelte@latest init');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// detect package manager from lockfile, fall back to npm
|
|
26
|
+
const pmName = detectPm(projectDir) || 'npm';
|
|
27
|
+
const pm = getPm(pmName);
|
|
28
|
+
|
|
29
|
+
const s = p.spinner();
|
|
30
|
+
s.start('Fetching component list from registry');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(ALL_COMPONENTS_URL, {
|
|
34
|
+
signal: AbortSignal.timeout(15_000)
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Failed to fetch components: ${response.status} ${response.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const componentUrls = await response.json();
|
|
41
|
+
if (!Array.isArray(componentUrls) || componentUrls.length === 0) {
|
|
42
|
+
throw new Error('No components found in registry');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// validate all URLs before passing to shell
|
|
46
|
+
for (const url of componentUrls) {
|
|
47
|
+
validateRegistryUrl(url);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
s.stop(`Found ${componentUrls.length} components`);
|
|
51
|
+
|
|
52
|
+
const installSpinner = p.spinner();
|
|
53
|
+
installSpinner.start('Installing components');
|
|
54
|
+
|
|
55
|
+
const [dlxCmd, ...dlxArgs] = pm.dlx.split(/\s+/);
|
|
56
|
+
execFileSync(
|
|
57
|
+
dlxCmd,
|
|
58
|
+
[...dlxArgs, 'shadcn-svelte@latest', 'add', ...componentUrls, '--yes', '--overwrite'],
|
|
59
|
+
{ stdio: 'pipe', cwd: projectDir }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
installSpinner.stop('All components installed');
|
|
63
|
+
|
|
64
|
+
// copy LICENSE alongside installed components
|
|
65
|
+
try {
|
|
66
|
+
const licenseSrc = join(__dirname, '..', 'LICENSE');
|
|
67
|
+
if (existsSync(licenseSrc)) {
|
|
68
|
+
let componentsDir = 'src/lib/components/ui';
|
|
69
|
+
try {
|
|
70
|
+
const config = JSON.parse(readFileSync(join(projectDir, 'components.json'), 'utf8'));
|
|
71
|
+
if (config.aliases?.components) {
|
|
72
|
+
componentsDir = config.aliases.components.replace('$lib', 'src/lib');
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
p.log.warn('Could not read components.json alias config, using default path');
|
|
76
|
+
}
|
|
77
|
+
const licenseDest = join(projectDir, componentsDir, 'LICENSE');
|
|
78
|
+
if (!existsSync(licenseDest)) {
|
|
79
|
+
writeFileSync(licenseDest, readFileSync(licenseSrc, 'utf8'));
|
|
80
|
+
p.log.success(`License copied to ${componentsDir}/LICENSE`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
p.log.warn('Could not copy LICENSE file');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
p.outro('Components installed successfully.');
|
|
88
|
+
} catch (error) {
|
|
89
|
+
s.stop('Failed');
|
|
90
|
+
p.log.error(error.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { setupCursor, setupClaude } from './add-ds-config-codeagent.js';
|
|
6
|
+
|
|
7
|
+
// run a command without a shell to prevent injection
|
|
8
|
+
function run(command, args, opts) {
|
|
9
|
+
execFileSync(command, args, { stdio: 'pipe', ...opts });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// write a file
|
|
13
|
+
function writeFile(filePath, content) {
|
|
14
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
writeFileSync(filePath, content);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// split a package manager command string into [command, ...args]
|
|
22
|
+
function splitCmd(cmdString) {
|
|
23
|
+
return cmdString.split(/\s+/);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// run sveltekit create
|
|
27
|
+
export function runSvCreate(pm, name, targetDir) {
|
|
28
|
+
const [cmd, ...baseArgs] = splitCmd(pm.dlx);
|
|
29
|
+
const args = [
|
|
30
|
+
...baseArgs,
|
|
31
|
+
'sv@latest',
|
|
32
|
+
'create',
|
|
33
|
+
name,
|
|
34
|
+
'--template',
|
|
35
|
+
'minimal',
|
|
36
|
+
'--no-types',
|
|
37
|
+
'--add',
|
|
38
|
+
'tailwindcss=plugins:none',
|
|
39
|
+
...splitCmd(pm.svInstallFlag)
|
|
40
|
+
];
|
|
41
|
+
const s = p.spinner();
|
|
42
|
+
s.start('Creating SvelteKit project');
|
|
43
|
+
try {
|
|
44
|
+
run(cmd, args, { cwd: targetDir });
|
|
45
|
+
s.stop('SvelteKit project created');
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
s.stop('Failed to create SvelteKit project');
|
|
49
|
+
p.log.error(error.stderr?.toString() || error.message);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// install design tokens
|
|
55
|
+
export function installTokens(pm, projectPath) {
|
|
56
|
+
const [cmd, ...baseArgs] = splitCmd(pm.install);
|
|
57
|
+
const args = [...baseArgs, '@archetypeai/ds-lib-tokens'];
|
|
58
|
+
const s = p.spinner();
|
|
59
|
+
s.start('Installing design tokens');
|
|
60
|
+
try {
|
|
61
|
+
run(cmd, args, { cwd: projectPath });
|
|
62
|
+
s.stop('Design tokens installed');
|
|
63
|
+
return true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
s.stop('Failed to install design tokens');
|
|
66
|
+
p.log.error(error.stderr?.toString() || error.message);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// install internal fonts package
|
|
72
|
+
export function installFonts(pm, projectPath) {
|
|
73
|
+
const [cmd, ...baseArgs] = splitCmd(pm.install);
|
|
74
|
+
const args = [...baseArgs, '@archetypeai/ds-lib-fonts-internal'];
|
|
75
|
+
const s = p.spinner();
|
|
76
|
+
s.start('Installing internal fonts');
|
|
77
|
+
try {
|
|
78
|
+
run(cmd, args, { cwd: projectPath });
|
|
79
|
+
s.stop('Internal fonts installed');
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
s.stop('Failed to install internal fonts');
|
|
83
|
+
p.log.warn('Make sure you are authenticated with npm (run: npm login)');
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// init shadcn-svelte
|
|
89
|
+
export function initShadcn(pm, projectPath) {
|
|
90
|
+
const s = p.spinner();
|
|
91
|
+
s.start('Setting up shadcn-svelte');
|
|
92
|
+
try {
|
|
93
|
+
const componentsJson = {
|
|
94
|
+
$schema: 'https://shadcn-svelte.com/schema.json',
|
|
95
|
+
style: 'default',
|
|
96
|
+
tailwind: {
|
|
97
|
+
config: '',
|
|
98
|
+
css: 'src/routes/layout.css',
|
|
99
|
+
baseColor: 'slate',
|
|
100
|
+
cssVariables: true,
|
|
101
|
+
prefix: ''
|
|
102
|
+
},
|
|
103
|
+
aliases: {
|
|
104
|
+
utils: '$lib/utils',
|
|
105
|
+
primitives: '$lib/components/primitives',
|
|
106
|
+
lib: '$lib',
|
|
107
|
+
components: '$lib/components'
|
|
108
|
+
},
|
|
109
|
+
typescript: false
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const componentsJsonPath = join(projectPath, 'components.json');
|
|
113
|
+
if (existsSync(componentsJsonPath)) {
|
|
114
|
+
p.log.info('components.json already exists, skipping');
|
|
115
|
+
} else {
|
|
116
|
+
writeFile(componentsJsonPath, JSON.stringify(componentsJson, null, 2));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const utilsPath = join(projectPath, 'src/lib/utils.js');
|
|
120
|
+
const utilsContent = `import { clsx } from "clsx";
|
|
121
|
+
import { twMerge } from "tailwind-merge";
|
|
122
|
+
|
|
123
|
+
export function cn(...inputs) {
|
|
124
|
+
\treturn twMerge(clsx(inputs));
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
if (existsSync(utilsPath)) {
|
|
128
|
+
p.log.info('src/lib/utils.js already exists, skipping');
|
|
129
|
+
} else {
|
|
130
|
+
writeFile(utilsPath, utilsContent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const [cmd, ...baseArgs] = splitCmd(pm.install);
|
|
134
|
+
run(cmd, [...baseArgs, 'clsx', 'tailwind-merge', 'tw-animate-css'], { cwd: projectPath });
|
|
135
|
+
|
|
136
|
+
s.stop('shadcn-svelte ready');
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
s.stop('Failed to set up shadcn-svelte');
|
|
140
|
+
p.log.error(error.stderr?.toString() || error.message);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// configure CSS
|
|
146
|
+
export function configureCss(projectPath, includeFonts) {
|
|
147
|
+
let css = '';
|
|
148
|
+
if (includeFonts) {
|
|
149
|
+
css += '@import "@archetypeai/ds-lib-fonts-internal";\n';
|
|
150
|
+
}
|
|
151
|
+
css += '@import "@archetypeai/ds-lib-tokens/theme.css";\n';
|
|
152
|
+
css += '@import "tailwindcss";\n';
|
|
153
|
+
css += '@import "tw-animate-css";\n';
|
|
154
|
+
|
|
155
|
+
const cssPath = join(projectPath, 'src/routes/layout.css');
|
|
156
|
+
writeFile(cssPath, css);
|
|
157
|
+
p.log.success('CSS configured');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// install Tailwind CSS v4
|
|
161
|
+
export function installTailwind(pm, projectPath) {
|
|
162
|
+
const [cmd, ...baseArgs] = splitCmd(pm.install);
|
|
163
|
+
const args = [...baseArgs, 'tailwindcss@latest'];
|
|
164
|
+
const s = p.spinner();
|
|
165
|
+
s.start('Installing Tailwind CSS v4');
|
|
166
|
+
try {
|
|
167
|
+
run(cmd, args, { cwd: projectPath });
|
|
168
|
+
s.stop('Tailwind CSS installed');
|
|
169
|
+
return true;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
s.stop('Failed to install Tailwind CSS');
|
|
172
|
+
p.log.error(error.stderr?.toString() || error.message);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// prepend CSS imports to existing layout.css
|
|
178
|
+
export function prependCss(projectPath, includeFonts) {
|
|
179
|
+
const cssPath = join(projectPath, 'src/routes/layout.css');
|
|
180
|
+
|
|
181
|
+
let existing = '';
|
|
182
|
+
if (existsSync(cssPath)) {
|
|
183
|
+
existing = readFileSync(cssPath, 'utf-8');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// skip if DS imports are already present
|
|
187
|
+
if (existing.includes('@archetypeai/ds-lib-tokens/theme.css')) {
|
|
188
|
+
p.log.info('DS imports already present in layout.css, skipping');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let imports = '';
|
|
193
|
+
if (includeFonts) {
|
|
194
|
+
imports += '@import "@archetypeai/ds-lib-fonts-internal";\n';
|
|
195
|
+
}
|
|
196
|
+
imports += '@import "@archetypeai/ds-lib-tokens/theme.css";\n';
|
|
197
|
+
imports += '@import "tailwindcss";\n';
|
|
198
|
+
imports += '@import "tw-animate-css";\n';
|
|
199
|
+
|
|
200
|
+
const dir = cssPath.substring(0, cssPath.lastIndexOf('/'));
|
|
201
|
+
if (!existsSync(dir)) {
|
|
202
|
+
mkdirSync(dir, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
writeFileSync(cssPath, existing ? imports + '\n' + existing : imports);
|
|
205
|
+
p.log.success('CSS configured');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// install components from shadcn-svelte registry
|
|
209
|
+
export function installComponents(pm, projectPath, componentUrls) {
|
|
210
|
+
const s = p.spinner();
|
|
211
|
+
s.start(`Installing ${componentUrls.length} components`);
|
|
212
|
+
try {
|
|
213
|
+
const urls = componentUrls.map((c) => c.url);
|
|
214
|
+
const [dlxCmd, ...dlxArgs] = splitCmd(pm.dlx);
|
|
215
|
+
run(dlxCmd, [...dlxArgs, 'shadcn-svelte@latest', 'add', ...urls, '--yes', '--overwrite'], {
|
|
216
|
+
cwd: projectPath
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const [installCmd, ...installArgs] = splitCmd(pm.install);
|
|
220
|
+
run(
|
|
221
|
+
installCmd,
|
|
222
|
+
[...installArgs, 'tailwind-variants', 'bits-ui', '@lucide/svelte', 'layerchart', 'paneforge'],
|
|
223
|
+
{ cwd: projectPath }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
s.stop(`${componentUrls.length} components installed`);
|
|
227
|
+
return true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
s.stop('Failed to install components');
|
|
230
|
+
p.log.error(error.stderr?.toString() || error.message);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// add a demo page with Button examples to app
|
|
236
|
+
export function createDemoPage(projectPath) {
|
|
237
|
+
const pagePath = join(projectPath, 'src/routes/+page.svelte');
|
|
238
|
+
const demoContent = `<script>
|
|
239
|
+
\timport Button from "$lib/components/ui/button/button.svelte";
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<div class="flex flex-col items-center justify-center min-h-screen space-y-xl">
|
|
243
|
+
<Button variant="default">Default</Button>
|
|
244
|
+
<Button variant="ghost">Ghost</Button>
|
|
245
|
+
<Button variant="outline">Outline</Button>
|
|
246
|
+
<Button variant="secondary">Secondary</Button>
|
|
247
|
+
<Button variant="destructive">Destructive</Button>
|
|
248
|
+
<Button variant="link">Link</Button>
|
|
249
|
+
</div>
|
|
250
|
+
`;
|
|
251
|
+
writeFile(pagePath, demoContent);
|
|
252
|
+
p.log.success('Demo page created');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// install agent configuration
|
|
256
|
+
export function installAgentConfig(projectPath, agentType) {
|
|
257
|
+
const s = p.spinner();
|
|
258
|
+
s.start(`Installing ${agentType === 'cursor' ? 'Cursor' : 'Claude Code'} agent configuration`);
|
|
259
|
+
try {
|
|
260
|
+
if (agentType === 'cursor') {
|
|
261
|
+
setupCursor(projectPath);
|
|
262
|
+
} else {
|
|
263
|
+
setupClaude(projectPath);
|
|
264
|
+
}
|
|
265
|
+
s.stop('Agent configuration installed');
|
|
266
|
+
return true;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
s.stop('Failed to install agent configuration');
|
|
269
|
+
p.log.error(error.message);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|