openuispec 0.1.14 → 0.1.15

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
@@ -155,6 +155,21 @@ brand:
155
155
 
156
156
  Validate with: `openuispec validate`
157
157
 
158
+ ## Output directories
159
+
160
+ By default, drift stores state in `generated/<target>/<project>/`. To point targets to your actual code directories, add `output_dir` to `openuispec.yaml`:
161
+
162
+ ```yaml
163
+ generation:
164
+ targets: [ios, android, web]
165
+ output_dir:
166
+ web: "../web-ui/"
167
+ android: "../kmp-ui/"
168
+ ios: "../kmp-ui/iosApp/"
169
+ ```
170
+
171
+ Paths are relative to `openuispec.yaml`. The `.openuispec-state.json` file is stored inside each output directory.
172
+
158
173
  ## Spec at a glance
159
174
 
160
175
  | Section | What it defines |
package/cli/init.ts CHANGED
@@ -106,6 +106,10 @@ i18n:
106
106
 
107
107
  generation:
108
108
  targets: [${targetList}]
109
+ # output_dir: # Optional: map targets to code directories
110
+ # ios: "../ios-app/" # relative to this file
111
+ # android: "../android-app/"
112
+ # web: "../web-ui/"
109
113
  output_format:
110
114
  ${outputLines}
111
115
 
@@ -250,10 +254,23 @@ openuispec drift --snapshot --target ${targets[0]} # Snapshot current state
250
254
  openuispec drift --all # Include stubs in drift check
251
255
  \`\`\`
252
256
 
253
- ## Targets
257
+ ## Targets and output directories
254
258
 
255
259
  This project generates native code for: **${targetList}**
256
260
 
261
+ By default, drift stores state in \`generated/<target>/<project>/\`. To point targets to your actual code directories, add \`output_dir\` to \`openuispec.yaml\`:
262
+
263
+ \`\`\`yaml
264
+ generation:
265
+ targets: [ios, android, web]
266
+ output_dir:
267
+ web: "../web-ui/"
268
+ android: "../kmp-ui/"
269
+ ios: "../kmp-ui/iosApp/"
270
+ \`\`\`
271
+
272
+ Paths are relative to \`openuispec.yaml\`.
273
+
257
274
  ## Learn more
258
275
 
259
276
  All docs and examples are in the installed \`openuispec\` package — check \`node_modules/openuispec/\` or run \`npm root -g\` for the global install path.
@@ -377,6 +394,18 @@ Workflow: read the schema → read an example from \`examples/taskflow/\` → cr
377
394
  - Actions: typed objects (navigate, api_call, set_state, confirm, sequence, feedback, etc.)
378
395
  - Adaptive layout: size classes (compact, regular, expanded) with per-section overrides
379
396
 
397
+ ## Output directories
398
+ Drift tracks spec changes per target. By default state is stored in \`generated/<target>/<project>/\`.
399
+ To map targets to actual code directories, set \`generation.output_dir\` in \`openuispec.yaml\`:
400
+ \`\`\`yaml
401
+ generation:
402
+ output_dir:
403
+ web: "../web-ui/"
404
+ android: "../kmp-ui/"
405
+ ios: "../kmp-ui/iosApp/"
406
+ \`\`\`
407
+ Paths are relative to \`openuispec.yaml\`. The \`.openuispec-state.json\` file is stored inside each output directory.
408
+
380
409
  ## CLI commands
381
410
  - \`openuispec init\` — scaffold a new spec project
382
411
  - \`openuispec validate [group...]\` — validate spec files against schemas
package/drift/index.ts CHANGED
@@ -159,27 +159,60 @@ function readProjectName(projectDir: string): string {
159
159
  return doc.project?.name ?? basename(projectDir);
160
160
  }
161
161
 
162
+ /** Read per-target output_dir map from the manifest. */
163
+ function readOutputDirs(projectDir: string): Record<string, string> {
164
+ try {
165
+ const doc = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
166
+ return doc.generation?.output_dir ?? {};
167
+ } catch {
168
+ return {};
169
+ }
170
+ }
171
+
162
172
  /** Resolve the generated output directory for a target. */
163
- function resolveOutputDir(cwd: string, projectName: string, target: string): string {
164
- return resolve(cwd, "generated", target, projectName);
173
+ function resolveOutputDir(projectDir: string, projectName: string, target: string): string {
174
+ const outputDirs = readOutputDirs(projectDir);
175
+ if (outputDirs[target]) {
176
+ return resolve(projectDir, outputDirs[target]);
177
+ }
178
+ // Default: generated/<target>/<project_name> relative to cwd
179
+ return resolve(projectDir, "..", "generated", target, projectName);
165
180
  }
166
181
 
167
- function stateFilePath(cwd: string, projectName: string, target: string): string {
168
- return join(resolveOutputDir(cwd, projectName, target), STATE_FILE);
182
+ function stateFilePath(projectDir: string, projectName: string, target: string): string {
183
+ return join(resolveOutputDir(projectDir, projectName, target), STATE_FILE);
169
184
  }
170
185
 
171
- function discoverTargets(cwd: string, projectName: string): string[] {
172
- const generatedDir = resolve(cwd, "generated");
173
- if (!existsSync(generatedDir)) return [];
174
- try {
175
- return readdirSync(generatedDir)
176
- .filter((entry) =>
177
- existsSync(stateFilePath(cwd, projectName, entry))
178
- )
179
- .sort();
180
- } catch {
181
- return [];
186
+ function discoverTargets(projectDir: string, projectName: string): string[] {
187
+ const outputDirs = readOutputDirs(projectDir);
188
+ const targets: string[] = [];
189
+
190
+ // Check configured output_dir entries
191
+ for (const [target, dir] of Object.entries(outputDirs)) {
192
+ const resolved = resolve(projectDir, dir);
193
+ if (existsSync(join(resolved, STATE_FILE))) {
194
+ targets.push(target);
195
+ }
196
+ }
197
+
198
+ // Also check default generated/ directory
199
+ const generatedDir = resolve(projectDir, "..", "generated");
200
+ if (existsSync(generatedDir)) {
201
+ try {
202
+ for (const entry of readdirSync(generatedDir)) {
203
+ if (!targets.includes(entry)) {
204
+ const defaultPath = join(generatedDir, entry, projectName, STATE_FILE);
205
+ if (existsSync(defaultPath)) {
206
+ targets.push(entry);
207
+ }
208
+ }
209
+ }
210
+ } catch {
211
+ // ignore
212
+ }
182
213
  }
214
+
215
+ return targets.sort();
183
216
  }
184
217
 
185
218
  /**
@@ -197,7 +230,7 @@ function normalizeEntry(value: string | FileEntry): FileEntry {
197
230
 
198
231
  function snapshot(cwd: string, projectDir: string, target: string): void {
199
232
  const projectName = readProjectName(projectDir);
200
- const outDir = resolveOutputDir(cwd, projectName, target);
233
+ const outDir = resolveOutputDir(projectDir, projectName, target);
201
234
  if (!existsSync(outDir)) {
202
235
  console.error(
203
236
  `Error: Output directory not found: ${relative(cwd, outDir)}\n` +
@@ -226,7 +259,7 @@ function snapshot(cwd: string, projectDir: string, target: string): void {
226
259
  files: entries,
227
260
  };
228
261
 
229
- const outPath = stateFilePath(cwd, projectName, target);
262
+ const outPath = stateFilePath(projectDir, projectName, target);
230
263
  writeFileSync(outPath, JSON.stringify(state, null, 2) + "\n");
231
264
  console.log(`Snapshot saved: ${relative(cwd, outPath)}`);
232
265
  console.log(` ${Object.keys(entries).length} files hashed`);
@@ -301,7 +334,7 @@ function check(
301
334
  includeAll: boolean
302
335
  ): void {
303
336
  const projectName = readProjectName(projectDir);
304
- const statePath = stateFilePath(cwd, projectName, target);
337
+ const statePath = stateFilePath(projectDir, projectName, target);
305
338
  if (!existsSync(statePath)) {
306
339
  console.error(
307
340
  `No snapshot found for target "${target}".\n` +
@@ -331,7 +364,7 @@ function checkAll(
331
364
  includeAll: boolean
332
365
  ): void {
333
366
  const projectName = readProjectName(projectDir);
334
- const targets = discoverTargets(cwd, projectName);
367
+ const targets = discoverTargets(projectDir, projectName);
335
368
  if (targets.length === 0) {
336
369
  console.error(
337
370
  "No snapshots found. Run: openuispec drift --snapshot --target <target>"
@@ -342,7 +375,7 @@ function checkAll(
342
375
  let anyDrift = false;
343
376
 
344
377
  for (const target of targets) {
345
- const statePath = stateFilePath(cwd, projectName, target);
378
+ const statePath = stateFilePath(projectDir, projectName, target);
346
379
  const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
347
380
  const result = computeDrift(projectDir, state, includeAll);
348
381
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -104,6 +104,13 @@
104
104
  "type": "string"
105
105
  }
106
106
  },
107
+ "output_dir": {
108
+ "type": "object",
109
+ "description": "Per-target output directory (relative to openuispec.yaml). Defaults to generated/<target>/<project_name> if not set.",
110
+ "additionalProperties": {
111
+ "type": "string"
112
+ }
113
+ },
107
114
  "output_format": {
108
115
  "type": "object",
109
116
  "description": "Per-target generation config",