openuispec 0.1.3 → 0.1.5
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/cli/init.ts +92 -176
- package/package.json +1 -1
package/cli/init.ts
CHANGED
|
@@ -35,13 +35,9 @@ async function askList(
|
|
|
35
35
|
options: string[],
|
|
36
36
|
defaults: string[]
|
|
37
37
|
): Promise<string[]> {
|
|
38
|
-
|
|
39
|
-
for (const opt of options) {
|
|
40
|
-
const mark = defaults.includes(opt) ? "[x]" : "[ ]";
|
|
41
|
-
console.log(` ${mark} ${opt}`);
|
|
42
|
-
}
|
|
38
|
+
const defaultStr = defaults.join(", ");
|
|
43
39
|
const raw = (
|
|
44
|
-
await rl.question(
|
|
40
|
+
await rl.question(`${question} [${options.join(", ")}] (${defaultStr}): `)
|
|
45
41
|
).trim();
|
|
46
42
|
if (!raw) return defaults;
|
|
47
43
|
return raw
|
|
@@ -122,84 +118,6 @@ api:
|
|
|
122
118
|
`;
|
|
123
119
|
}
|
|
124
120
|
|
|
125
|
-
function starterColorTokens(): string {
|
|
126
|
-
return `# Design tokens: Color palette
|
|
127
|
-
brand:
|
|
128
|
-
primary:
|
|
129
|
-
"50": "#EEF2FF"
|
|
130
|
-
"100": "#E0E7FF"
|
|
131
|
-
"500": "#6366F1"
|
|
132
|
-
"600": "#4F46E5"
|
|
133
|
-
"900": "#312E81"
|
|
134
|
-
|
|
135
|
-
surface:
|
|
136
|
-
background: "#FFFFFF"
|
|
137
|
-
card: "#F9FAFB"
|
|
138
|
-
elevated: "#FFFFFF"
|
|
139
|
-
|
|
140
|
-
text:
|
|
141
|
-
primary: "#111827"
|
|
142
|
-
secondary: "#6B7280"
|
|
143
|
-
disabled: "#D1D5DB"
|
|
144
|
-
|
|
145
|
-
semantic:
|
|
146
|
-
success: "#10B981"
|
|
147
|
-
warning: "#F59E0B"
|
|
148
|
-
error: "#EF4444"
|
|
149
|
-
info: "#3B82F6"
|
|
150
|
-
`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function starterTypographyTokens(): string {
|
|
154
|
-
return `# Design tokens: Typography
|
|
155
|
-
font_families:
|
|
156
|
-
sans: { default: "System" }
|
|
157
|
-
|
|
158
|
-
type_scale:
|
|
159
|
-
title_lg: { size: 28, weight: bold, tracking: 0, line_height: 1.2 }
|
|
160
|
-
title_md: { size: 22, weight: semibold, tracking: 0, line_height: 1.3 }
|
|
161
|
-
title_sm: { size: 17, weight: semibold, tracking: 0, line_height: 1.3 }
|
|
162
|
-
body_lg: { size: 17, weight: regular, tracking: 0, line_height: 1.5 }
|
|
163
|
-
body_md: { size: 15, weight: regular, tracking: 0, line_height: 1.5 }
|
|
164
|
-
body_sm: { size: 13, weight: regular, tracking: 0, line_height: 1.4 }
|
|
165
|
-
label_lg: { size: 15, weight: medium, tracking: 0.02, line_height: 1.3 }
|
|
166
|
-
label_md: { size: 13, weight: medium, tracking: 0.02, line_height: 1.3 }
|
|
167
|
-
caption: { size: 11, weight: regular, tracking: 0.02, line_height: 1.3 }
|
|
168
|
-
`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function starterSpacingTokens(): string {
|
|
172
|
-
return `# Design tokens: Spacing
|
|
173
|
-
base_unit: 4
|
|
174
|
-
platform_flex: "10%"
|
|
175
|
-
|
|
176
|
-
scale:
|
|
177
|
-
"0": 0
|
|
178
|
-
"1": 4
|
|
179
|
-
"2": 8
|
|
180
|
-
"3": 12
|
|
181
|
-
"4": 16
|
|
182
|
-
"5": 20
|
|
183
|
-
"6": 24
|
|
184
|
-
"8": 32
|
|
185
|
-
"10": 40
|
|
186
|
-
"12": 48
|
|
187
|
-
"16": 64
|
|
188
|
-
`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function starterLocale(): string {
|
|
192
|
-
return JSON.stringify(
|
|
193
|
-
{
|
|
194
|
-
app: {
|
|
195
|
-
name: "My App",
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
null,
|
|
199
|
-
2
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
121
|
function aiRulesBlock(specDir: string, targets: string[]): string {
|
|
204
122
|
const targetList = targets.map((t) => `"${t}"`).join(", ");
|
|
205
123
|
return `
|
|
@@ -207,49 +125,67 @@ function aiRulesBlock(specDir: string, targets: string[]): string {
|
|
|
207
125
|
# ================================
|
|
208
126
|
# This project uses OpenUISpec to define UI as a semantic spec.
|
|
209
127
|
# Spec files are the single source of truth for all UI across platforms.
|
|
128
|
+
# Targets: ${targetList}
|
|
129
|
+
|
|
130
|
+
## What is OpenUISpec
|
|
131
|
+
OpenUISpec is a YAML-based spec format that describes an app's UI semantically — tokens, screens, flows, and platform overrides. AI reads the spec and generates native code (SwiftUI, Compose, React). AI reads native code and updates the spec. The spec is the sync layer between platforms.
|
|
210
132
|
|
|
211
133
|
## Spec location
|
|
212
134
|
- Spec root: \`${specDir}/\`
|
|
213
|
-
- Manifest: \`${specDir}/openuispec.yaml\`
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
##
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
135
|
+
- Manifest: \`${specDir}/openuispec.yaml\` — always read this first.
|
|
136
|
+
- Tokens: \`${specDir}/tokens/\` — colors, typography, spacing, motion, icons, themes
|
|
137
|
+
- Screens: \`${specDir}/screens/\` — one YAML file per screen
|
|
138
|
+
- Flows: \`${specDir}/flows/\` — multi-step navigation journeys
|
|
139
|
+
- Contracts: \`${specDir}/contracts/\` — UI component definitions
|
|
140
|
+
- Platform: \`${specDir}/platform/\` — per-target overrides (iOS, Android, Web)
|
|
141
|
+
- Locales: \`${specDir}/locales/\` — i18n strings (JSON, ICU MessageFormat)
|
|
142
|
+
|
|
143
|
+
## If spec directories are empty (first-time setup)
|
|
144
|
+
This means the project has existing UI code but hasn't been specced yet. Your job:
|
|
145
|
+
|
|
146
|
+
1. **Find existing screens** — scan the codebase for UI screen files (SwiftUI views, Compose screens, React components/pages).
|
|
147
|
+
2. **Create stubs** — for each screen, create \`${specDir}/screens/<name>.yaml\` with:
|
|
148
|
+
\`\`\`yaml
|
|
149
|
+
screen_name:
|
|
150
|
+
semantic: "Brief description of what this screen does"
|
|
151
|
+
status: stub
|
|
152
|
+
layout:
|
|
153
|
+
type: scroll_vertical
|
|
154
|
+
\`\`\`
|
|
155
|
+
3. **Extract tokens** — scan the codebase for colors, fonts, spacing values and create token files in \`${specDir}/tokens/\`.
|
|
156
|
+
4. **Update the manifest** — fill in \`data_model\` and \`api.endpoints\` in \`${specDir}/openuispec.yaml\` based on the existing code.
|
|
157
|
+
5. **Spec screens on demand** — when the user asks to spec a screen, read the native code, create a full spec, and change \`status: draft\` → \`ready\`.
|
|
225
158
|
|
|
226
159
|
## Screen and flow status
|
|
227
|
-
Screens and flows have a \`status\` field that controls drift tracking:
|
|
228
160
|
- \`stub\` — placeholder, not yet specced. Drift detection skips these.
|
|
229
|
-
- \`draft\` —
|
|
161
|
+
- \`draft\` — actively being specced. Tracked by drift.
|
|
230
162
|
- \`ready\` — fully specified (default if omitted). Tracked by drift.
|
|
231
163
|
|
|
232
|
-
|
|
233
|
-
1.
|
|
234
|
-
2.
|
|
235
|
-
3.
|
|
236
|
-
4.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
-
|
|
164
|
+
## Making UI changes
|
|
165
|
+
1. Read the relevant spec files before modifying any UI code.
|
|
166
|
+
2. If the change requires a spec update, modify the spec FIRST, then update code.
|
|
167
|
+
3. Never modify generated code without updating the spec.
|
|
168
|
+
4. When adding a new screen, create the corresponding spec file.
|
|
169
|
+
5. When removing a screen, remove the spec file and update flow references.
|
|
170
|
+
|
|
171
|
+
## After modifying spec files
|
|
172
|
+
1. Run \`openuispec validate\` to check specs against the schema.
|
|
173
|
+
2. Run \`openuispec drift --snapshot --target <target>\` for each affected platform.
|
|
174
|
+
3. Run \`openuispec drift\` to verify no untracked drift remains.
|
|
175
|
+
|
|
176
|
+
## Spec format reference
|
|
177
|
+
- 7 contract families: nav_container, surface, action_trigger, input_field, data_display, collection, feedback
|
|
178
|
+
- Custom contracts: prefixed with \`x_\` (e.g., \`x_media_player\`)
|
|
179
|
+
- Data binding: \`$data:\`, \`$state:\`, \`$param:\`, \`$t:\` prefixes
|
|
180
|
+
- Actions: typed objects (navigate, api_call, set_state, confirm, sequence, feedback, etc.)
|
|
181
|
+
- Adaptive layout: size classes (compact, regular, expanded) with per-section overrides
|
|
182
|
+
|
|
183
|
+
## CLI commands
|
|
184
|
+
- \`openuispec init\` — scaffold a new spec project
|
|
185
|
+
- \`openuispec validate [group...]\` — validate spec files against schemas
|
|
186
|
+
- \`openuispec drift --target <t>\` — check for spec drift
|
|
187
|
+
- \`openuispec drift --snapshot --target <t>\` — snapshot current state
|
|
188
|
+
- \`openuispec drift --all\` — include stubs in drift check
|
|
253
189
|
`;
|
|
254
190
|
}
|
|
255
191
|
|
|
@@ -261,10 +197,9 @@ export async function init(): Promise<void> {
|
|
|
261
197
|
console.log("\nOpenUISpec — Project Setup\n");
|
|
262
198
|
|
|
263
199
|
try {
|
|
264
|
-
// 1. Project name
|
|
200
|
+
// 1. Project name (display name in manifest, derived from current folder)
|
|
265
201
|
const cwd = process.cwd();
|
|
266
|
-
const defaultName =
|
|
267
|
-
cwd.split("/").pop()?.replace(/[^a-zA-Z0-9]/g, "") || "MyApp";
|
|
202
|
+
const defaultName = cwd.split("/").pop() || "MyApp";
|
|
268
203
|
const name = await ask(rl, "Project name", defaultName);
|
|
269
204
|
|
|
270
205
|
// 2. Spec directory
|
|
@@ -276,7 +211,7 @@ export async function init(): Promise<void> {
|
|
|
276
211
|
rl,
|
|
277
212
|
"\nWhich platforms?",
|
|
278
213
|
allTargets,
|
|
279
|
-
|
|
214
|
+
allTargets
|
|
280
215
|
);
|
|
281
216
|
|
|
282
217
|
if (targets.length === 0) {
|
|
@@ -284,16 +219,6 @@ export async function init(): Promise<void> {
|
|
|
284
219
|
process.exit(1);
|
|
285
220
|
}
|
|
286
221
|
|
|
287
|
-
// 4. Starter tokens?
|
|
288
|
-
const wantTokens = await ask(rl, "Include starter tokens? (y/n)", "y");
|
|
289
|
-
|
|
290
|
-
// 5. AI rules?
|
|
291
|
-
const wantRules = await ask(
|
|
292
|
-
rl,
|
|
293
|
-
"Add rules to CLAUDE.md and AGENTS.md? (y/n)",
|
|
294
|
-
"y"
|
|
295
|
-
);
|
|
296
|
-
|
|
297
222
|
rl.close();
|
|
298
223
|
|
|
299
224
|
// ── create folders ─────────────────────────────────────────────
|
|
@@ -322,27 +247,6 @@ export async function init(): Promise<void> {
|
|
|
322
247
|
manifestTemplate(name, targets, specDir)
|
|
323
248
|
);
|
|
324
249
|
|
|
325
|
-
// ── starter tokens ─────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
if (wantTokens.toLowerCase().startsWith("y")) {
|
|
328
|
-
writeIfMissing(join(root, "tokens", "color.yaml"), starterColorTokens());
|
|
329
|
-
writeIfMissing(
|
|
330
|
-
join(root, "tokens", "typography.yaml"),
|
|
331
|
-
starterTypographyTokens()
|
|
332
|
-
);
|
|
333
|
-
writeIfMissing(
|
|
334
|
-
join(root, "tokens", "spacing.yaml"),
|
|
335
|
-
starterSpacingTokens()
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ── starter locale ─────────────────────────────────────────────
|
|
340
|
-
|
|
341
|
-
writeIfMissing(
|
|
342
|
-
join(root, "locales", "en.json"),
|
|
343
|
-
starterLocale() + "\n"
|
|
344
|
-
);
|
|
345
|
-
|
|
346
250
|
// ── .gitkeep for empty dirs ────────────────────────────────────
|
|
347
251
|
|
|
348
252
|
for (const d of dirs) {
|
|
@@ -361,23 +265,21 @@ export async function init(): Promise<void> {
|
|
|
361
265
|
|
|
362
266
|
// ── AI assistant rules ─────────────────────────────────────────
|
|
363
267
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
appendFileSync(filePath, "\n" + rules);
|
|
376
|
-
console.log(` update ${file} (appended rules)`);
|
|
377
|
-
} else {
|
|
378
|
-
writeFileSync(filePath, rules.trimStart());
|
|
379
|
-
console.log(` create ${file}`);
|
|
268
|
+
const rules = aiRulesBlock(specDir, targets);
|
|
269
|
+
|
|
270
|
+
for (const file of ["CLAUDE.md", "AGENTS.md"]) {
|
|
271
|
+
const filePath = join(cwd, file);
|
|
272
|
+
if (existsSync(filePath)) {
|
|
273
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
274
|
+
if (existing.includes("OpenUISpec")) {
|
|
275
|
+
console.log(` skip ${file} (already has OpenUISpec rules)`);
|
|
276
|
+
continue;
|
|
380
277
|
}
|
|
278
|
+
appendFileSync(filePath, "\n" + rules);
|
|
279
|
+
console.log(` update ${file} (appended rules)`);
|
|
280
|
+
} else {
|
|
281
|
+
writeFileSync(filePath, rules.trimStart());
|
|
282
|
+
console.log(` create ${file}`);
|
|
381
283
|
}
|
|
382
284
|
}
|
|
383
285
|
|
|
@@ -386,14 +288,28 @@ export async function init(): Promise<void> {
|
|
|
386
288
|
console.log(`
|
|
387
289
|
Done! Your spec project is ready at ./${specDir}/
|
|
388
290
|
|
|
389
|
-
|
|
390
|
-
1. Edit ${specDir}/openuispec.yaml
|
|
391
|
-
2.
|
|
392
|
-
3.
|
|
393
|
-
4.
|
|
291
|
+
Getting started (new project):
|
|
292
|
+
1. Edit ${specDir}/openuispec.yaml — define your data model and API
|
|
293
|
+
2. Create screens in ${specDir}/screens/ (one YAML per screen)
|
|
294
|
+
3. Create flows in ${specDir}/flows/ (multi-step navigation)
|
|
295
|
+
4. Ask AI to generate native code from the spec
|
|
394
296
|
5. Run \`openuispec drift --snapshot --target ${targets[0]}\` to baseline
|
|
395
297
|
|
|
396
|
-
|
|
298
|
+
Getting started (existing project):
|
|
299
|
+
1. Ask AI to read your existing UI code and generate spec files:
|
|
300
|
+
"Read src/screens/HomeScreen.swift and create ${specDir}/screens/home.yaml as status: stub"
|
|
301
|
+
2. Spec screens incrementally: stub → draft → ready
|
|
302
|
+
3. Only ready/draft screens are tracked by drift detection
|
|
303
|
+
4. Run \`openuispec validate\` to check specs against the schema
|
|
304
|
+
|
|
305
|
+
Commands:
|
|
306
|
+
openuispec validate Validate spec files
|
|
307
|
+
openuispec drift --target ios Check for spec changes
|
|
308
|
+
openuispec drift --snapshot --target ios Save current state
|
|
309
|
+
|
|
310
|
+
AI rules have been added to CLAUDE.md and AGENTS.md.
|
|
311
|
+
|
|
312
|
+
Learn more: https://github.com/rsktash/openuispec
|
|
397
313
|
`);
|
|
398
314
|
} catch (err) {
|
|
399
315
|
rl.close();
|