@sun-asterisk/sungen 3.2.3 → 3.2.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.
Files changed (184) hide show
  1. package/dist/cli/commands/dashboard.js +2 -2
  2. package/dist/cli/commands/dashboard.js.map +1 -1
  3. package/dist/cli/commands/delivery.d.ts.map +1 -1
  4. package/dist/cli/commands/delivery.js +22 -5
  5. package/dist/cli/commands/delivery.js.map +1 -1
  6. package/dist/cli/commands/feedback.d.ts +11 -0
  7. package/dist/cli/commands/feedback.d.ts.map +1 -1
  8. package/dist/cli/commands/feedback.js +154 -57
  9. package/dist/cli/commands/feedback.js.map +1 -1
  10. package/dist/cli/commands/login.d.ts +19 -0
  11. package/dist/cli/commands/login.d.ts.map +1 -0
  12. package/dist/cli/commands/login.js +76 -0
  13. package/dist/cli/commands/login.js.map +1 -0
  14. package/dist/cli/commands/qa-feedback.d.ts +14 -0
  15. package/dist/cli/commands/qa-feedback.d.ts.map +1 -0
  16. package/dist/cli/commands/qa-feedback.js +83 -0
  17. package/dist/cli/commands/qa-feedback.js.map +1 -0
  18. package/dist/cli/index.js +4 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/dashboard/snapshot-builder.d.ts +3 -0
  21. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  22. package/dist/dashboard/snapshot-builder.js +155 -52
  23. package/dist/dashboard/snapshot-builder.js.map +1 -1
  24. package/dist/dashboard/templates/index.html +23 -23
  25. package/dist/dashboard/types.d.ts +8 -0
  26. package/dist/dashboard/types.d.ts.map +1 -1
  27. package/dist/exporters/api-testcase-formatter.d.ts +55 -0
  28. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -0
  29. package/dist/exporters/api-testcase-formatter.js +304 -0
  30. package/dist/exporters/api-testcase-formatter.js.map +1 -0
  31. package/dist/exporters/csv-exporter.d.ts +13 -1
  32. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  33. package/dist/exporters/csv-exporter.js +35 -6
  34. package/dist/exporters/csv-exporter.js.map +1 -1
  35. package/dist/exporters/feature-parser.d.ts +4 -0
  36. package/dist/exporters/feature-parser.d.ts.map +1 -1
  37. package/dist/exporters/feature-parser.js +7 -2
  38. package/dist/exporters/feature-parser.js.map +1 -1
  39. package/dist/exporters/json-exporter.d.ts.map +1 -1
  40. package/dist/exporters/json-exporter.js +11 -3
  41. package/dist/exporters/json-exporter.js.map +1 -1
  42. package/dist/exporters/playwright-report-parser.d.ts +21 -2
  43. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  44. package/dist/exporters/playwright-report-parser.js +52 -12
  45. package/dist/exporters/playwright-report-parser.js.map +1 -1
  46. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  47. package/dist/exporters/scenario-merger.js +50 -2
  48. package/dist/exporters/scenario-merger.js.map +1 -1
  49. package/dist/exporters/spec-parser.js +1 -1
  50. package/dist/exporters/spec-parser.js.map +1 -1
  51. package/dist/exporters/test-data-resolver.d.ts +7 -0
  52. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  53. package/dist/exporters/test-data-resolver.js +13 -0
  54. package/dist/exporters/test-data-resolver.js.map +1 -1
  55. package/dist/exporters/types.d.ts +13 -0
  56. package/dist/exporters/types.d.ts.map +1 -1
  57. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  58. package/dist/exporters/xlsx-exporter.js +131 -42
  59. package/dist/exporters/xlsx-exporter.js.map +1 -1
  60. package/dist/generators/gherkin-parser/index.d.ts +15 -0
  61. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  62. package/dist/generators/gherkin-parser/index.js +33 -1
  63. package/dist/generators/gherkin-parser/index.js.map +1 -1
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
  72. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
  73. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
  74. package/dist/generators/test-generator/patterns/index.d.ts +12 -0
  75. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  76. package/dist/generators/test-generator/patterns/index.js +21 -1
  77. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  78. package/dist/harness/script-check.d.ts.map +1 -1
  79. package/dist/harness/script-check.js +10 -5
  80. package/dist/harness/script-check.js.map +1 -1
  81. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  82. package/dist/orchestrator/ai-skills/writer.js +46 -4
  83. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  84. package/dist/orchestrator/project-initializer.d.ts +14 -0
  85. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  86. package/dist/orchestrator/project-initializer.js +58 -0
  87. package/dist/orchestrator/project-initializer.js.map +1 -1
  88. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
  89. package/dist/orchestrator/templates/ai-src/commands/create-test.md +7 -0
  90. package/dist/orchestrator/templates/ai-src/commands/feedback.md +6 -6
  91. package/dist/orchestrator/templates/ai-src/commands/locale.md +26 -4
  92. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
  93. package/dist/orchestrator/templates/wdio.conf.ts +39 -3
  94. package/dist/tools/sungen-api/edit-in-editor.d.ts +15 -0
  95. package/dist/tools/sungen-api/edit-in-editor.d.ts.map +1 -0
  96. package/dist/tools/sungen-api/edit-in-editor.js +85 -0
  97. package/dist/tools/sungen-api/edit-in-editor.js.map +1 -0
  98. package/dist/tools/sungen-api/feedback-client.d.ts +24 -0
  99. package/dist/tools/sungen-api/feedback-client.d.ts.map +1 -0
  100. package/dist/tools/sungen-api/feedback-client.js +53 -0
  101. package/dist/tools/sungen-api/feedback-client.js.map +1 -0
  102. package/dist/tools/sungen-api/read-stdin.d.ts +6 -0
  103. package/dist/tools/sungen-api/read-stdin.d.ts.map +1 -0
  104. package/dist/tools/sungen-api/read-stdin.js +19 -0
  105. package/dist/tools/sungen-api/read-stdin.js.map +1 -0
  106. package/dist/tools/sungen-auth/api-base-url.d.ts +14 -0
  107. package/dist/tools/sungen-auth/api-base-url.d.ts.map +1 -0
  108. package/dist/tools/sungen-auth/api-base-url.js +28 -0
  109. package/dist/tools/sungen-auth/api-base-url.js.map +1 -0
  110. package/dist/tools/sungen-auth/credentials-store.d.ts +19 -0
  111. package/dist/tools/sungen-auth/credentials-store.d.ts.map +1 -0
  112. package/dist/tools/sungen-auth/credentials-store.js +94 -0
  113. package/dist/tools/sungen-auth/credentials-store.js.map +1 -0
  114. package/dist/tools/sungen-auth/login-flow.d.ts +33 -0
  115. package/dist/tools/sungen-auth/login-flow.d.ts.map +1 -0
  116. package/dist/tools/sungen-auth/login-flow.js +107 -0
  117. package/dist/tools/sungen-auth/login-flow.js.map +1 -0
  118. package/dist/tools/sungen-auth/loopback-server.d.ts +20 -0
  119. package/dist/tools/sungen-auth/loopback-server.d.ts.map +1 -0
  120. package/dist/tools/sungen-auth/loopback-server.js +132 -0
  121. package/dist/tools/sungen-auth/loopback-server.js.map +1 -0
  122. package/dist/tools/sungen-auth/open-browser.d.ts +10 -0
  123. package/dist/tools/sungen-auth/open-browser.d.ts.map +1 -0
  124. package/dist/tools/sungen-auth/open-browser.js +43 -0
  125. package/dist/tools/sungen-auth/open-browser.js.map +1 -0
  126. package/dist/tools/sungen-auth/pkce.d.ts +13 -0
  127. package/dist/tools/sungen-auth/pkce.d.ts.map +1 -0
  128. package/dist/tools/sungen-auth/pkce.js +27 -0
  129. package/dist/tools/sungen-auth/pkce.js.map +1 -0
  130. package/dist/tools/sungen-auth/sungen-auth-types.d.ts +41 -0
  131. package/dist/tools/sungen-auth/sungen-auth-types.d.ts.map +1 -0
  132. package/dist/tools/sungen-auth/sungen-auth-types.js +7 -0
  133. package/dist/tools/sungen-auth/sungen-auth-types.js.map +1 -0
  134. package/package.json +4 -3
  135. package/src/cli/commands/dashboard.ts +2 -2
  136. package/src/cli/commands/delivery.ts +24 -6
  137. package/src/cli/commands/feedback.ts +123 -51
  138. package/src/cli/commands/login.ts +75 -0
  139. package/src/cli/commands/qa-feedback.ts +77 -0
  140. package/src/cli/index.ts +4 -0
  141. package/src/dashboard/snapshot-builder.ts +146 -46
  142. package/src/dashboard/templates/index.html +23 -23
  143. package/src/dashboard/types.ts +10 -2
  144. package/src/exporters/api-testcase-formatter.ts +325 -0
  145. package/src/exporters/csv-exporter.ts +46 -7
  146. package/src/exporters/feature-parser.ts +7 -2
  147. package/src/exporters/json-exporter.ts +11 -3
  148. package/src/exporters/playwright-report-parser.ts +50 -11
  149. package/src/exporters/scenario-merger.ts +49 -3
  150. package/src/exporters/spec-parser.ts +1 -1
  151. package/src/exporters/test-data-resolver.ts +13 -0
  152. package/src/exporters/types.ts +13 -0
  153. package/src/exporters/xlsx-exporter.ts +155 -49
  154. package/src/generators/gherkin-parser/index.ts +44 -1
  155. package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
  156. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
  157. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  158. package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
  159. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
  160. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
  161. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
  162. package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
  163. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
  164. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
  165. package/src/generators/test-generator/patterns/index.ts +21 -1
  166. package/src/harness/script-check.ts +10 -5
  167. package/src/orchestrator/ai-skills/writer.ts +39 -4
  168. package/src/orchestrator/project-initializer.ts +59 -0
  169. package/src/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
  170. package/src/orchestrator/templates/ai-src/commands/create-test.md +7 -0
  171. package/src/orchestrator/templates/ai-src/commands/feedback.md +6 -6
  172. package/src/orchestrator/templates/ai-src/commands/locale.md +26 -4
  173. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
  174. package/src/orchestrator/templates/wdio.conf.ts +39 -3
  175. package/src/tools/sungen-api/edit-in-editor.ts +48 -0
  176. package/src/tools/sungen-api/feedback-client.ts +62 -0
  177. package/src/tools/sungen-api/read-stdin.ts +15 -0
  178. package/src/tools/sungen-auth/api-base-url.ts +23 -0
  179. package/src/tools/sungen-auth/credentials-store.ts +57 -0
  180. package/src/tools/sungen-auth/login-flow.ts +135 -0
  181. package/src/tools/sungen-auth/loopback-server.ts +113 -0
  182. package/src/tools/sungen-auth/open-browser.ts +42 -0
  183. package/src/tools/sungen-auth/pkce.ts +27 -0
  184. package/src/tools/sungen-auth/sungen-auth-types.ts +45 -0
@@ -44,6 +44,9 @@ const DEFAULT_BASE_LOCALE = 'vi';
44
44
 
45
45
  export interface DashboardTarget {
46
46
  name: string;
47
+ kind: 'screen' | 'flow' | 'api';
48
+ /** Derived convenience (`kind === 'flow'`) — kept for the flow-specific
49
+ * branches that predate the api tier. */
47
50
  isFlow: boolean;
48
51
  }
49
52
 
@@ -85,6 +88,23 @@ export function buildDashboardSnapshot(options: BuildSnapshotOptions): Dashboard
85
88
  // Per-target build
86
89
  // ----------------------------------------------------------------------------
87
90
 
91
+ /**
92
+ * True when this is a mobile project (`platform: mobile` in qa/capabilities.yaml).
93
+ * Mobile projects run per-OS (android/ios) — the dashboard surfaces those on a
94
+ * distinct platform axis (one FeatureSnapshot per OS) rather than as locale tabs.
95
+ * Mirrors `isMobileProject` in src/cli/commands/delivery.ts.
96
+ */
97
+ function isMobileProject(cwd: string): boolean {
98
+ try {
99
+ const p = path.join(cwd, 'qa', 'capabilities.yaml');
100
+ if (!fs.existsSync(p)) return false;
101
+ const parsed = parseYaml(fs.readFileSync(p, 'utf-8')) as { platform?: string } | null;
102
+ return parsed?.platform === 'mobile';
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
88
108
  function buildOneScreen(
89
109
  cwd: string,
90
110
  target: DashboardTarget,
@@ -105,28 +125,49 @@ function buildOneScreen(
105
125
  const specMdFile = path.join(base, 'requirements', 'spec.md');
106
126
  const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : undefined;
107
127
 
128
+ const mobile = isMobileProject(cwd);
108
129
  const features: FeatureSnapshot[] = [];
109
130
  for (const featureBaseName of featureBaseNames) {
110
- const fs2 = buildOneFeature(cwd, target, featureBaseName, env, specLink);
111
- if (fs2) features.push(fs2);
131
+ features.push(...buildOneFeature(cwd, target, featureBaseName, env, specLink, mobile));
112
132
  }
113
133
  if (features.length === 0) return null;
114
134
 
115
135
  // Flat scenarios across all features (each scenario already carries its
116
136
  // own `featureBaseName`, so consumers that want to group can do so cheaply).
137
+ // For mobile, per-OS features flatten here so the Compare view can key each
138
+ // scenario by (tcId, platform) via its OS-precise testEnvironment.
117
139
  const scenarios = features.flatMap((f) => f.scenarios);
118
- const label = target.isFlow ? `flow/${target.name}` : target.name;
119
- const primary = features[0];
140
+ const label = target.kind === 'flow' ? `flow/${target.name}`
141
+ : target.kind === 'api' ? `api/${target.name}`
142
+ : target.name;
143
+
144
+ // Screen-level platform axis: 'mobile' when any feature is OS-tagged; the
145
+ // union of OSes that actually produced a result file this run.
146
+ const execSet = new Set<'android' | 'ios'>();
147
+ let anyPlatform = false;
148
+ for (const f of features) {
149
+ if (f.platform) anyPlatform = true;
150
+ for (const p of f.executedPlatforms ?? []) execSet.add(p);
151
+ }
152
+ const executedPlatforms = execSet.size > 0
153
+ ? (['android', 'ios'] as const).filter((p) => execSet.has(p))
154
+ : undefined;
155
+ // Representative feature for the back-compat top-level fields: prefer Android
156
+ // (deterministic, matches delivery's summary-row preference).
157
+ const primary = features.find((f) => f.platform === 'android') ?? features[0];
120
158
 
121
159
  return {
122
160
  name: label,
123
161
  isFlow: target.isFlow,
162
+ kind: target.kind,
163
+ platform: anyPlatform ? 'mobile' : undefined,
124
164
  featureName: primary.featureName,
125
165
  featurePath: primary.featurePath,
126
166
  specLink,
127
167
  summary: rollupScreenSummary(features),
128
168
  scenarios,
129
169
  features,
170
+ executedPlatforms,
130
171
  };
131
172
  }
132
173
 
@@ -141,14 +182,15 @@ function buildOneFeature(
141
182
  featureBaseName: string,
142
183
  env: EnvironmentInfo,
143
184
  specLink: string | undefined,
144
- ): FeatureSnapshot | null {
185
+ mobile: boolean,
186
+ ): FeatureSnapshot[] {
145
187
  const base = qaDir(cwd, target);
146
188
  const genBase = generatedDir(cwd, target);
147
189
  const featureFile = path.join(base, 'features', `${featureBaseName}.feature`);
148
190
  const testDataFile = path.join(base, 'test-data', `${featureBaseName}.yaml`);
149
191
  const specFile = path.join(genBase, `${featureBaseName}.spec.ts`);
150
192
 
151
- if (!fs.existsSync(featureFile)) return null;
193
+ if (!fs.existsSync(featureFile)) return [];
152
194
 
153
195
  const feature = parseFeatureMetadata(featureFile);
154
196
  const spec = fs.existsSync(specFile)
@@ -168,27 +210,16 @@ function buildOneFeature(
168
210
  // Discover locale variants by scanning <feature>-test-result*.json files.
169
211
  // Always includes base ('') as the first entry, even if its results file
170
212
  // is missing — UI still needs to render the feature.
171
- // NOTE (mobile): for a `platform: mobile` project the "locale" variants are
172
- // actually the per-OS runs (android / ios) — the dashboard reuses the locale
173
- // switcher to surface them as tabs. The env column per tab is OS-precise (see
174
- // resolvePlatformAppId below), but the top-level featurePath (base variant)
175
- // defaults to the Android id. Delivery has a dedicated per-OS path; aligning the
176
- // dashboard onto a distinct "platform" axis is a possible follow-up.
177
213
  const variants = discoverLocaleVariants(genBase, featureBaseName);
178
214
 
179
- // Build per-locale ScreenSnapshot, then collect into FeatureSnapshot.locales[].
180
- // Base variant is also used to populate the top-level (`scenarios`, `summary`)
181
- // for back-compat with consumers that don't read the locales array.
182
- const locales: LocaleSnapshot[] = [];
183
- let baseBuilt: { scenarios: ScenarioSnapshot[]; summary: ScreenSummaryStats; featureName: string; featurePath?: string; specLink?: string } | null = null;
184
-
185
- for (const variant of variants) {
215
+ // Build one per-variant ScreenSnapshot (test-data overlay + results + OS-precise
216
+ // env column). Reused by both the web/i18n (locale-axis) and mobile (platform-axis) paths.
217
+ const buildVariant = (variant: FeatureLocaleVariant) => {
186
218
  const testData = fs.existsSync(testDataFile)
187
219
  ? loadTestData(testDataFile, variant.locale || null)
188
220
  : {};
189
221
  const results = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
190
-
191
- const built = buildScreenSnapshot({
222
+ return buildScreenSnapshot({
192
223
  screen: featureBaseName,
193
224
  isFlow: target.isFlow,
194
225
  featureName: feature.featureName,
@@ -203,17 +234,65 @@ function buildOneFeature(
203
234
  featureBaseName,
204
235
  selectorKeyMap,
205
236
  });
237
+ };
238
+ type Built = ReturnType<typeof buildVariant>;
239
+
240
+ // -- Mobile (platform axis): the per-OS runs (android / ios) are their own
241
+ // FeatureSnapshot entries — one per OS, `platform` tagged — NOT locale tabs.
242
+ // Locale overlays for a given OS (e.g. `android.ja`) nest inside that OS's
243
+ // entry as `locales[]`. This mirrors the delivery per-OS deliverables. --
244
+ if (mobile) {
245
+ const osOf = (loc: string): 'android' | 'ios' | null =>
246
+ /^android(\.|$)/.test(loc) ? 'android' : /^ios(\.|$)/.test(loc) ? 'ios' : null;
247
+ const osVariants = variants.filter((v) => osOf(v.locale));
248
+ if (osVariants.length > 0) {
249
+ const executedPlatforms = (['android', 'ios'] as const).filter((os) =>
250
+ osVariants.some((v) => osOf(v.locale) === os && v.resultsPath)
251
+ );
252
+ const feats: FeatureSnapshot[] = [];
253
+ for (const os of ['android', 'ios'] as const) {
254
+ const group = osVariants.filter((v) => osOf(v.locale) === os);
255
+ if (group.length === 0) continue;
256
+ const localeSnaps: LocaleSnapshot[] = [];
257
+ let primaryBuilt: Built | null = null;
258
+ for (const v of group) {
259
+ const built = buildVariant(v);
260
+ localeSnaps.push({ locale: v.locale, displayCode: v.displayCode, summary: built.summary, scenarios: built.scenarios });
261
+ // Representative = the bare OS run (locale === os); else the first overlay.
262
+ if (v.locale === os || !primaryBuilt) primaryBuilt = built;
263
+ }
264
+ if (!primaryBuilt) continue;
265
+ feats.push({
266
+ featureBaseName,
267
+ featureName: primaryBuilt.featureName,
268
+ featurePath: primaryBuilt.featurePath,
269
+ specLink: primaryBuilt.specLink,
270
+ summary: primaryBuilt.summary,
271
+ scenarios: primaryBuilt.scenarios,
272
+ platform: os,
273
+ executedPlatforms,
274
+ locales: localeSnaps.length >= 2 ? localeSnaps : undefined,
275
+ });
276
+ }
277
+ if (feats.length > 0) return feats;
278
+ }
279
+ // Mobile project but no per-OS result files yet → fall through to the base
280
+ // (locale-axis) path so the test CASES still render.
281
+ }
282
+
283
+ // -- Web / i18n (locale axis): one FeatureSnapshot; variants become locales[]. --
284
+ const locales: LocaleSnapshot[] = [];
285
+ let baseBuilt: Built | { scenarios: ScenarioSnapshot[]; summary: ScreenSummaryStats; featureName: string; featurePath?: string; specLink?: string } | null = null;
206
286
 
287
+ for (const variant of variants) {
288
+ const built = buildVariant(variant);
207
289
  locales.push({
208
290
  locale: variant.locale,
209
291
  displayCode: variant.displayCode,
210
292
  summary: built.summary,
211
293
  scenarios: built.scenarios,
212
294
  });
213
-
214
- if (variant.locale === '') {
215
- baseBuilt = built;
216
- }
295
+ if (variant.locale === '') baseBuilt = built;
217
296
  }
218
297
 
219
298
  // Guarantee a base entry — every feature in the dashboard must have
@@ -227,9 +306,9 @@ function buildOneFeature(
227
306
  specLink,
228
307
  };
229
308
  }
230
- if (!baseBuilt) return null;
309
+ if (!baseBuilt) return [];
231
310
 
232
- return {
311
+ return [{
233
312
  featureBaseName,
234
313
  featureName: baseBuilt.featureName,
235
314
  featurePath: baseBuilt.featurePath,
@@ -239,7 +318,7 @@ function buildOneFeature(
239
318
  // Only expose `locales` array when 2+ variants exist — single-locale
240
319
  // features stay slimmer and UI can skip the locale tab UI entirely.
241
320
  locales: locales.length >= 2 ? locales : undefined,
242
- };
321
+ }];
243
322
  }
244
323
 
245
324
  /**
@@ -361,14 +440,24 @@ function bumpKey(record: Record<string, number>, key: string): void {
361
440
  // Path helpers (mirror delivery.ts to stay consistent)
362
441
  // ----------------------------------------------------------------------------
363
442
 
443
+ /** qa/ subfolder for a unit kind (mirrors delivery.ts's qaParent). */
444
+ function qaParent(kind: DashboardTarget['kind']): string {
445
+ return kind === 'flow' ? 'flows' : kind === 'api' ? 'api' : 'screens';
446
+ }
447
+
364
448
  function qaDir(cwd: string, target: DashboardTarget): string {
365
- return path.join(cwd, 'qa', target.isFlow ? 'flows' : 'screens', target.name);
449
+ return path.join(cwd, 'qa', qaParent(target.kind), target.name);
366
450
  }
367
451
 
368
452
  function generatedDir(cwd: string, target: DashboardTarget): string {
369
- return target.isFlow
370
- ? path.join(cwd, 'specs', 'generated', 'flows', target.name)
371
- : path.join(cwd, 'specs', 'generated', target.name);
453
+ // Mirrors delivery.ts: screen → specs/generated/<name>, flow → …/flows/<name>,
454
+ // api → …/api/<name> (api flows carry name 'flows/<flow>', composing correctly).
455
+ const sub = target.kind === 'flow'
456
+ ? path.join('flows', target.name)
457
+ : target.kind === 'api'
458
+ ? path.join('api', target.name)
459
+ : target.name;
460
+ return path.join(cwd, 'specs', 'generated', sub);
372
461
  }
373
462
 
374
463
  // ----------------------------------------------------------------------------
@@ -378,17 +467,23 @@ function generatedDir(cwd: string, target: DashboardTarget): string {
378
467
  export function listDashboardTargets(cwd: string): DashboardTarget[] {
379
468
  const targets: DashboardTarget[] = [];
380
469
 
381
- const screensDir = path.join(cwd, 'qa', 'screens');
382
- if (fs.existsSync(screensDir)) {
383
- for (const d of fs.readdirSync(screensDir, { withFileTypes: true })) {
384
- if (d.isDirectory()) targets.push({ name: d.name, isFlow: false });
470
+ const scan = (kind: DashboardTarget['kind'], skip: (n: string) => boolean = () => false): void => {
471
+ const dir = path.join(cwd, 'qa', qaParent(kind));
472
+ if (!fs.existsSync(dir)) return;
473
+ for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
474
+ if (d.isDirectory() && !skip(d.name)) targets.push({ name: d.name, kind, isFlow: kind === 'flow' });
385
475
  }
386
- }
387
-
388
- const flowsDir = path.join(cwd, 'qa', 'flows');
389
- if (fs.existsSync(flowsDir)) {
390
- for (const d of fs.readdirSync(flowsDir, { withFileTypes: true })) {
391
- if (d.isDirectory()) targets.push({ name: d.name, isFlow: true });
476
+ };
477
+ scan('screen');
478
+ scan('flow');
479
+ scan('api', (n) => n === 'flows'); // api areas: qa/api/<area> (the flows/ container is a nested tier)
480
+
481
+ // api flows: qa/api/flows/<flow> → name 'flows/<flow>', kind 'api'
482
+ // (paths compose to qa/api/flows/… + specs/generated/api/flows/…), mirroring delivery.ts.
483
+ const apiFlowsRoot = path.join(cwd, 'qa', 'api', 'flows');
484
+ if (fs.existsSync(apiFlowsRoot)) {
485
+ for (const d of fs.readdirSync(apiFlowsRoot, { withFileTypes: true })) {
486
+ if (d.isDirectory()) targets.push({ name: path.posix.join('flows', d.name), kind: 'api', isFlow: false });
392
487
  }
393
488
  }
394
489
 
@@ -396,10 +491,15 @@ export function listDashboardTargets(cwd: string): DashboardTarget[] {
396
491
  }
397
492
 
398
493
  export function resolveTargetType(cwd: string, name: string): DashboardTarget {
399
- if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) {
400
- return { name, isFlow: true };
401
- }
402
- return { name, isFlow: false };
494
+ if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) return { name, kind: 'flow', isFlow: true };
495
+ // Screen before api so `dashboard <name>` resolves the same unit as `delivery <name>`
496
+ // when a repo has both qa/screens/<name> and qa/api/<name> (delivery.ts precedence).
497
+ if (fs.existsSync(path.join(cwd, 'qa', 'screens', name))) return { name, kind: 'screen', isFlow: false };
498
+ if (fs.existsSync(path.join(cwd, 'qa', 'api', name))) return { name, kind: 'api', isFlow: false };
499
+ // `flows/<flow>` or a bare api-flow name → qa/api/flows/<flow>
500
+ const apiFlow = name.startsWith('flows/') ? name : path.posix.join('flows', name);
501
+ if (fs.existsSync(path.join(cwd, 'qa', 'api', apiFlow))) return { name: apiFlow, kind: 'api', isFlow: false };
502
+ return { name, kind: 'screen', isFlow: false };
403
503
  }
404
504
 
405
505
  // ----------------------------------------------------------------------------