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

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 (153) 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/cli/commands/journey.d.ts.map +1 -1
  23. package/dist/cli/commands/journey.js +19 -1
  24. package/dist/cli/commands/journey.js.map +1 -1
  25. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  26. package/dist/dashboard/snapshot-builder.js +10 -1
  27. package/dist/dashboard/snapshot-builder.js.map +1 -1
  28. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  29. package/dist/exporters/csv-exporter.js +3 -1
  30. package/dist/exporters/csv-exporter.js.map +1 -1
  31. package/dist/exporters/json-exporter.js +3 -1
  32. package/dist/exporters/json-exporter.js.map +1 -1
  33. package/dist/exporters/mobile-app-id.d.ts +22 -0
  34. package/dist/exporters/mobile-app-id.d.ts.map +1 -0
  35. package/dist/exporters/mobile-app-id.js +37 -0
  36. package/dist/exporters/mobile-app-id.js.map +1 -0
  37. package/dist/exporters/spec-parser.d.ts.map +1 -1
  38. package/dist/exporters/spec-parser.js +6 -2
  39. package/dist/exporters/spec-parser.js.map +1 -1
  40. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +3 -0
  41. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  42. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  43. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  44. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  45. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  46. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  47. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  48. package/dist/generators/test-generator/code-generator.js +38 -3
  49. package/dist/generators/test-generator/code-generator.js.map +1 -1
  50. package/dist/generators/test-generator/template-engine.d.ts +3 -0
  51. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  52. package/dist/generators/test-generator/template-engine.js +11 -1
  53. package/dist/generators/test-generator/template-engine.js.map +1 -1
  54. package/dist/generators/test-generator/utils/selector-resolver.d.ts +11 -1
  55. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  56. package/dist/generators/test-generator/utils/selector-resolver.js +33 -0
  57. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  58. package/dist/harness/audit.js +1 -1
  59. package/dist/harness/audit.js.map +1 -1
  60. package/dist/harness/catalog/universal-viewpoints.yaml +15 -0
  61. package/dist/harness/journey.d.ts +20 -0
  62. package/dist/harness/journey.d.ts.map +1 -1
  63. package/dist/harness/journey.js +97 -11
  64. package/dist/harness/journey.js.map +1 -1
  65. package/dist/harness/sensors.d.ts +1 -1
  66. package/dist/harness/sensors.d.ts.map +1 -1
  67. package/dist/harness/sensors.js +32 -9
  68. package/dist/harness/sensors.js.map +1 -1
  69. package/dist/orchestrator/flow-manager.d.ts +11 -0
  70. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  71. package/dist/orchestrator/flow-manager.js +147 -29
  72. package/dist/orchestrator/flow-manager.js.map +1 -1
  73. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +59 -0
  74. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -0
  75. package/dist/orchestrator/mobile-runtime-scaffolder.js +298 -0
  76. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -0
  77. package/dist/orchestrator/project-initializer.d.ts +25 -7
  78. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  79. package/dist/orchestrator/project-initializer.js +115 -62
  80. package/dist/orchestrator/project-initializer.js.map +1 -1
  81. package/dist/orchestrator/screen-manager.d.ts +30 -0
  82. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  83. package/dist/orchestrator/screen-manager.js +205 -22
  84. package/dist/orchestrator/screen-manager.js.map +1 -1
  85. package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +12 -0
  86. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
  87. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
  88. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -2
  89. package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
  90. package/dist/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
  91. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -1
  92. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
  93. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
  94. package/dist/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
  95. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
  96. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
  97. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +11 -2
  98. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
  99. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
  100. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +8 -1
  101. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
  102. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
  103. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
  104. package/package.json +3 -3
  105. package/src/capabilities/discover.ts +29 -3
  106. package/src/cli/commands/add-flow.ts +2 -0
  107. package/src/cli/commands/add.ts +4 -0
  108. package/src/cli/commands/capability.ts +64 -1
  109. package/src/cli/commands/delivery.ts +89 -2
  110. package/src/cli/commands/generate.ts +2 -1
  111. package/src/cli/commands/init.ts +38 -3
  112. package/src/cli/commands/journey.ts +15 -2
  113. package/src/dashboard/snapshot-builder.ts +10 -1
  114. package/src/exporters/csv-exporter.ts +3 -1
  115. package/src/exporters/json-exporter.ts +3 -1
  116. package/src/exporters/mobile-app-id.ts +34 -0
  117. package/src/exporters/spec-parser.ts +6 -2
  118. package/src/generators/test-generator/adapters/adapter-interface.ts +3 -0
  119. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  120. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  121. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  122. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  123. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  124. package/src/generators/test-generator/code-generator.ts +39 -3
  125. package/src/generators/test-generator/template-engine.ts +14 -1
  126. package/src/generators/test-generator/utils/selector-resolver.ts +51 -1
  127. package/src/harness/audit.ts +1 -1
  128. package/src/harness/catalog/universal-viewpoints.yaml +15 -0
  129. package/src/harness/journey.ts +110 -9
  130. package/src/harness/sensors.ts +25 -8
  131. package/src/orchestrator/flow-manager.ts +165 -29
  132. package/src/orchestrator/mobile-runtime-scaffolder.ts +277 -0
  133. package/src/orchestrator/project-initializer.ts +127 -64
  134. package/src/orchestrator/screen-manager.ts +237 -22
  135. package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +12 -0
  136. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
  137. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
  138. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -2
  139. package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
  140. package/src/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
  141. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -1
  142. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
  143. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
  144. package/src/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
  145. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
  146. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
  147. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +11 -2
  148. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
  149. package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
  150. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +8 -1
  151. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
  152. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
  153. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Mobile Runtime Scaffolder
3
+ *
4
+ * Emits the Appium/WebdriverIO harness files into a project directory.
5
+ * Called by ProjectInitializer (init --mobile) and, in Phase 02, by `capability add mobile`.
6
+ * Never duplicated — always imported from here.
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { spawnSync } from 'child_process';
12
+ import { writeCapabilities } from '../harness/capability';
13
+
14
+ export interface MobileInitOptions {
15
+ enabled: boolean;
16
+ /** Mobile-only project: skip all web (Playwright) scaffolding, deps, and Chromium download. */
17
+ mobileOnly?: boolean;
18
+ appPackage?: string;
19
+ appActivity?: string;
20
+ appApk?: string;
21
+ androidUdid?: string;
22
+ iosBundleId?: string;
23
+ iosDevice?: string;
24
+ iosVersion?: string;
25
+ }
26
+
27
+ export interface ScaffoldResult {
28
+ created: string[];
29
+ skipped: string[];
30
+ }
31
+
32
+ /** Read a template from the orchestrator's bundled templates directory. */
33
+ function readTemplate(filename: string): string {
34
+ const templatePath = path.join(__dirname, 'templates', filename);
35
+ return fs.readFileSync(templatePath, 'utf-8');
36
+ }
37
+
38
+ /**
39
+ * Emit the mobile runtime harness into `cwd`.
40
+ *
41
+ * Files written:
42
+ * wdio.conf.ts — project root (seeded from mobile options)
43
+ * specs/reporters/pw-shape-reporter.ts — Playwright-shaped JSON reporter for WDIO
44
+ * .env.appium — seeded from flags (gitignored)
45
+ * .env.appium.example — committed placeholder guide
46
+ *
47
+ * The caller is responsible for adding `test:mobile` to package.json scripts and
48
+ * writing qa/capabilities.yaml — helpers for both are exported below.
49
+ */
50
+ export function emitMobileRuntime(cwd: string, opts: MobileInitOptions): ScaffoldResult {
51
+ const created: string[] = [];
52
+ const skipped: string[] = [];
53
+
54
+ // wdio.conf.ts (project root) — substitute init-provided app defaults into the template
55
+ const wdioPath = path.join(cwd, 'wdio.conf.ts');
56
+ if (fs.existsSync(wdioPath)) {
57
+ skipped.push('wdio.conf.ts');
58
+ } else {
59
+ const content = readTemplate('wdio.conf.ts')
60
+ .replace('__APP_PACKAGE__', opts.appPackage || 'com.example.app')
61
+ // appActivity is optional — leave empty when not provided so UiAutomator2
62
+ // auto-resolves the launcher activity from appPackage. Only pin when explicitly passed.
63
+ .replace('__APP_ACTIVITY__', opts.appActivity || '')
64
+ .replace('__ANDROID_UDID__', opts.androidUdid || 'emulator-5554')
65
+ .replace('__IOS_BUNDLE_ID__', opts.iosBundleId || 'com.example.app')
66
+ .replace('__IOS_DEVICE__', opts.iosDevice || 'iPhone 16')
67
+ .replace('__IOS_VERSION__', opts.iosVersion || '18.0');
68
+ fs.writeFileSync(wdioPath, content, 'utf-8');
69
+ created.push('wdio.conf.ts');
70
+ }
71
+
72
+ // specs/reporters/pw-shape-reporter.ts
73
+ const reporterPath = path.join(cwd, 'specs', 'reporters', 'pw-shape-reporter.ts');
74
+ if (fs.existsSync(reporterPath)) {
75
+ skipped.push('specs/reporters/pw-shape-reporter.ts');
76
+ } else {
77
+ fs.mkdirSync(path.dirname(reporterPath), { recursive: true });
78
+ fs.writeFileSync(reporterPath, readTemplate('specs-pw-shape-reporter.ts'), 'utf-8');
79
+ created.push('specs/reporters/pw-shape-reporter.ts');
80
+ }
81
+
82
+ // .env.appium — seeded from the flags that were provided; env vars that were not passed
83
+ // are left commented out so the file is a useful guide without imposing bogus values.
84
+ const envPath = path.join(cwd, '.env.appium');
85
+ if (fs.existsSync(envPath)) {
86
+ skipped.push('.env.appium');
87
+ } else {
88
+ const lines: string[] = [
89
+ '# .env.appium — loaded by wdio.conf.ts. Shell exports override these values.',
90
+ '# See .env.appium.example for the full reference.',
91
+ '',
92
+ ];
93
+ if (opts.appPackage) lines.push(`APP_PACKAGE=${opts.appPackage}`);
94
+ if (opts.appActivity) lines.push(`APP_ACTIVITY=${opts.appActivity}`);
95
+ if (opts.appApk) lines.push(`APP_APK=${opts.appApk}`);
96
+ if (opts.androidUdid) lines.push(`ANDROID_UDID=${opts.androidUdid}`);
97
+ if (opts.iosBundleId) lines.push(`IOS_BUNDLE_ID=${opts.iosBundleId}`);
98
+ if (opts.iosDevice) lines.push(`IOS_DEVICE=${opts.iosDevice}`);
99
+ if (opts.iosVersion) lines.push(`IOS_VERSION=${opts.iosVersion}`);
100
+ lines.push('');
101
+ fs.writeFileSync(envPath, lines.join('\n'), 'utf-8');
102
+ created.push('.env.appium');
103
+ }
104
+
105
+ // .env.appium.example — committed placeholder guide (template stored without leading dot
106
+ // so copy-templates' glob picks it up; the output file gets the dot).
107
+ const envExamplePath = path.join(cwd, '.env.appium.example');
108
+ if (fs.existsSync(envExamplePath)) {
109
+ skipped.push('.env.appium.example');
110
+ } else {
111
+ fs.writeFileSync(envExamplePath, readTemplate('env.appium.example'), 'utf-8');
112
+ created.push('.env.appium.example');
113
+ }
114
+
115
+ return { created, skipped };
116
+ }
117
+
118
+ /**
119
+ * Write qa/capabilities.yaml for a mobile project.
120
+ * Uses the canonical writeCapabilities from harness/capability so the format is always consistent.
121
+ */
122
+ export function writeMobileCapabilities(cwd: string): string {
123
+ return writeCapabilities(cwd, { platform: 'mobile', enabled: ['mobile'] });
124
+ }
125
+
126
+ /**
127
+ * Resolve the install spec for `@sungen/driver-mobile`.
128
+ *
129
+ * In a monorepo checkout (dev / dogfood) the driver lives at `packages/driver-mobile` next to core and
130
+ * is NOT published to the registry — so we install it by `file:` path. In a published install (core in
131
+ * node_modules, no sibling package) we fall back to the registry pinned to the core version (lockstep).
132
+ *
133
+ * Why: a bare `@sungen/driver-mobile@<coreVersion>` hits npm E404 whenever the driver is unpublished or
134
+ * the consuming project lives outside the workspace (`test-mobile/`). The local `file:` path makes the
135
+ * dev/dogfood path work without the registry, while the registry fallback preserves lockstep for users.
136
+ */
137
+ function resolveMobileDriverSpec(): string {
138
+ // From this file (src/orchestrator OR dist/orchestrator) the sibling driver package is ../../../driver-mobile.
139
+ const localDriverDir = path.resolve(__dirname, '..', '..', '..', 'driver-mobile');
140
+ if (fs.existsSync(path.join(localDriverDir, 'package.json'))) {
141
+ return `file:${localDriverDir}`;
142
+ }
143
+ let coreVersion = 'latest';
144
+ try {
145
+ coreVersion = require('../../package.json').version || 'latest';
146
+ } catch {
147
+ // noop — fall back to latest
148
+ }
149
+ return `@sungen/driver-mobile@${coreVersion}`;
150
+ }
151
+
152
+ /** Best-effort `npm install -D <specs…> [extraArgs…]` in `cwd`. Returns true on exit code 0. */
153
+ function npmInstallDev(cwd: string, specs: string[], extraArgs: string[] = []): boolean {
154
+ const result = spawnSync('npm', ['install', '-D', ...specs, ...extraArgs], { cwd, stdio: 'inherit', shell: true });
155
+ return result.status === 0;
156
+ }
157
+
158
+ /**
159
+ * Locate the project's locally-installed appium CLI bin. Walks up from `cwd` so it works whether the
160
+ * appium dep is nested under @sungen/driver-mobile or hoisted to a parent node_modules/.bin. Returns
161
+ * null when appium is not installed in the project (then we skip cleanly — no `npx` auto-fetch).
162
+ */
163
+ function appiumBinPath(cwd: string): string | null {
164
+ const binName = process.platform === 'win32' ? 'appium.cmd' : 'appium';
165
+ let dir = path.resolve(cwd);
166
+ for (let i = 0; i < 8; i++) {
167
+ const bin = path.join(dir, 'node_modules', '.bin', binName);
168
+ if (fs.existsSync(bin)) return bin;
169
+ const parent = path.dirname(dir);
170
+ if (parent === dir) break;
171
+ dir = parent;
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /** Appium platform drivers already installed in ~/.appium (per-user). Empty set if it can't tell. */
177
+ function installedAppiumDrivers(appiumBin: string, cwd: string): Set<string> {
178
+ const r = spawnSync(appiumBin, ['driver', 'list', '--installed', '--json'], { cwd, encoding: 'utf-8' });
179
+ if (r.status !== 0 || !r.stdout) return new Set();
180
+ try { return new Set(Object.keys(JSON.parse(r.stdout))); } catch { return new Set(); }
181
+ }
182
+
183
+ /**
184
+ * Auto-install the Appium PLATFORM drivers needed for ANY session — both the appium-mcp live capture
185
+ * (`/sungen:create-test`) and the wdio runner (`/sungen:run-test`). Provisioned by the SAME code path
186
+ * that installs @sungen/driver-mobile (right after its `appium` dep is installed). Best-effort + idempotent.
187
+ *
188
+ * - `uiautomator2` (Android): cross-platform → always.
189
+ * - `xcuitest` (iOS): macOS-only — it builds WebDriverAgent via Xcode at RUN time, usable only on a
190
+ * Mac → installed only on darwin; elsewhere we print the command for when on a Mac.
191
+ *
192
+ * Runs the project-local appium bin directly (NOT `npx`, which would either auto-fetch a latest appium
193
+ * or hard-fail with `--no-install`). If appium isn't installed in the project, skip with a clear hint.
194
+ */
195
+ function installAppiumPlatformDrivers(cwd: string): void {
196
+ const appiumBin = appiumBinPath(cwd);
197
+ if (!appiumBin) {
198
+ console.warn(
199
+ '⚠️ appium is not installed in the project — skipping platform-driver install.\n' +
200
+ ' Install the mobile runtime first (the @sungen/driver-mobile deps via `npm install`), then run:\n' +
201
+ ` npx appium driver install uiautomator2${process.platform === 'darwin' ? ' (+ xcuitest)' : ''}`,
202
+ );
203
+ return;
204
+ }
205
+ const installed = installedAppiumDrivers(appiumBin, cwd);
206
+ const want = ['uiautomator2', ...(process.platform === 'darwin' ? ['xcuitest'] : [])];
207
+ for (const drv of want) {
208
+ if (installed.has(drv)) { console.log(`✓ appium driver "${drv}" already installed.`); continue; }
209
+ console.log(`📦 Installing appium driver "${drv}"...`);
210
+ const r = spawnSync(appiumBin, ['driver', 'install', drv], { cwd, stdio: 'inherit' });
211
+ if (!r || r.status !== 0) {
212
+ console.warn(`⚠️ appium driver "${drv}" not installed (best-effort) — retry: npx appium driver install ${drv}`);
213
+ }
214
+ }
215
+ if (process.platform !== 'darwin') {
216
+ console.log('ℹ️ iOS (xcuitest) is macOS-only (needs Xcode at run time) — skipped on this OS. On a Mac: npx appium driver install xcuitest');
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Best-effort provision of the mobile runtime into a project that has opted into mobile
222
+ * (`sungen init --mobile` / `sungen capability add mobile` — never auto-run; consent-gated).
223
+ *
224
+ * Installs everything the runner needs:
225
+ * 1. `@sungen/driver-mobile` + its WebdriverIO/Appium runtime (declared as that package's `dependencies`).
226
+ * A local `file:` spec (dev/dogfood) uses `npm install --install-links` so npm COPIES the package and
227
+ * installs its transitive deps — a plain file: install only SYMLINKS it and skips the runtime. A
228
+ * published/registry spec installs deps normally.
229
+ * 2. the appium PLATFORM drivers (uiautomator2 / xcuitest, ~/.appium-managed) — NOT npm packages, so
230
+ * installed via the appium CLI here (installAppiumPlatformDrivers), so a session can actually start.
231
+ *
232
+ * Non-fatal: logs a warning on failure so offline/air-gapped init still scaffolds correctly.
233
+ *
234
+ * @returns true if the driver install succeeded (or was already present), false on failure
235
+ */
236
+ export function installMobileDriver(cwd: string): boolean {
237
+ const driverSpec = resolveMobileDriverSpec();
238
+
239
+ // A `file:` spec (unpublished dev/dogfood driver) MUST install with --install-links: a plain file:
240
+ // install only SYMLINKS the package and skips its transitive deps, so the wdio/appium runtime never
241
+ // lands in the project. --install-links makes npm COPY it and install those deps. Registry specs
242
+ // install deps normally, so the flag is omitted there.
243
+ const isFileSpec = driverSpec.startsWith('file:');
244
+
245
+ // Provisioned only when BOTH the driver dep AND its appium runtime are present — a driver that was
246
+ // symlinked earlier (no runtime) must be re-installed (with --install-links) to actually pull appium.
247
+ let hasDriverDep = false;
248
+ try {
249
+ const pkgJsonPath = path.join(cwd, 'package.json');
250
+ if (fs.existsSync(pkgJsonPath)) {
251
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
252
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
253
+ hasDriverDep = !!allDeps['@sungen/driver-mobile'];
254
+ }
255
+ } catch {
256
+ // proceed to install
257
+ }
258
+ const runtimeReady = hasDriverDep && appiumBinPath(cwd) !== null;
259
+
260
+ let driverOk = runtimeReady;
261
+ if (!runtimeReady) {
262
+ console.log(`📦 Installing mobile driver + runtime (${driverSpec})...`);
263
+ driverOk = npmInstallDev(cwd, [driverSpec], isFileSpec ? ['--install-links'] : []);
264
+ if (!driverOk) {
265
+ console.warn(
266
+ `⚠️ Could not install ${driverSpec} — scaffold is complete but the mobile driver is not installed.\n` +
267
+ ` Install manually later: npm install -D ${driverSpec}${isFileSpec ? ' --install-links' : ''}`,
268
+ );
269
+ }
270
+ }
271
+
272
+ // Appium platform driver(s) — REQUIRED for any session (appium-mcp live capture AND the wdio runner).
273
+ // NOT npm packages, so installed via the appium CLI here, by the same code path that installs the driver.
274
+ installAppiumPlatformDrivers(cwd);
275
+
276
+ return driverOk;
277
+ }
@@ -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)) {