@sun-asterisk/sungen 3.2.2-beta.11 → 3.2.2-beta.13

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 (146) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +26 -3
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/cli/commands/add-flow.d.ts.map +1 -1
  5. package/dist/cli/commands/add-flow.js +2 -0
  6. package/dist/cli/commands/add-flow.js.map +1 -1
  7. package/dist/cli/commands/add.d.ts.map +1 -1
  8. package/dist/cli/commands/add.js +4 -0
  9. package/dist/cli/commands/add.js.map +1 -1
  10. package/dist/cli/commands/capability.d.ts.map +1 -1
  11. package/dist/cli/commands/capability.js +61 -0
  12. package/dist/cli/commands/capability.js.map +1 -1
  13. package/dist/cli/commands/delivery.d.ts.map +1 -1
  14. package/dist/cli/commands/delivery.js +89 -2
  15. package/dist/cli/commands/delivery.js.map +1 -1
  16. package/dist/cli/commands/generate.d.ts.map +1 -1
  17. package/dist/cli/commands/generate.js +2 -1
  18. package/dist/cli/commands/generate.js.map +1 -1
  19. package/dist/cli/commands/init.d.ts.map +1 -1
  20. package/dist/cli/commands/init.js +27 -2
  21. package/dist/cli/commands/init.js.map +1 -1
  22. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  23. package/dist/dashboard/snapshot-builder.js +10 -1
  24. package/dist/dashboard/snapshot-builder.js.map +1 -1
  25. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  26. package/dist/exporters/csv-exporter.js +3 -1
  27. package/dist/exporters/csv-exporter.js.map +1 -1
  28. package/dist/exporters/json-exporter.js +3 -1
  29. package/dist/exporters/json-exporter.js.map +1 -1
  30. package/dist/exporters/mobile-app-id.d.ts +22 -0
  31. package/dist/exporters/mobile-app-id.d.ts.map +1 -0
  32. package/dist/exporters/mobile-app-id.js +37 -0
  33. package/dist/exporters/mobile-app-id.js.map +1 -0
  34. package/dist/exporters/spec-parser.d.ts.map +1 -1
  35. package/dist/exporters/spec-parser.js +6 -2
  36. package/dist/exporters/spec-parser.js.map +1 -1
  37. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +3 -0
  38. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  39. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  40. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  41. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  42. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  43. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  44. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  45. package/dist/generators/test-generator/code-generator.js +38 -3
  46. package/dist/generators/test-generator/code-generator.js.map +1 -1
  47. package/dist/generators/test-generator/template-engine.d.ts +3 -0
  48. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  49. package/dist/generators/test-generator/template-engine.js +11 -1
  50. package/dist/generators/test-generator/template-engine.js.map +1 -1
  51. package/dist/generators/test-generator/utils/selector-resolver.d.ts +11 -1
  52. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  53. package/dist/generators/test-generator/utils/selector-resolver.js +33 -0
  54. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  55. package/dist/harness/audit.js +1 -1
  56. package/dist/harness/audit.js.map +1 -1
  57. package/dist/harness/catalog/universal-viewpoints.yaml +15 -0
  58. package/dist/harness/journey.d.ts.map +1 -1
  59. package/dist/harness/journey.js +22 -11
  60. package/dist/harness/journey.js.map +1 -1
  61. package/dist/harness/sensors.d.ts +1 -1
  62. package/dist/harness/sensors.d.ts.map +1 -1
  63. package/dist/harness/sensors.js +32 -9
  64. package/dist/harness/sensors.js.map +1 -1
  65. package/dist/orchestrator/flow-manager.d.ts +11 -0
  66. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  67. package/dist/orchestrator/flow-manager.js +147 -29
  68. package/dist/orchestrator/flow-manager.js.map +1 -1
  69. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +59 -0
  70. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -0
  71. package/dist/orchestrator/mobile-runtime-scaffolder.js +298 -0
  72. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -0
  73. package/dist/orchestrator/project-initializer.d.ts +25 -7
  74. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  75. package/dist/orchestrator/project-initializer.js +115 -62
  76. package/dist/orchestrator/project-initializer.js.map +1 -1
  77. package/dist/orchestrator/screen-manager.d.ts +30 -0
  78. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  79. package/dist/orchestrator/screen-manager.js +205 -22
  80. package/dist/orchestrator/screen-manager.js.map +1 -1
  81. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
  82. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
  83. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -1
  84. package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
  85. package/dist/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
  86. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +7 -0
  87. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
  88. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
  89. package/dist/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
  90. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
  91. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
  92. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +8 -1
  93. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
  94. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
  95. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +7 -0
  96. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
  97. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
  98. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
  99. package/package.json +3 -3
  100. package/src/capabilities/discover.ts +29 -3
  101. package/src/cli/commands/add-flow.ts +2 -0
  102. package/src/cli/commands/add.ts +4 -0
  103. package/src/cli/commands/capability.ts +64 -1
  104. package/src/cli/commands/delivery.ts +89 -2
  105. package/src/cli/commands/generate.ts +2 -1
  106. package/src/cli/commands/init.ts +38 -3
  107. package/src/dashboard/snapshot-builder.ts +10 -1
  108. package/src/exporters/csv-exporter.ts +3 -1
  109. package/src/exporters/json-exporter.ts +3 -1
  110. package/src/exporters/mobile-app-id.ts +34 -0
  111. package/src/exporters/spec-parser.ts +6 -2
  112. package/src/generators/test-generator/adapters/adapter-interface.ts +3 -0
  113. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  114. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  115. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  116. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  117. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  118. package/src/generators/test-generator/code-generator.ts +39 -3
  119. package/src/generators/test-generator/template-engine.ts +14 -1
  120. package/src/generators/test-generator/utils/selector-resolver.ts +51 -1
  121. package/src/harness/audit.ts +1 -1
  122. package/src/harness/catalog/universal-viewpoints.yaml +15 -0
  123. package/src/harness/journey.ts +21 -9
  124. package/src/harness/sensors.ts +25 -8
  125. package/src/orchestrator/flow-manager.ts +165 -29
  126. package/src/orchestrator/mobile-runtime-scaffolder.ts +277 -0
  127. package/src/orchestrator/project-initializer.ts +127 -64
  128. package/src/orchestrator/screen-manager.ts +237 -22
  129. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
  130. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
  131. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -1
  132. package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
  133. package/src/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
  134. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +7 -0
  135. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
  136. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
  137. package/src/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
  138. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
  139. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
  140. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +8 -1
  141. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
  142. package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
  143. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +7 -0
  144. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
  145. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
  146. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
@@ -7,16 +7,31 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { execSync } from 'child_process';
9
9
  import { AI_RULES_FILE_MAPPING } from './ai-rules-updater';
10
+ import {
11
+ MobileInitOptions,
12
+ emitMobileRuntime,
13
+ writeMobileCapabilities,
14
+ installMobileDriver,
15
+ } from './mobile-runtime-scaffolder';
16
+
17
+ export { MobileInitOptions };
10
18
 
11
19
  export class ProjectInitializer {
12
20
  private baseCwd: string;
13
21
  private cwd: string;
14
22
  private createdItems: string[] = [];
15
23
  private skippedItems: string[] = [];
24
+ private mobile: MobileInitOptions;
16
25
 
17
- constructor(private baseUrl: string) {
26
+ constructor(private baseUrl: string, mobile?: MobileInitOptions) {
18
27
  this.baseCwd = process.cwd();
19
28
  this.cwd = this.baseCwd;
29
+ this.mobile = mobile ?? { enabled: false };
30
+ }
31
+
32
+ /** Mobile-only project: no Playwright/web scaffolding. */
33
+ private get mobileOnly(): boolean {
34
+ return !!this.mobile.mobileOnly;
20
35
  }
21
36
 
22
37
  /**
@@ -30,34 +45,41 @@ export class ProjectInitializer {
30
45
  this.createdItems = [];
31
46
  this.skippedItems = [];
32
47
 
33
- console.log('🚀 Initializing Sungen project...\n');
48
+ console.log(`🚀 Initializing Sungen ${this.mobileOnly ? 'mobile-only ' : ''}project...\n`);
34
49
 
35
50
  if (normalizedProjectName) {
36
51
  this.createProjectRoot(normalizedProjectName);
37
52
  }
38
53
 
39
- // Create directories
54
+ // Create directories (mobile-only skips the web Playwright storage dir)
40
55
  this.createDirectories();
41
56
 
42
57
  // Create qa/context.md for QA lead to fill project-wide context
43
58
  this.createContext();
44
59
 
45
- // Create qa/capabilities.yaml — the platform driver profile (default: web/Playwright)
60
+ // Create qa/capabilities.yaml — web default; mobile path uses writeMobileCapabilities
46
61
  this.createCapabilities();
47
62
 
48
- // Ensure package.json and install Playwright
63
+ // Ensure package.json and install deps (Playwright for web; skipped for mobile-only)
49
64
  await this.setupDependencies();
50
65
 
51
- // Create playwright config if doesn't exist
52
- this.createPlaywrightConfig();
66
+ // Web-only scaffolding skipped entirely for a mobile-only project
67
+ if (!this.mobileOnly) {
68
+ this.createPlaywrightConfig();
69
+ this.createSpecsBase();
70
+ }
53
71
 
54
- // Create tsconfig.json if doesn't exist
72
+ // Create tsconfig.json if doesn't exist (needed by both web + mobile)
55
73
  this.createTsConfig();
56
74
 
57
- // Create specs/base.ts for shared context and specs/test-data.ts for runtime data
58
- this.createSpecsBase();
75
+ // specs/test-data.ts (runtime YAML loader) — shared by BOTH web and mobile specs
59
76
  this.createSpecsTestData();
60
77
 
78
+ // Mobile harness (Appium/WebdriverIO) — when `--mobile` (a mobile-only project)
79
+ if (this.mobile.enabled) {
80
+ this.emitMobileHarness();
81
+ }
82
+
61
83
  // Create/update .gitignore
62
84
  this.updateGitignore();
63
85
 
@@ -90,17 +112,14 @@ export class ProjectInitializer {
90
112
  }
91
113
 
92
114
  /**
93
- * Create essential directories only
115
+ * Create essential directories only.
116
+ * Mobile-only: skips specs/storage (Playwright auth state); qa/screens + qa/flows still created
117
+ * (1-platform model — mobile uses the same tree as web, NOT a separate qa/mobile/ path).
94
118
  */
95
119
  private createDirectories(): void {
96
- const dirs = [
97
- 'qa',
98
- 'qa/screens',
99
- 'qa/flows',
100
- 'specs',
101
- 'specs/generated',
102
- 'specs/storage',
103
- ];
120
+ const dirs = this.mobileOnly
121
+ ? ['qa', 'qa/screens', 'qa/flows', 'specs', 'specs/generated']
122
+ : ['qa', 'qa/screens', 'qa/flows', 'specs', 'specs/generated', 'specs/storage'];
104
123
 
105
124
  for (const dir of dirs) {
106
125
  const fullPath = path.join(this.cwd, dir);
@@ -147,13 +166,17 @@ export class ProjectInitializer {
147
166
  }
148
167
 
149
168
  /**
150
- * Update .gitignore with Sungen-specific entries
169
+ * Update .gitignore with Sungen-specific entries.
170
+ * Mobile projects add .env.appium (device ids + machine paths) and specs/.auth-mobile/
171
+ * (Appium auth contract). Web projects add Playwright auth state + report dirs.
151
172
  */
152
173
  private updateGitignore(): void {
153
174
  const gitignorePath = path.join(this.cwd, '.gitignore');
154
175
  const sungenEntries = [
155
176
  '# Sungen generated files',
156
177
  'specs/.auth/', // session storage state = cookies/tokens (sensitive)
178
+ 'specs/.auth-mobile/', // mobile auth contract = credentials (sensitive)
179
+ '.env.appium', // local mobile-test env = machine paths + device/team ids (.example IS committed)
157
180
  'test-results/',
158
181
  'playwright-report/',
159
182
  '.playwright-mcp/',
@@ -199,7 +222,8 @@ export class ProjectInitializer {
199
222
  }
200
223
 
201
224
  /**
202
- * Create VS Code settings for Copilot auto-attach
225
+ * Create VS Code settings for Copilot auto-attach.
226
+ * Mobile-only: approves wdio + npx appium instead of npx playwright.
203
227
  */
204
228
  private createVSCodeSettings(): void {
205
229
  const settingsPath = path.join(this.cwd, '.vscode', 'settings.json');
@@ -214,7 +238,9 @@ export class ProjectInitializer {
214
238
  'github.copilot.chat.agent.runTasks': true,
215
239
  'chat.tools.terminal.autoApprove': {
216
240
  sungen: true,
217
- 'npx playwright': true,
241
+ ...(this.mobileOnly
242
+ ? { wdio: true, 'npx appium': true }
243
+ : { 'npx playwright': true }),
218
244
  },
219
245
  };
220
246
 
@@ -224,15 +250,14 @@ export class ProjectInitializer {
224
250
  this.skippedItems.push('.vscode/settings.json');
225
251
  }
226
252
 
227
- // .vscode/mcp.json (Copilot format) — safe-merge Playwright + Figma entries
253
+ // .vscode/mcp.json (Copilot format) — appium-mcp for mobile-only, Playwright MCP otherwise
228
254
  this.mergeMCPConfig(
229
255
  path.join(this.cwd, '.vscode', 'mcp.json'),
230
256
  'servers',
231
257
  {
232
- playwright: {
233
- command: 'npx',
234
- args: ['@playwright/mcp@latest'],
235
- },
258
+ ...(this.mobileOnly
259
+ ? { 'appium-mcp': { command: 'npx', args: ['-y', 'appium-mcp'] } }
260
+ : { playwright: { command: 'npx', args: ['@playwright/mcp@latest'] } }),
236
261
  figma: {
237
262
  type: 'http',
238
263
  url: 'https://mcp.figma.com/mcp',
@@ -243,18 +268,17 @@ export class ProjectInitializer {
243
268
  }
244
269
 
245
270
  /**
246
- * Create root .mcp.json for Claude Code — safe-merge Playwright + Figma entries
271
+ * Create root .mcp.json for Claude Code.
272
+ * Mobile-only: appium-mcp instead of Playwright MCP. Figma always included.
247
273
  */
248
274
  private createClaudeMCPConfig(): void {
249
275
  this.mergeMCPConfig(
250
276
  path.join(this.cwd, '.mcp.json'),
251
277
  'mcpServers',
252
278
  {
253
- playwright: {
254
- type: 'stdio',
255
- command: 'npx',
256
- args: ['@playwright/mcp@latest'],
257
- },
279
+ ...(this.mobileOnly
280
+ ? { 'appium-mcp': { type: 'stdio', command: 'npx', args: ['-y', 'appium-mcp'] } }
281
+ : { playwright: { type: 'stdio', command: 'npx', args: ['@playwright/mcp@latest'] } }),
258
282
  figma: {
259
283
  type: 'http',
260
284
  url: 'https://mcp.figma.com/mcp',
@@ -337,10 +361,18 @@ export class ProjectInitializer {
337
361
  if (projectName) {
338
362
  console.log(` ${stepIndex++}. cd ${projectName}`);
339
363
  }
340
- console.log(` ${stepIndex++}. Create your first screen: sungen add --screen <name> --path <url-path>`);
341
- console.log(` ${stepIndex++}. Use AI (Copilot/Claude) to generate Gherkin + selectors from the live page`);
342
- console.log(` ${stepIndex++}. Compile tests: sungen generate --screen <name>`);
343
- console.log(` ${stepIndex++}. Run tests: npx playwright test\n`);
364
+ if (this.mobileOnly) {
365
+ console.log(` ${stepIndex++}. Fill in .env.appium with your device/app details (see .env.appium.example)`);
366
+ console.log(` ${stepIndex++}. Create your first screen: sungen add --screen <name>`);
367
+ console.log(` ${stepIndex++}. Use AI (Copilot/Claude) to generate Gherkin + selectors from the live app`);
368
+ console.log(` ${stepIndex++}. Compile tests: sungen generate --screen <name>`);
369
+ console.log(` ${stepIndex++}. Run tests: npm run test:mobile\n`);
370
+ } else {
371
+ console.log(` ${stepIndex++}. Create your first screen: sungen add --screen <name> --path <url-path>`);
372
+ console.log(` ${stepIndex++}. Use AI (Copilot/Claude) to generate Gherkin + selectors from the live page`);
373
+ console.log(` ${stepIndex++}. Compile tests: sungen generate --screen <name>`);
374
+ console.log(` ${stepIndex++}. Run tests: npx playwright test\n`);
375
+ }
344
376
  }
345
377
 
346
378
  /**
@@ -390,8 +422,9 @@ export class ProjectInitializer {
390
422
  }
391
423
 
392
424
  /**
393
- * Create qa/capabilities.yaml — the platform driver profile (Phase 2a).
394
- * Defaults to `web` (Playwright). Mobile/API projects switch via `sungen capability add`.
425
+ * Create qa/capabilities.yaml — the platform driver profile.
426
+ * Web default: platform=web, enabled=[web].
427
+ * Mobile: platform=mobile, enabled=[mobile] (via writeMobileCapabilities from the scaffolder).
395
428
  */
396
429
  private createCapabilities(): void {
397
430
  const p = path.join(this.cwd, 'qa', 'capabilities.yaml');
@@ -399,13 +432,18 @@ export class ProjectInitializer {
399
432
  this.skippedItems.push('qa/capabilities.yaml');
400
433
  return;
401
434
  }
402
- const content =
403
- '# Sungen capability profile — which runtime/drivers this project uses.\n' +
404
- '# platform: the runtime adapter — web → Playwright, mobile → Appium.\n' +
405
- '# enabled: drivers turned on (add more via `sungen capability add <driver>`).\n\n' +
406
- 'platform: web\n' +
407
- 'enabled:\n - web\n';
408
- fs.writeFileSync(p, content, 'utf-8');
435
+
436
+ if (this.mobile.enabled) {
437
+ writeMobileCapabilities(this.cwd);
438
+ } else {
439
+ const content =
440
+ '# Sungen capability profile — which runtime/drivers this project uses.\n' +
441
+ '# platform: the runtime adapter — web → Playwright, mobile → Appium.\n' +
442
+ '# enabled: drivers turned on (add more via `sungen capability add <driver>`).\n\n' +
443
+ 'platform: web\n' +
444
+ 'enabled:\n - web\n';
445
+ fs.writeFileSync(p, content, 'utf-8');
446
+ }
409
447
  this.createdItems.push('qa/capabilities.yaml');
410
448
  }
411
449
 
@@ -451,6 +489,20 @@ export class ProjectInitializer {
451
489
  this.createdItems.push('specs/generated/test-data.ts');
452
490
  }
453
491
 
492
+ /**
493
+ * Delegate to mobile-runtime-scaffolder to emit Appium/WDIO harness files,
494
+ * then best-effort install @sungen/driver-mobile at lockstep version.
495
+ * Called only when `--mobile` is set.
496
+ */
497
+ private emitMobileHarness(): void {
498
+ const result = emitMobileRuntime(this.cwd, this.mobile);
499
+ this.createdItems.push(...result.created);
500
+ this.skippedItems.push(...result.skipped);
501
+
502
+ // Best-effort driver install — non-fatal so offline init still scaffolds.
503
+ installMobileDriver(this.cwd);
504
+ }
505
+
454
506
  /**
455
507
  * Read a template file from the templates directory
456
508
  */
@@ -476,8 +528,11 @@ export class ProjectInitializer {
476
528
  // Ensure standard scripts exist in package.json
477
529
  this.ensurePackageScripts(packageJsonPath);
478
530
 
479
- // Check which dependencies are missing
480
- const requiredDeps = ['@playwright/test', '@types/node', 'yaml'];
531
+ // Mobile-only drops @playwright/test (no web runner needed).
532
+ const requiredDeps = this.mobileOnly
533
+ ? ['@types/node', 'yaml']
534
+ : ['@playwright/test', '@types/node', 'yaml'];
535
+
481
536
  let missingDeps: string[] = requiredDeps;
482
537
  try {
483
538
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
@@ -489,15 +544,17 @@ export class ProjectInitializer {
489
544
 
490
545
  if (missingDeps.length === 0) {
491
546
  console.log('✓ All dependencies already installed\n');
492
- return;
547
+ } else {
548
+ console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
549
+ execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
493
550
  }
494
551
 
495
- console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
496
- execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
497
-
498
- console.log('\n🎭 Installing Chromium (default browser)...\n');
499
- execSync('npx playwright install --with-deps chromium', execOpts);
500
- console.log('\n💡 To install other browsers: npm run install:browsers -- firefox webkit\n');
552
+ // Chromium is only needed for the web (Playwright) runner — skip for mobile-only projects.
553
+ if (!this.mobileOnly) {
554
+ console.log('\n🎭 Installing Chromium (default browser)...\n');
555
+ execSync('npx playwright install --with-deps chromium', execOpts);
556
+ console.log('\n💡 To install other browsers: npm run install:browsers -- firefox webkit\n');
557
+ }
501
558
  }
502
559
 
503
560
  /**
@@ -516,15 +573,21 @@ export class ProjectInitializer {
516
573
  packageJson.scripts = {};
517
574
  }
518
575
 
519
- const standardScripts: Record<string, string> = {
520
- 'test': 'playwright test specs/generated/',
521
- 'test:headed': 'playwright test specs/generated/ --headed',
522
- 'test:debug': 'playwright test specs/generated/ --debug',
523
- 'test:ui': 'playwright test specs/generated/ --ui',
524
- 'report': 'playwright show-report',
525
- 'generate': 'sungen generate --all',
526
- 'install:browsers': 'npx playwright install',
527
- };
576
+ // Mobile-only projects get just the generate + mobile-run scripts (no Playwright web scripts).
577
+ const standardScripts: Record<string, string> = this.mobileOnly
578
+ ? {
579
+ 'generate': 'sungen generate --all',
580
+ 'test:mobile': 'wdio run wdio.conf.ts',
581
+ }
582
+ : {
583
+ 'test': 'playwright test specs/generated/',
584
+ 'test:headed': 'playwright test specs/generated/ --headed',
585
+ 'test:debug': 'playwright test specs/generated/ --debug',
586
+ 'test:ui': 'playwright test specs/generated/ --ui',
587
+ 'report': 'playwright show-report',
588
+ 'generate': 'sungen generate --all',
589
+ 'install:browsers': 'npx playwright install',
590
+ };
528
591
 
529
592
  let added = 0;
530
593
  for (const [name, command] of Object.entries(standardScripts)) {
@@ -6,6 +6,7 @@
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { chromium } from '@playwright/test';
9
+ import { readCapabilities } from '../harness/capability';
9
10
 
10
11
  export interface ScreenOptions {
11
12
  name: string;
@@ -13,6 +14,20 @@ export interface ScreenOptions {
13
14
  capture?: boolean;
14
15
  feature?: string;
15
16
  description?: string;
17
+ /**
18
+ * Optional per-OS tag emitter: 'android' | 'ios'. When set, the feature stub
19
+ * gets @platform:android or @platform:ios for OS-specific screens. Does NOT
20
+ * affect directory routing (always qa/screens/). Mobile project is detected via
21
+ * qa/capabilities.yaml, not this flag.
22
+ */
23
+ platform?: string;
24
+ /**
25
+ * Mobile navigation recipe (the in-app web-path analog). A `>`-separated list of nav steps
26
+ * that take the app from launch to THIS screen, e.g. "tap [My Page] tab > tap [Settings] row".
27
+ * Scaffolded into the feature's Background after the `Given User is on [Home] screen` anchor.
28
+ * Mobile-only; ignored (with a warning) for web projects. Omit when this IS the launcher screen.
29
+ */
30
+ reach?: string;
16
31
  }
17
32
 
18
33
  export class ScreenManager {
@@ -31,15 +46,31 @@ export class ScreenManager {
31
46
  // Validate screen name
32
47
  this.validateScreenName(options.name);
33
48
 
49
+ // Detect project platform from qa/capabilities.yaml (single source of truth)
50
+ const caps = readCapabilities(this.cwd);
51
+ const isMobile = caps.platform === 'mobile';
52
+
53
+ // --reach is mobile-only; warn and drop when the project is web
54
+ if (options.reach && !isMobile) {
55
+ console.warn(
56
+ `Warning: --reach is a mobile navigation recipe and is ignored for web projects ` +
57
+ `(web screens are addressed by their --path URL). Dropping it.`
58
+ );
59
+ options = { ...options, reach: undefined };
60
+ }
61
+
34
62
  // Normalize screen name for directory
35
63
  const screenName = this.normalizeScreenName(options.name);
64
+ // Always use qa/screens/ regardless of platform (1-platform model — no qa/mobile/ tree)
36
65
  const screenDir = path.join(this.screensDir, screenName);
37
66
 
38
- // Derive filename: --feature flag creates "<screen>-<feature>" name
67
+ // Derive filename: --feature flag creates "<screen>-<feature>" name.
68
+ // For mobile, `path` is the app entry (pkg/Activity), NOT a URL — never derive
69
+ // the filename from it; use the screen name instead.
39
70
  let filename: string;
40
71
  if (options.feature) {
41
72
  filename = `${screenName}-${this.normalizeScreenName(options.feature)}`;
42
- } else if (options.path) {
73
+ } else if (options.path && !isMobile) {
43
74
  filename = this.extractFilenameFromPath(options.path);
44
75
  } else {
45
76
  filename = screenName;
@@ -92,18 +123,25 @@ export class ScreenManager {
92
123
  const featureContent = this.generateFeatureTemplate(options, filename);
93
124
  fs.writeFileSync(featurePath, featureContent, 'utf-8');
94
125
 
95
- // Generate empty selector YAML (with page entry)
126
+ // Generate empty selector YAML. Web gets a `page` entry (the route); mobile
127
+ // selectors come from Appium MCP at run-test, so it's just a header comment.
96
128
  if (!fs.existsSync(selectorPath)) {
97
- const pagePath = options.path || `/${screenName}`;
98
- fs.writeFileSync(selectorPath, [
99
- `# ${options.name} Screen Selectors`,
100
- `# Fill via AI or manually`,
101
- ``,
102
- `${screenName}:`,
103
- ` type: 'page'`,
104
- ` value: '${pagePath}'`,
105
- ``,
106
- ].join('\n'), 'utf-8');
129
+ const selectorLines = isMobile
130
+ ? [
131
+ `# ${options.name} Screen Selectors (mobile)`,
132
+ `# Generated at /sungen:run-test via Appium MCP — accessibility-id preferred.`,
133
+ ``,
134
+ ]
135
+ : [
136
+ `# ${options.name} Screen Selectors`,
137
+ `# Fill via AI or manually`,
138
+ ``,
139
+ `${screenName}:`,
140
+ ` type: 'page'`,
141
+ ` value: '${options.path || `/${screenName}`}'`,
142
+ ``,
143
+ ];
144
+ fs.writeFileSync(selectorPath, selectorLines.join('\n'), 'utf-8');
107
145
  }
108
146
 
109
147
  // Generate empty test-data YAML
@@ -186,9 +224,9 @@ export class ScreenManager {
186
224
  ].join('\n'), 'utf-8');
187
225
  }
188
226
 
189
- // Auto-screenshot: capture page only when --capture is set (requires --path)
227
+ // Auto-screenshot: capture page only when --capture is set and not mobile (requires --path)
190
228
  // Use filename (not screenName) so each feature gets its own screenshots
191
- if (options.capture && options.path) {
229
+ if (options.capture && options.path && !isMobile) {
192
230
  await this.captureScreenshot(options.path, requirementsUiDir, filename);
193
231
  }
194
232
 
@@ -204,12 +242,21 @@ export class ScreenManager {
204
242
  }
205
243
  console.log('');
206
244
 
207
- console.log('Next steps:');
208
- console.log(` 1. Fill requirements/spec.md with screen spec (fields, validation, business rules)`);
209
- console.log(` Optionally add UI designs to requirements/ui/ (screenshots, mockups)`);
210
- console.log(` 2. Generate test cases: /sungen:create-test ${screenName} (or /sungen-create-test)`);
211
- console.log(` 3. Compile: sungen generate --screen ${screenName}`);
212
- console.log(` 4. Run: npx playwright test\n`);
245
+ if (isMobile) {
246
+ console.log('Next steps:');
247
+ console.log(` 1. Fill requirements/spec.md with screen spec (elements, actions, business rules)`);
248
+ console.log(` Use /sungen:add-screen with sungen-capture-mobile to scan via Appium MCP`);
249
+ console.log(` 2. Generate test cases: /sungen:create-test ${screenName}`);
250
+ console.log(` 3. Compile: sungen generate --screen ${screenName} --framework appium`);
251
+ console.log(` 4. Run: npm run test:mobile\n`);
252
+ } else {
253
+ console.log('Next steps:');
254
+ console.log(` 1. Fill requirements/spec.md with screen spec (fields, validation, business rules)`);
255
+ console.log(` Optionally add UI designs to requirements/ui/ (screenshots, mockups)`);
256
+ console.log(` 2. Generate test cases: /sungen:create-test ${screenName} (or /sungen-create-test)`);
257
+ console.log(` 3. Compile: sungen generate --screen ${screenName}`);
258
+ console.log(` 4. Run: npx playwright test\n`);
259
+ }
213
260
  }
214
261
 
215
262
  /**
@@ -291,6 +338,57 @@ export class ScreenManager {
291
338
  }
292
339
 
293
340
  private generateSpecTemplate(options: ScreenOptions, screenName: string): string {
341
+ const caps = readCapabilities(this.cwd);
342
+ const isMobile = caps.platform === 'mobile';
343
+
344
+ if (isMobile) {
345
+ const appEntry = options.path || this.readWdioAppEntry() || '<appPackage>';
346
+ return `# ${options.name} Screen Specification
347
+
348
+ ## Overview
349
+ - **App Entry:** ${appEntry}
350
+ - **Platform:** mobile (Android + iOS)
351
+ - **Auth Required:** no
352
+ - **Reach:** ${options.reach || '<!-- nav recipe: how to reach this screen from app launch, e.g. "tap [My Page] tab > tap [Settings] row" -->'}
353
+
354
+ ## Sections
355
+
356
+ ### Section: [Section Name]
357
+ - **Type:** list | card | form | modal | navigation | tab-bar
358
+ - **Description:** [Brief description of this section]
359
+
360
+ #### Elements
361
+ | Element | Type | Identifier | Notes |
362
+ |---------|------|------------|-------|
363
+ | [Element Name] | button | accessibility-id | [tap target, e.g., submits form] |
364
+
365
+ #### Actions
366
+ | Action | Element | Behavior |
367
+ |--------|---------|----------|
368
+ | [Action Name] | button | [What happens on tap] |
369
+
370
+ #### States
371
+ | State | Condition | Visual |
372
+ |-------|-----------|--------|
373
+ | Default | Screen load | [Default appearance] |
374
+ | Loading | After tap | [Loading indicator] |
375
+ | Error | Action fails | [Error appearance] |
376
+ | Success | Action complete | [Success behavior] |
377
+
378
+ ## Business Rules
379
+ <!-- Rules that affect test logic: limits, permissions, conditions -->
380
+ - [Rule 1]
381
+
382
+ ## Accessibility
383
+ <!-- Accessibility IDs, content-desc, aria-labels -->
384
+ - Accessibility IDs: [element: id]
385
+
386
+ ## Notes
387
+ <!-- Edge cases, known issues, environment-specific behavior -->
388
+ - [Note 1]
389
+ `;
390
+ }
391
+
294
392
  const pagePath = options.path || `/${screenName}`;
295
393
  return `# ${options.name} Screen Specification
296
394
 
@@ -348,9 +446,48 @@ export class ScreenManager {
348
446
 
349
447
  private generateFeatureTemplate(options: ScreenOptions, filename: string): string {
350
448
  const screenName = this.normalizeScreenName(options.name);
351
- const featurePath = options.path || `/${screenName}`;
352
449
  const description = options.description || `interact with the ${options.name} screen`;
353
450
 
451
+ const caps = readCapabilities(this.cwd);
452
+ const isMobile = caps.platform === 'mobile';
453
+
454
+ if (isMobile) {
455
+ // Determine platform tag: --platform android/ios emits a per-OS tag for OS-specific screens.
456
+ // Default (no --platform) = cross-platform, no OS-specific tag.
457
+ const platformTag =
458
+ options.platform === 'android' ? '@platform:android'
459
+ : options.platform === 'ios' ? '@platform:ios'
460
+ : '@platform:mobile';
461
+
462
+ // App entry: explicit --path → wdio.conf.ts default → placeholder.
463
+ const appEntry = options.path || this.readWdioAppEntry() || '<appPackage>';
464
+
465
+ // Navigation recipe (Background block)
466
+ const background = this.buildMobileBackground(options.reach);
467
+
468
+ return `${platformTag}
469
+ Feature: ${options.name} Screen
470
+
471
+ As a user
472
+ I want to ${description}
473
+ So that I can accomplish my tasks
474
+ Path: ${appEntry}
475
+
476
+ # Mobile (Appium) — selectors generated at /sungen:run-test via Appium MCP.
477
+ # Use accessibility-id selectors; assert text content with \`header with {{var}}\`
478
+ # (compiles to content-desc/label via toHaveAttribute). See sungen-mobile-gestures
479
+ # for swipe/long-press/scroll steps.
480
+
481
+ ${background}
482
+ @high
483
+ Scenario: Sample scenario for ${options.name}
484
+ When User tap [element]
485
+ Then User see [result] header with {{result_text}}
486
+ `;
487
+ }
488
+
489
+ // Web feature stub
490
+ const featurePath = options.path || `/${screenName}`;
354
491
  return `Feature: ${options.name} Screen
355
492
 
356
493
  As a user
@@ -367,4 +504,82 @@ export class ScreenManager {
367
504
  Then User see [result] text with {{success}}
368
505
  `;
369
506
  }
507
+
508
+ /**
509
+ * Read the default app entry (`<appPackage>/<appActivity>`) from the project's wdio.conf.ts so a
510
+ * mobile `add-screen` without `--path` scaffolds a real, runnable `Path:` matching the configured
511
+ * app instead of a `<placeholder>`. Supports both the env-default form
512
+ * (`APP_PACKAGE = process.env.APP_PACKAGE || '…'`) and the literal capability form
513
+ * (`'appium:appPackage': '…'`). Returns null when wdio.conf.ts is absent or unparseable.
514
+ */
515
+ private readWdioAppEntry(): string | null {
516
+ const wdioPath = path.join(this.cwd, 'wdio.conf.ts');
517
+ if (!fs.existsSync(wdioPath)) return null;
518
+ let src: string;
519
+ try {
520
+ src = fs.readFileSync(wdioPath, 'utf-8');
521
+ } catch {
522
+ return null;
523
+ }
524
+ const pick = (re: RegExp): string | undefined => src.match(re)?.[1]?.trim();
525
+ const pkg =
526
+ pick(/APP_PACKAGE\s*=\s*process\.env\.APP_PACKAGE\s*\|\|\s*['"]([^'"]+)['"]/) ??
527
+ pick(/['"]appium:appPackage['"]\s*:\s*['"]([^'"]+)['"]/);
528
+ if (!pkg) return null;
529
+ const activity =
530
+ pick(/APP_ACTIVITY\s*=\s*process\.env\.APP_ACTIVITY\s*\|\|\s*['"]([^'"]+)['"]/) ??
531
+ pick(/['"]appium:appActivity['"]\s*:\s*['"]([^'"]+)['"]/);
532
+ return activity ? `${pkg}/${activity}` : pkg;
533
+ }
534
+
535
+ /**
536
+ * Build the Background block for a mobile screen — the navigation recipe (in-app web-path
537
+ * analog). Line 1 is the `Given User is on [Home] screen` launcher/landing anchor; each step
538
+ * from `--reach` (a `>`-separated list) becomes a `When/And User <step>` nav line, taking the
539
+ * app from launch to THIS screen. capture-mobile replays these to land on the target screen
540
+ * before scanning. With no recipe, emit a guided stub (anchor + commented placeholder).
541
+ */
542
+ private buildMobileBackground(reach?: string): string {
543
+ // Normalize one nav segment to a bare step body ("tap [My Page] tab"): drop any leading
544
+ // Gherkin keyword and a leading "User " so we can re-emit "When/And User <body>" cleanly.
545
+ const normalizeStep = (raw: string): string =>
546
+ raw
547
+ .trim()
548
+ .replace(/^(given|when|and|then|but)\s+/i, '')
549
+ .replace(/^user\s+/i, '')
550
+ .trim();
551
+
552
+ const steps = (reach || '')
553
+ .split('>')
554
+ .map(normalizeStep)
555
+ .filter(Boolean);
556
+
557
+ if (steps.length === 0) {
558
+ // No recipe → guided stub. Keep the anchor (correct as-is when this IS the launcher
559
+ // screen); the comments tell create-test / capture-mobile how to fill the nav steps.
560
+ return [
561
+ ' Background:',
562
+ ' # Navigation recipe — how to reach THIS screen from app launch (the in-app',
563
+ ' # web-path analog). Line 1 is the launcher/landing screen the app opens on;',
564
+ ' # add a `When/And User tap [...]` per hop to reach this screen. capture-mobile',
565
+ ' # replays these so Appium lands here (not the launcher) before scanning. Leave',
566
+ ' # only the anchor if this IS the launcher screen. Prefill with',
567
+ ' # --reach "tap [Tab] > tap [Row]" at add-screen time.',
568
+ ' Given User is on [Home] screen',
569
+ ' # When User tap [<tab or button that leads to this screen>]',
570
+ '',
571
+ ].join('\n');
572
+ }
573
+
574
+ const navLines = steps.map(
575
+ (body, i) => ` ${i === 0 ? 'When' : 'And'} User ${body}`,
576
+ );
577
+ return [
578
+ ' Background:',
579
+ ' # Navigation recipe — replayed by capture-mobile to land on this screen before scanning.',
580
+ ' Given User is on [Home] screen',
581
+ ...navLines,
582
+ '',
583
+ ].join('\n');
584
+ }
370
585
  }