@unotest/mobile 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to `@unotest/mobile` will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.1.1] — 2026-05-14
8
+
9
+ ### Changed
10
+
11
+ - `doctor` no longer warns when `package.json` lacks `react-native` / `expo`.
12
+ The harness drives iOS via WebDriverAgent, which works against any iOS app
13
+ (RN/Expo, native Swift/SwiftUI/Obj-C, Flutter exposing accessibility
14
+ semantics). The check is now an informational hint about the kind of
15
+ project detected, with guidance on `accessibilityIdentifier` for native
16
+ apps.
17
+
7
18
  ## [0.1.0] — 2026-05-14
8
19
 
9
20
  Initial public release.
@@ -101,22 +101,22 @@ function runEnvironmentChecks(opts = {}) {
101
101
  const pkg = JSON.parse(pkgRaw);
102
102
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
103
103
  const isRn = "react-native" in deps || "expo" in deps;
104
- if (!isRn) {
104
+ if (isRn) {
105
+ results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
106
+ } else {
105
107
  results.push({
106
- name: "rn-project",
107
- severity: "warning",
108
- message: "package.json doesn't declare react-native or expo.",
109
- detail: "unotest-mobile is iOS RN/Expo-focused. Continuing \u2014 but make sure this is intentional."
108
+ name: "project-type",
109
+ severity: "ok",
110
+ message: "no React Native / Expo dependency in package.json",
111
+ detail: "Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. Selectors resolve via the iOS accessibility tree \u2014 set accessibilityIdentifier on the views you want to target."
110
112
  });
111
- } else {
112
- results.push({ name: "rn-project", severity: "ok", message: "React Native / Expo detected" });
113
113
  }
114
114
  } catch {
115
115
  results.push({
116
- name: "rn-project",
117
- severity: "warning",
118
- message: "No readable package.json in current directory.",
119
- detail: "Run init from your project's root."
116
+ name: "project-type",
117
+ severity: "ok",
118
+ message: "no package.json in current directory",
119
+ detail: "That's fine for native iOS projects. If this is a Node-based project, run from its root so unotest-mobile can detect React Native / Expo."
120
120
  });
121
121
  }
122
122
  return results;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runner/init/environment-check.ts","../../src/runner/doctor.ts"],"sourcesContent":["// Environment validation for `unotest-mobile init` and `doctor`. Each check\n// is small + injectable (exec function) for unit testing without touching\n// the host environment.\n\nimport { execFileSync } from \"node:child_process\";\n\nexport type CheckSeverity = \"ok\" | \"warning\" | \"error\";\n\nexport interface CheckResult {\n name: string;\n severity: CheckSeverity;\n message: string;\n detail?: string;\n}\n\nexport interface EnvironmentCheckOptions {\n allowNonMacos?: boolean;\n // Injectable for tests:\n platform?: NodeJS.Platform;\n nodeVersion?: string;\n exec?: (cmd: string, args: string[]) => string;\n}\n\nclass ExecError extends Error {\n constructor(public readonly cmd: string, public readonly args: string[]) {\n super(`${cmd} ${args.join(\" \")} failed`);\n }\n}\n\nfunction defaultExec(cmd: string, args: string[]): string {\n try {\n return execFileSync(cmd, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n } catch {\n throw new ExecError(cmd, args);\n }\n}\n\nexport function runEnvironmentChecks(opts: EnvironmentCheckOptions = {}): CheckResult[] {\n const results: CheckResult[] = [];\n const platform = opts.platform ?? process.platform;\n const nodeVersion = opts.nodeVersion ?? process.versions.node;\n const exec = opts.exec ?? defaultExec;\n\n // 1. Platform — hard requirement for iOS Simulator (Apple-only).\n if (platform !== \"darwin\") {\n results.push({\n name: \"platform\",\n severity: opts.allowNonMacos ? \"warning\" : \"error\",\n message: `macOS required (got \"${platform}\"). iOS simulators only run on macOS (Apple licensing).`,\n detail: opts.allowNonMacos\n ? \"Continuing with --allow-non-macos. Tests cannot run without a macOS host.\"\n : \"Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner).\",\n });\n if (!opts.allowNonMacos) return results;\n } else {\n results.push({ name: \"platform\", severity: \"ok\", message: \"macOS detected\" });\n }\n\n // 2. Node version.\n const major = Number.parseInt(nodeVersion.split(\".\")[0] ?? \"0\", 10);\n if (major < 20) {\n results.push({\n name: \"node\",\n severity: \"error\",\n message: `Node 20+ required (got ${nodeVersion}).`,\n detail: \"Upgrade via nvm/fnm/volta.\",\n });\n return results;\n }\n results.push({ name: \"node\", severity: \"ok\", message: `Node ${nodeVersion}` });\n\n // Mac-only checks below.\n if (platform !== \"darwin\") return results;\n\n // 3. Xcode CLI tools.\n try {\n const xcodePath = exec(\"xcode-select\", [\"-p\"]).trim();\n results.push({ name: \"xcode-cli\", severity: \"ok\", message: `Xcode CLI tools at ${xcodePath}` });\n } catch {\n results.push({\n name: \"xcode-cli\",\n severity: \"error\",\n message: \"Xcode Command Line Tools not found.\",\n detail: \"Install: `xcode-select --install`\",\n });\n return results;\n }\n\n // 4. xcrun simctl.\n try {\n exec(\"xcrun\", [\"simctl\", \"help\"]);\n results.push({ name: \"simctl\", severity: \"ok\", message: \"xcrun simctl available\" });\n } catch {\n results.push({\n name: \"simctl\",\n severity: \"error\",\n message: \"xcrun simctl not available.\",\n detail: \"Install full Xcode (from Mac App Store), not just Command Line Tools.\",\n });\n return results;\n }\n\n // 5. iOS simulators present.\n try {\n const json = exec(\"xcrun\", [\"simctl\", \"list\", \"devices\", \"available\", \"--json\"]);\n const parsed = JSON.parse(json) as { devices: Record<string, unknown[]> };\n const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);\n if (total === 0) {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"No iOS simulators found.\",\n detail: \"Create one in Xcode → Window → Devices and Simulators → '+'.\",\n });\n } else {\n results.push({ name: \"simulators\", severity: \"ok\", message: `${total} simulators available` });\n }\n } catch {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"Could not enumerate simulators.\",\n });\n }\n\n // 6. Project type — soft warning if not RN/Expo.\n try {\n const pkgRaw = exec(\"cat\", [\"package.json\"]);\n const pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n const isRn = \"react-native\" in deps || \"expo\" in deps;\n if (!isRn) {\n results.push({\n name: \"rn-project\",\n severity: \"warning\",\n message: \"package.json doesn't declare react-native or expo.\",\n detail: \"unotest-mobile is iOS RN/Expo-focused. Continuing — but make sure this is intentional.\",\n });\n } else {\n results.push({ name: \"rn-project\", severity: \"ok\", message: \"React Native / Expo detected\" });\n }\n } catch {\n results.push({\n name: \"rn-project\",\n severity: \"warning\",\n message: \"No readable package.json in current directory.\",\n detail: \"Run init from your project's root.\",\n });\n }\n\n return results;\n}\n","// `unotest-mobile doctor` — re-run environment checks without touching files.\n// Useful when init was run earlier but something changed (Xcode updated,\n// sims deleted, etc.).\n\nimport { runEnvironmentChecks } from \"./init/environment-check.ts\";\n\nfunction symbol(severity: \"ok\" | \"warning\" | \"error\"): string {\n return severity === \"ok\" ? \"✓\" : severity === \"warning\" ? \"⚠\" : \"✗\";\n}\n\nfunction main(): number {\n const argv = process.argv.slice(2);\n const allowNonMacos = argv.includes(\"--allow-non-macos\");\n\n console.log(\"unotest-mobile doctor\\n\");\n const checks = runEnvironmentChecks({ allowNonMacos });\n\n let hardFailed = false;\n let warnings = 0;\n for (const c of checks) {\n console.log(`${symbol(c.severity)} ${c.name}: ${c.message}`);\n if (c.detail) console.log(` ${c.detail}`);\n if (c.severity === \"error\") hardFailed = true;\n if (c.severity === \"warning\") warnings++;\n }\n\n if (hardFailed) {\n console.error(\"\\nEnvironment not ready.\");\n return 1;\n }\n if (warnings > 0) {\n console.log(`\\n${warnings} warning(s). Tests may still work, but review them.`);\n return 0;\n }\n console.log(\"\\nEnvironment OK.\");\n return 0;\n}\n\nprocess.exit(main());\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/runner/init/environment-check.ts","../../src/runner/doctor.ts"],"sourcesContent":["// Environment validation for `unotest-mobile init` and `doctor`. Each check\n// is small + injectable (exec function) for unit testing without touching\n// the host environment.\n\nimport { execFileSync } from \"node:child_process\";\n\nexport type CheckSeverity = \"ok\" | \"warning\" | \"error\";\n\nexport interface CheckResult {\n name: string;\n severity: CheckSeverity;\n message: string;\n detail?: string;\n}\n\nexport interface EnvironmentCheckOptions {\n allowNonMacos?: boolean;\n // Injectable for tests:\n platform?: NodeJS.Platform;\n nodeVersion?: string;\n exec?: (cmd: string, args: string[]) => string;\n}\n\nclass ExecError extends Error {\n constructor(public readonly cmd: string, public readonly args: string[]) {\n super(`${cmd} ${args.join(\" \")} failed`);\n }\n}\n\nfunction defaultExec(cmd: string, args: string[]): string {\n try {\n return execFileSync(cmd, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n } catch {\n throw new ExecError(cmd, args);\n }\n}\n\nexport function runEnvironmentChecks(opts: EnvironmentCheckOptions = {}): CheckResult[] {\n const results: CheckResult[] = [];\n const platform = opts.platform ?? process.platform;\n const nodeVersion = opts.nodeVersion ?? process.versions.node;\n const exec = opts.exec ?? defaultExec;\n\n // 1. Platform — hard requirement for iOS Simulator (Apple-only).\n if (platform !== \"darwin\") {\n results.push({\n name: \"platform\",\n severity: opts.allowNonMacos ? \"warning\" : \"error\",\n message: `macOS required (got \"${platform}\"). iOS simulators only run on macOS (Apple licensing).`,\n detail: opts.allowNonMacos\n ? \"Continuing with --allow-non-macos. Tests cannot run without a macOS host.\"\n : \"Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner).\",\n });\n if (!opts.allowNonMacos) return results;\n } else {\n results.push({ name: \"platform\", severity: \"ok\", message: \"macOS detected\" });\n }\n\n // 2. Node version.\n const major = Number.parseInt(nodeVersion.split(\".\")[0] ?? \"0\", 10);\n if (major < 20) {\n results.push({\n name: \"node\",\n severity: \"error\",\n message: `Node 20+ required (got ${nodeVersion}).`,\n detail: \"Upgrade via nvm/fnm/volta.\",\n });\n return results;\n }\n results.push({ name: \"node\", severity: \"ok\", message: `Node ${nodeVersion}` });\n\n // Mac-only checks below.\n if (platform !== \"darwin\") return results;\n\n // 3. Xcode CLI tools.\n try {\n const xcodePath = exec(\"xcode-select\", [\"-p\"]).trim();\n results.push({ name: \"xcode-cli\", severity: \"ok\", message: `Xcode CLI tools at ${xcodePath}` });\n } catch {\n results.push({\n name: \"xcode-cli\",\n severity: \"error\",\n message: \"Xcode Command Line Tools not found.\",\n detail: \"Install: `xcode-select --install`\",\n });\n return results;\n }\n\n // 4. xcrun simctl.\n try {\n exec(\"xcrun\", [\"simctl\", \"help\"]);\n results.push({ name: \"simctl\", severity: \"ok\", message: \"xcrun simctl available\" });\n } catch {\n results.push({\n name: \"simctl\",\n severity: \"error\",\n message: \"xcrun simctl not available.\",\n detail: \"Install full Xcode (from Mac App Store), not just Command Line Tools.\",\n });\n return results;\n }\n\n // 5. iOS simulators present.\n try {\n const json = exec(\"xcrun\", [\"simctl\", \"list\", \"devices\", \"available\", \"--json\"]);\n const parsed = JSON.parse(json) as { devices: Record<string, unknown[]> };\n const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);\n if (total === 0) {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"No iOS simulators found.\",\n detail: \"Create one in Xcode → Window → Devices and Simulators → '+'.\",\n });\n } else {\n results.push({ name: \"simulators\", severity: \"ok\", message: `${total} simulators available` });\n }\n } catch {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"Could not enumerate simulators.\",\n });\n }\n\n // 6. Project type — informational hint about the kind of app detected.\n // unotest-mobile drives iOS via WebDriverAgent, which works against any\n // iOS app (RN/Expo, native Swift/SwiftUI, Flutter exposing a11y semantics)\n // — selectors resolve against the iOS accessibility tree, not RN-specific\n // hooks. The RN/Expo detection is a hint, not a requirement.\n try {\n const pkgRaw = exec(\"cat\", [\"package.json\"]);\n const pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n const isRn = \"react-native\" in deps || \"expo\" in deps;\n if (isRn) {\n results.push({ name: \"project-type\", severity: \"ok\", message: \"React Native / Expo detected\" });\n } else {\n results.push({\n name: \"project-type\",\n severity: \"ok\",\n message: \"no React Native / Expo dependency in package.json\",\n detail:\n \"Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. \" +\n \"Selectors resolve via the iOS accessibility tree — set accessibilityIdentifier \" +\n \"on the views you want to target.\",\n });\n }\n } catch {\n results.push({\n name: \"project-type\",\n severity: \"ok\",\n message: \"no package.json in current directory\",\n detail:\n \"That's fine for native iOS projects. If this is a Node-based project, \" +\n \"run from its root so unotest-mobile can detect React Native / Expo.\",\n });\n }\n\n return results;\n}\n","// `unotest-mobile doctor` — re-run environment checks without touching files.\n// Useful when init was run earlier but something changed (Xcode updated,\n// sims deleted, etc.).\n\nimport { runEnvironmentChecks } from \"./init/environment-check.ts\";\n\nfunction symbol(severity: \"ok\" | \"warning\" | \"error\"): string {\n return severity === \"ok\" ? \"✓\" : severity === \"warning\" ? \"⚠\" : \"✗\";\n}\n\nfunction main(): number {\n const argv = process.argv.slice(2);\n const allowNonMacos = argv.includes(\"--allow-non-macos\");\n\n console.log(\"unotest-mobile doctor\\n\");\n const checks = runEnvironmentChecks({ allowNonMacos });\n\n let hardFailed = false;\n let warnings = 0;\n for (const c of checks) {\n console.log(`${symbol(c.severity)} ${c.name}: ${c.message}`);\n if (c.detail) console.log(` ${c.detail}`);\n if (c.severity === \"error\") hardFailed = true;\n if (c.severity === \"warning\") warnings++;\n }\n\n if (hardFailed) {\n console.error(\"\\nEnvironment not ready.\");\n return 1;\n }\n if (warnings > 0) {\n console.log(`\\n${warnings} warning(s). Tests may still work, but review them.`);\n return 0;\n }\n console.log(\"\\nEnvironment OK.\");\n return 0;\n}\n\nprocess.exit(main());\n"],"mappings":"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","names":[]}
@@ -106,22 +106,22 @@ function runEnvironmentChecks(opts = {}) {
106
106
  const pkg = JSON.parse(pkgRaw);
107
107
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
108
108
  const isRn = "react-native" in deps || "expo" in deps;
109
- if (!isRn) {
109
+ if (isRn) {
110
+ results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
111
+ } else {
110
112
  results.push({
111
- name: "rn-project",
112
- severity: "warning",
113
- message: "package.json doesn't declare react-native or expo.",
114
- detail: "unotest-mobile is iOS RN/Expo-focused. Continuing \u2014 but make sure this is intentional."
113
+ name: "project-type",
114
+ severity: "ok",
115
+ message: "no React Native / Expo dependency in package.json",
116
+ detail: "Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. Selectors resolve via the iOS accessibility tree \u2014 set accessibilityIdentifier on the views you want to target."
115
117
  });
116
- } else {
117
- results.push({ name: "rn-project", severity: "ok", message: "React Native / Expo detected" });
118
118
  }
119
119
  } catch {
120
120
  results.push({
121
- name: "rn-project",
122
- severity: "warning",
123
- message: "No readable package.json in current directory.",
124
- detail: "Run init from your project's root."
121
+ name: "project-type",
122
+ severity: "ok",
123
+ message: "no package.json in current directory",
124
+ detail: "That's fine for native iOS projects. If this is a Node-based project, run from its root so unotest-mobile can detect React Native / Expo."
125
125
  });
126
126
  }
127
127
  return results;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runner/init.ts","../../src/runner/init/environment-check.ts","../../src/runner/init/file-templates.ts","../../src/runner/init/mcp-config-merger.ts","../../src/runner/init/gitignore-updater.ts"],"sourcesContent":["// `unotest-mobile init` — bootstrap a consumer project.\n//\n// Creates the canonical layout (unotest/e2e/, _helpers/, starter scenario),\n// drops a Claude Code skill, registers MCP via .mcp.json, seeds\n// unotest/.env.example + unotest/.env, updates .gitignore.\n//\n// Idempotent: skips existing files unless --force. Never overwrites\n// unotest/.env (too risky — user secrets live there).\n//\n// Environment-check first — hard-fail on non-macOS, missing Xcode, etc.\n// before touching the filesystem, so the user gets one clear error.\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { runEnvironmentChecks } from \"./init/environment-check.ts\";\nimport { templates } from \"./init/file-templates.ts\";\nimport { mergeMcpConfig, McpJsonParseError } from \"./init/mcp-config-merger.ts\";\nimport { appendUniqueLines } from \"./init/gitignore-updater.ts\";\n\nconst here = dirname(fileURLToPath(import.meta.url));\n// dist/runner/init.js → package root\nconst packageRoot = resolve(here, \"..\", \"..\");\n\ninterface InitOptions {\n force: boolean;\n allowNonMacos: boolean;\n}\n\nfunction parseArgs(argv: string[]): InitOptions {\n return {\n force: argv.includes(\"--force\"),\n allowNonMacos: argv.includes(\"--allow-non-macos\"),\n };\n}\n\nfunction symbol(severity: \"ok\" | \"warning\" | \"error\"): string {\n return severity === \"ok\" ? \"✓\" : severity === \"warning\" ? \"⚠\" : \"✗\";\n}\n\nfunction ensureDir(p: string): void {\n if (!existsSync(p)) mkdirSync(p, { recursive: true });\n}\n\nfunction writeIfNeeded(\n filePath: string,\n content: string,\n force: boolean,\n): \"created\" | \"skipped\" | \"overwrote\" {\n const exists = existsSync(filePath);\n if (exists && !force) return \"skipped\";\n ensureDir(dirname(filePath));\n writeFileSync(filePath, content);\n return exists ? \"overwrote\" : \"created\";\n}\n\nfunction readPackageFile(relativePath: string): string | null {\n const abs = resolve(packageRoot, relativePath);\n if (!existsSync(abs)) return null;\n return readFileSync(abs, \"utf8\");\n}\n\nfunction main(): number {\n const opts = parseArgs(process.argv.slice(2));\n const target = process.cwd();\n\n console.log(\"unotest-mobile init — bootstrapping project\\n\");\n\n // ─── Environment ──────────────────────────────────────────────────────\n console.log(\"Environment:\");\n const checks = runEnvironmentChecks({ allowNonMacos: opts.allowNonMacos });\n let hardFailed = false;\n for (const c of checks) {\n console.log(` ${symbol(c.severity)} ${c.name}: ${c.message}`);\n if (c.detail) console.log(` ${c.detail}`);\n if (c.severity === \"error\") hardFailed = true;\n }\n if (hardFailed) {\n console.error(\"\\nFix the errors above and re-run.\");\n return 1;\n }\n\n // ─── Files ────────────────────────────────────────────────────────────\n console.log(\"\\nFiles:\");\n const summary: Array<{ path: string; status: string }> = [];\n\n // Starter scenarios + helpers dir\n ensureDir(join(target, \"unotest/e2e/_helpers\"));\n summary.push({\n path: \"unotest/e2e/smoke-welcome.js\",\n status: writeIfNeeded(join(target, \"unotest/e2e/smoke-welcome.js\"), templates.smokeWelcome, opts.force),\n });\n summary.push({\n path: \"unotest/e2e/_template.js\",\n status: writeIfNeeded(join(target, \"unotest/e2e/_template.js\"), templates.scenarioTemplate, opts.force),\n });\n summary.push({\n path: \"unotest/AGENTS.md\",\n status: writeIfNeeded(join(target, \"unotest/AGENTS.md\"), templates.agentsMd, opts.force),\n });\n\n // Skill (copy from package)\n const skillSrc = readPackageFile(\".claude/skills/write-e2e-test.md\");\n if (skillSrc !== null) {\n summary.push({\n path: \".claude/skills/write-e2e-test.md\",\n status: writeIfNeeded(join(target, \".claude/skills/write-e2e-test.md\"), skillSrc, opts.force),\n });\n } else {\n summary.push({ path: \".claude/skills/write-e2e-test.md\", status: \"missing-in-package\" });\n }\n\n // .mcp.json — merge\n const mcpPath = join(target, \".mcp.json\");\n try {\n const existing = existsSync(mcpPath) ? readFileSync(mcpPath, \"utf8\") : null;\n const merge = mergeMcpConfig(\n existing,\n \"unotest-mobile\",\n {\n command: templates.mcpServerEntry.command,\n args: [...templates.mcpServerEntry.args],\n },\n { force: opts.force },\n );\n if (merge.action !== \"already-present\") {\n writeFileSync(mcpPath, merge.newContent);\n }\n summary.push({ path: \".mcp.json\", status: merge.action });\n } catch (e) {\n if (e instanceof McpJsonParseError) {\n console.error(`\\n✗ .mcp.json: ${e.message}`);\n return 1;\n }\n throw e;\n }\n\n // unotest/.env.example — reference template, committed by user. Always\n // written/refreshed (no secrets, so overwriting is safe and keeps the\n // reference in sync with the installed package version).\n const envExamplePath = join(target, \"unotest/.env.example\");\n summary.push({\n path: \"unotest/.env.example\",\n status: writeIfNeeded(envExamplePath, templates.envExample, true),\n });\n\n // unotest/.env — copied from the example. NEVER overwrite (user secrets).\n const envDst = join(target, \"unotest/.env\");\n if (!existsSync(envDst)) {\n writeFileSync(envDst, templates.envExample);\n summary.push({ path: \"unotest/.env\", status: \"created\" });\n } else {\n summary.push({ path: \"unotest/.env\", status: \"skipped (exists — fill in manually)\" });\n }\n\n // .gitignore — append unique\n const gitignorePath = join(target, \".gitignore\");\n const gitignoreExisting = existsSync(gitignorePath) ? readFileSync(gitignorePath, \"utf8\") : null;\n const giUpdate = appendUniqueLines(gitignoreExisting, [...templates.gitignoreLines]);\n if (giUpdate.added.length > 0) {\n writeFileSync(gitignorePath, giUpdate.newContent);\n summary.push({ path: \".gitignore\", status: `added ${giUpdate.added.length} line(s)` });\n } else {\n summary.push({ path: \".gitignore\", status: \"already up-to-date\" });\n }\n\n // Print summary\n for (const item of summary) {\n const symbolForStatus = item.status === \"created\" || item.status.startsWith(\"added\")\n ? \"✓\"\n : item.status === \"skipped\" || item.status.startsWith(\"already\")\n ? \"·\"\n : item.status === \"overwrote\" || item.status === \"force-overwrote\"\n ? \"↻\"\n : \"?\";\n console.log(` ${symbolForStatus} ${item.path} — ${item.status}`);\n }\n\n // Next steps\n console.log(`\nNext:\n 1. Edit ${relative(target, envDst) || \"unotest/.env\"} with your project's values\n (DATABASE_URL, SIM_A_NAME, APP_BUNDLE_ID, ...)\n 2. Boot iOS simulators matching SIM_A_NAME / SIM_B_NAME (Xcode → Devices)\n 3. Open Claude Code in this directory — MCP server auto-registers via\n .mcp.json. Ask the agent to write a test.\n 4. CLI: \\`npx @unotest/mobile e2e smoke-welcome\\` to run the starter.\n\nRe-check environment anytime: \\`npx @unotest/mobile doctor\\`\n`);\n return 0;\n}\n\nprocess.exit(main());\n","// Environment validation for `unotest-mobile init` and `doctor`. Each check\n// is small + injectable (exec function) for unit testing without touching\n// the host environment.\n\nimport { execFileSync } from \"node:child_process\";\n\nexport type CheckSeverity = \"ok\" | \"warning\" | \"error\";\n\nexport interface CheckResult {\n name: string;\n severity: CheckSeverity;\n message: string;\n detail?: string;\n}\n\nexport interface EnvironmentCheckOptions {\n allowNonMacos?: boolean;\n // Injectable for tests:\n platform?: NodeJS.Platform;\n nodeVersion?: string;\n exec?: (cmd: string, args: string[]) => string;\n}\n\nclass ExecError extends Error {\n constructor(public readonly cmd: string, public readonly args: string[]) {\n super(`${cmd} ${args.join(\" \")} failed`);\n }\n}\n\nfunction defaultExec(cmd: string, args: string[]): string {\n try {\n return execFileSync(cmd, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n } catch {\n throw new ExecError(cmd, args);\n }\n}\n\nexport function runEnvironmentChecks(opts: EnvironmentCheckOptions = {}): CheckResult[] {\n const results: CheckResult[] = [];\n const platform = opts.platform ?? process.platform;\n const nodeVersion = opts.nodeVersion ?? process.versions.node;\n const exec = opts.exec ?? defaultExec;\n\n // 1. Platform — hard requirement for iOS Simulator (Apple-only).\n if (platform !== \"darwin\") {\n results.push({\n name: \"platform\",\n severity: opts.allowNonMacos ? \"warning\" : \"error\",\n message: `macOS required (got \"${platform}\"). iOS simulators only run on macOS (Apple licensing).`,\n detail: opts.allowNonMacos\n ? \"Continuing with --allow-non-macos. Tests cannot run without a macOS host.\"\n : \"Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner).\",\n });\n if (!opts.allowNonMacos) return results;\n } else {\n results.push({ name: \"platform\", severity: \"ok\", message: \"macOS detected\" });\n }\n\n // 2. Node version.\n const major = Number.parseInt(nodeVersion.split(\".\")[0] ?? \"0\", 10);\n if (major < 20) {\n results.push({\n name: \"node\",\n severity: \"error\",\n message: `Node 20+ required (got ${nodeVersion}).`,\n detail: \"Upgrade via nvm/fnm/volta.\",\n });\n return results;\n }\n results.push({ name: \"node\", severity: \"ok\", message: `Node ${nodeVersion}` });\n\n // Mac-only checks below.\n if (platform !== \"darwin\") return results;\n\n // 3. Xcode CLI tools.\n try {\n const xcodePath = exec(\"xcode-select\", [\"-p\"]).trim();\n results.push({ name: \"xcode-cli\", severity: \"ok\", message: `Xcode CLI tools at ${xcodePath}` });\n } catch {\n results.push({\n name: \"xcode-cli\",\n severity: \"error\",\n message: \"Xcode Command Line Tools not found.\",\n detail: \"Install: `xcode-select --install`\",\n });\n return results;\n }\n\n // 4. xcrun simctl.\n try {\n exec(\"xcrun\", [\"simctl\", \"help\"]);\n results.push({ name: \"simctl\", severity: \"ok\", message: \"xcrun simctl available\" });\n } catch {\n results.push({\n name: \"simctl\",\n severity: \"error\",\n message: \"xcrun simctl not available.\",\n detail: \"Install full Xcode (from Mac App Store), not just Command Line Tools.\",\n });\n return results;\n }\n\n // 5. iOS simulators present.\n try {\n const json = exec(\"xcrun\", [\"simctl\", \"list\", \"devices\", \"available\", \"--json\"]);\n const parsed = JSON.parse(json) as { devices: Record<string, unknown[]> };\n const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);\n if (total === 0) {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"No iOS simulators found.\",\n detail: \"Create one in Xcode → Window → Devices and Simulators → '+'.\",\n });\n } else {\n results.push({ name: \"simulators\", severity: \"ok\", message: `${total} simulators available` });\n }\n } catch {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"Could not enumerate simulators.\",\n });\n }\n\n // 6. Project type — soft warning if not RN/Expo.\n try {\n const pkgRaw = exec(\"cat\", [\"package.json\"]);\n const pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n const isRn = \"react-native\" in deps || \"expo\" in deps;\n if (!isRn) {\n results.push({\n name: \"rn-project\",\n severity: \"warning\",\n message: \"package.json doesn't declare react-native or expo.\",\n detail: \"unotest-mobile is iOS RN/Expo-focused. Continuing — but make sure this is intentional.\",\n });\n } else {\n results.push({ name: \"rn-project\", severity: \"ok\", message: \"React Native / Expo detected\" });\n }\n } catch {\n results.push({\n name: \"rn-project\",\n severity: \"warning\",\n message: \"No readable package.json in current directory.\",\n detail: \"Run init from your project's root.\",\n });\n }\n\n return results;\n}\n","// Static content for files generated by `unotest-mobile init`. Pure\n// strings — no I/O. Caller writes them at the right path.\n\nexport const templates = {\n smokeWelcome: `// id-smoke-welcome\n// First-run sanity: harness reaches sim, WDA, and app launch handshake\n// #00aa00\nfunction test_smoke_welcome() {\n setDevice(\"A\");\n appLaunch(true);\n}\n`,\n\n scenarioTemplate: `// id-<your-scenario-id>\n// <one-line description>\n// #888888\nfunction test_<your_name>() {\n // 1. SETUP — DB / API fixtures (helpers from _helpers/)\n // wipe_e2e_users();\n // user_id = seed_user(\"e2e@example.com\", \"e2e-pass\");\n\n // 2. ENTER — bring app to initial UI state\n setDevice(\"A\");\n appLaunch(true);\n waitFor(getByTestId(\"screen-welcome\"), 15000);\n\n // 3. ACT — actions you're testing\n // signin(\"e2e@example.com\", \"e2e-pass\");\n // tap(getByTestId(\"btn-something\"));\n\n // 4. ASSERT — UI + DB / API checks\n // assertVisible(getByTestId(\"screen-target\"));\n // count = dbQuery(\"SELECT count(*)::text FROM x WHERE ...\");\n // assertEqual(count, \"1\");\n}\n`,\n\n agentsMd: `# AI Agents: how to write e2e tests for this project\n\nThis project uses [\\`@unotest/mobile\\`](https://www.npmjs.com/package/@unotest/mobile)\nfor end-to-end testing of iOS React Native flows.\n\n## Layout\n\n- \\`unotest/e2e/\\` — scenarios (one \\`test_*\\` entry function per file)\n- \\`unotest/e2e/_helpers/\\` — project-specific helpers, visible to all scenarios\n- \\`unotest/e2e/<feature>/\\` — feature-scoped subfolders (recommended)\n\n## How to write a test\n\nThe full guide — DSL functions, scenario shape, helper rules, linter codes,\ndebugging — is the \\`write-e2e-test\\` Claude Code skill at\n\\`.claude/skills/write-e2e-test.md\\`. It is the single source of truth for\nboth Claude Code and other AI agents. Read it before writing tests.\n\n## Running\n\n- \\`npx @unotest/mobile e2e <name>\\` — run \\`unotest/e2e/<name>.js\\`\n- \\`npx @unotest/mobile lint\\` — static check of all scenarios\n- \\`npx @unotest/mobile doctor\\` — re-check environment\n\nInside Claude Code, prefer the MCP \\`run_test\\` tool with\n\\`pauseOnFailure: true\\` — it pauses on the failed step so you can\n\\`inspect_runtime\\`, fix the scenario, and \\`resume\\`.\n`,\n\n envExample: `# unotest-mobile — copied to unotest/.env by \\`init\\`. Fill in below.\n\n# --- App under test --------------------------------------------------------\nAPP_BUNDLE_ID=com.example.myapp\nAPP_URL_SCHEME=myapp\nINVITE_DEEPLINK_PREFIX=myapp://invite/\n\n# --- Backend ---------------------------------------------------------------\nAPI_BASE_URL=http://localhost:3000/api\n# Optional. Absolute path used as default cwd for the \\`shell(...)\\` DSL\n# primitive. Set if your scenarios shell out to project-local CLIs\n# (pnpm cli, rails runner, etc.) that must run from the project root.\nPROJECT_ROOT=\n\n# --- Database --------------------------------------------------------------\n# Pick the driver matching your backend. Install peer-deps only as needed:\n# npm i -D pg @types/pg # for postgresql://\n# npm i -D mysql2 # for mysql://\n# (SQLite via better-sqlite3 is bundled — no separate install.)\n#\n# Docker-compose Postgres/MySQL — ensure host port is exposed\n# (\\`ports: [\"5432:5432\"]\\`). Native clients connect from the host, not from\n# inside the compose network.\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_test\n\n# --- Simulators ------------------------------------------------------------\n# Names must match \\`xcrun simctl list devices\\`.\nSIM_A_NAME=iPhone 15\nSIM_B_NAME=iPhone 15 Plus\nSIM_POOL=A,B\n\n# --- Dev tooling -----------------------------------------------------------\nMETRO_URL=http://localhost:8081\n# Expo dev-client: auto-open dev-client deep link after a clean launch.\nEXPO_DEV_CLIENT=false\nSESSION_LOG_PATH=sessions/current.jsonl\nARTIFACTS_DIR=artifacts\n\n# --- WebDriverAgent --------------------------------------------------------\n# Per-slot WDA ports. Each slot in SIM_POOL needs one. Format: \"slot=port\".\nWDA_PORTS=A=8100,B=8101\nWDA_DEFAULT_ACTION_WAIT_MS=2000\nWDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000\n\n# --- D-17 paused-runtime TTL ----------------------------------------------\n# How long a paused-on-failure runtime stays alive before auto-abort (ms).\n# Default 30 minutes. Inspecting the runtime resets the timer.\nPAUSED_RUNTIME_TTL_MS=1800000\n`,\n\n gitignoreLines: [\n \"\",\n \"# unotest-mobile\",\n \"unotest/.env\",\n \"unotest/artifacts/\",\n \"unotest/sessions/\",\n ],\n\n mcpServerEntry: {\n command: \"npx\",\n args: [\"-y\", \"@unotest/mobile\"],\n } as const,\n};\n","// `.mcp.json` merger — preserves other registered MCP servers when adding\n// the unotest-mobile entry. Pure functions over JSON content; FS is split\n// out so callers can unit-test the merge logic without touching disk.\n\nexport interface McpServerConfig {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\nexport interface McpConfig {\n mcpServers?: Record<string, McpServerConfig>;\n}\n\nexport type McpMergeAction = \"created\" | \"added\" | \"already-present\" | \"force-overwrote\";\n\nexport interface McpMergeResult {\n action: McpMergeAction;\n newContent: string;\n}\n\nexport class McpJsonParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"McpJsonParseError\";\n }\n}\n\n/**\n * Compute the new content of `.mcp.json` given the existing source (or\n * `null` if the file doesn't exist yet) and the server entry to add.\n * Returns the merged JSON text plus an action label for caller logging.\n */\nexport function mergeMcpConfig(\n existingSource: string | null,\n serverName: string,\n serverConfig: McpServerConfig,\n options: { force?: boolean } = {},\n): McpMergeResult {\n if (existingSource === null) {\n const content = JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2) + \"\\n\";\n return { action: \"created\", newContent: content };\n }\n\n let parsed: McpConfig;\n try {\n parsed = JSON.parse(existingSource) as McpConfig;\n } catch (e) {\n throw new McpJsonParseError(\n `Failed to parse .mcp.json as JSON: ${(e as Error).message}. ` +\n `Fix or remove the file and re-run init.`,\n );\n }\n\n if (!parsed.mcpServers || typeof parsed.mcpServers !== \"object\") {\n parsed.mcpServers = {};\n }\n\n const alreadyPresent = serverName in parsed.mcpServers;\n if (alreadyPresent && !options.force) {\n return { action: \"already-present\", newContent: existingSource };\n }\n\n parsed.mcpServers[serverName] = serverConfig;\n const content = JSON.stringify(parsed, null, 2) + \"\\n\";\n return {\n action: alreadyPresent ? \"force-overwrote\" : \"added\",\n newContent: content,\n };\n}\n","// Idempotent append-unique-lines for .gitignore (or any line-based file).\n// Pure function over content — caller handles FS.\n\nexport interface GitignoreUpdate {\n added: string[];\n alreadyPresent: string[];\n newContent: string;\n}\n\nexport function appendUniqueLines(\n existingContent: string | null,\n linesToAdd: string[],\n): GitignoreUpdate {\n const existing = existingContent ?? \"\";\n const existingLines = new Set(existing.split(\"\\n\").map((l) => l.trim()));\n const added: string[] = [];\n const alreadyPresent: string[] = [];\n\n for (const line of linesToAdd) {\n const trimmed = line.trim();\n if (trimmed === \"\" || existingLines.has(trimmed)) {\n if (trimmed !== \"\") alreadyPresent.push(line);\n continue;\n }\n added.push(line);\n existingLines.add(trimmed);\n }\n\n if (added.length === 0) {\n return { added, alreadyPresent, newContent: existing };\n }\n\n const sep = existing === \"\" ? \"\" : existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n const newContent = existing + sep + added.join(\"\\n\") + \"\\n\";\n return { added, alreadyPresent, newContent };\n}\n"],"mappings":"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","names":["content"]}
1
+ {"version":3,"sources":["../../src/runner/init.ts","../../src/runner/init/environment-check.ts","../../src/runner/init/file-templates.ts","../../src/runner/init/mcp-config-merger.ts","../../src/runner/init/gitignore-updater.ts"],"sourcesContent":["// `unotest-mobile init` — bootstrap a consumer project.\n//\n// Creates the canonical layout (unotest/e2e/, _helpers/, starter scenario),\n// drops a Claude Code skill, registers MCP via .mcp.json, seeds\n// unotest/.env.example + unotest/.env, updates .gitignore.\n//\n// Idempotent: skips existing files unless --force. Never overwrites\n// unotest/.env (too risky — user secrets live there).\n//\n// Environment-check first — hard-fail on non-macOS, missing Xcode, etc.\n// before touching the filesystem, so the user gets one clear error.\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { runEnvironmentChecks } from \"./init/environment-check.ts\";\nimport { templates } from \"./init/file-templates.ts\";\nimport { mergeMcpConfig, McpJsonParseError } from \"./init/mcp-config-merger.ts\";\nimport { appendUniqueLines } from \"./init/gitignore-updater.ts\";\n\nconst here = dirname(fileURLToPath(import.meta.url));\n// dist/runner/init.js → package root\nconst packageRoot = resolve(here, \"..\", \"..\");\n\ninterface InitOptions {\n force: boolean;\n allowNonMacos: boolean;\n}\n\nfunction parseArgs(argv: string[]): InitOptions {\n return {\n force: argv.includes(\"--force\"),\n allowNonMacos: argv.includes(\"--allow-non-macos\"),\n };\n}\n\nfunction symbol(severity: \"ok\" | \"warning\" | \"error\"): string {\n return severity === \"ok\" ? \"✓\" : severity === \"warning\" ? \"⚠\" : \"✗\";\n}\n\nfunction ensureDir(p: string): void {\n if (!existsSync(p)) mkdirSync(p, { recursive: true });\n}\n\nfunction writeIfNeeded(\n filePath: string,\n content: string,\n force: boolean,\n): \"created\" | \"skipped\" | \"overwrote\" {\n const exists = existsSync(filePath);\n if (exists && !force) return \"skipped\";\n ensureDir(dirname(filePath));\n writeFileSync(filePath, content);\n return exists ? \"overwrote\" : \"created\";\n}\n\nfunction readPackageFile(relativePath: string): string | null {\n const abs = resolve(packageRoot, relativePath);\n if (!existsSync(abs)) return null;\n return readFileSync(abs, \"utf8\");\n}\n\nfunction main(): number {\n const opts = parseArgs(process.argv.slice(2));\n const target = process.cwd();\n\n console.log(\"unotest-mobile init — bootstrapping project\\n\");\n\n // ─── Environment ──────────────────────────────────────────────────────\n console.log(\"Environment:\");\n const checks = runEnvironmentChecks({ allowNonMacos: opts.allowNonMacos });\n let hardFailed = false;\n for (const c of checks) {\n console.log(` ${symbol(c.severity)} ${c.name}: ${c.message}`);\n if (c.detail) console.log(` ${c.detail}`);\n if (c.severity === \"error\") hardFailed = true;\n }\n if (hardFailed) {\n console.error(\"\\nFix the errors above and re-run.\");\n return 1;\n }\n\n // ─── Files ────────────────────────────────────────────────────────────\n console.log(\"\\nFiles:\");\n const summary: Array<{ path: string; status: string }> = [];\n\n // Starter scenarios + helpers dir\n ensureDir(join(target, \"unotest/e2e/_helpers\"));\n summary.push({\n path: \"unotest/e2e/smoke-welcome.js\",\n status: writeIfNeeded(join(target, \"unotest/e2e/smoke-welcome.js\"), templates.smokeWelcome, opts.force),\n });\n summary.push({\n path: \"unotest/e2e/_template.js\",\n status: writeIfNeeded(join(target, \"unotest/e2e/_template.js\"), templates.scenarioTemplate, opts.force),\n });\n summary.push({\n path: \"unotest/AGENTS.md\",\n status: writeIfNeeded(join(target, \"unotest/AGENTS.md\"), templates.agentsMd, opts.force),\n });\n\n // Skill (copy from package)\n const skillSrc = readPackageFile(\".claude/skills/write-e2e-test.md\");\n if (skillSrc !== null) {\n summary.push({\n path: \".claude/skills/write-e2e-test.md\",\n status: writeIfNeeded(join(target, \".claude/skills/write-e2e-test.md\"), skillSrc, opts.force),\n });\n } else {\n summary.push({ path: \".claude/skills/write-e2e-test.md\", status: \"missing-in-package\" });\n }\n\n // .mcp.json — merge\n const mcpPath = join(target, \".mcp.json\");\n try {\n const existing = existsSync(mcpPath) ? readFileSync(mcpPath, \"utf8\") : null;\n const merge = mergeMcpConfig(\n existing,\n \"unotest-mobile\",\n {\n command: templates.mcpServerEntry.command,\n args: [...templates.mcpServerEntry.args],\n },\n { force: opts.force },\n );\n if (merge.action !== \"already-present\") {\n writeFileSync(mcpPath, merge.newContent);\n }\n summary.push({ path: \".mcp.json\", status: merge.action });\n } catch (e) {\n if (e instanceof McpJsonParseError) {\n console.error(`\\n✗ .mcp.json: ${e.message}`);\n return 1;\n }\n throw e;\n }\n\n // unotest/.env.example — reference template, committed by user. Always\n // written/refreshed (no secrets, so overwriting is safe and keeps the\n // reference in sync with the installed package version).\n const envExamplePath = join(target, \"unotest/.env.example\");\n summary.push({\n path: \"unotest/.env.example\",\n status: writeIfNeeded(envExamplePath, templates.envExample, true),\n });\n\n // unotest/.env — copied from the example. NEVER overwrite (user secrets).\n const envDst = join(target, \"unotest/.env\");\n if (!existsSync(envDst)) {\n writeFileSync(envDst, templates.envExample);\n summary.push({ path: \"unotest/.env\", status: \"created\" });\n } else {\n summary.push({ path: \"unotest/.env\", status: \"skipped (exists — fill in manually)\" });\n }\n\n // .gitignore — append unique\n const gitignorePath = join(target, \".gitignore\");\n const gitignoreExisting = existsSync(gitignorePath) ? readFileSync(gitignorePath, \"utf8\") : null;\n const giUpdate = appendUniqueLines(gitignoreExisting, [...templates.gitignoreLines]);\n if (giUpdate.added.length > 0) {\n writeFileSync(gitignorePath, giUpdate.newContent);\n summary.push({ path: \".gitignore\", status: `added ${giUpdate.added.length} line(s)` });\n } else {\n summary.push({ path: \".gitignore\", status: \"already up-to-date\" });\n }\n\n // Print summary\n for (const item of summary) {\n const symbolForStatus = item.status === \"created\" || item.status.startsWith(\"added\")\n ? \"✓\"\n : item.status === \"skipped\" || item.status.startsWith(\"already\")\n ? \"·\"\n : item.status === \"overwrote\" || item.status === \"force-overwrote\"\n ? \"↻\"\n : \"?\";\n console.log(` ${symbolForStatus} ${item.path} — ${item.status}`);\n }\n\n // Next steps\n console.log(`\nNext:\n 1. Edit ${relative(target, envDst) || \"unotest/.env\"} with your project's values\n (DATABASE_URL, SIM_A_NAME, APP_BUNDLE_ID, ...)\n 2. Boot iOS simulators matching SIM_A_NAME / SIM_B_NAME (Xcode → Devices)\n 3. Open Claude Code in this directory — MCP server auto-registers via\n .mcp.json. Ask the agent to write a test.\n 4. CLI: \\`npx @unotest/mobile e2e smoke-welcome\\` to run the starter.\n\nRe-check environment anytime: \\`npx @unotest/mobile doctor\\`\n`);\n return 0;\n}\n\nprocess.exit(main());\n","// Environment validation for `unotest-mobile init` and `doctor`. Each check\n// is small + injectable (exec function) for unit testing without touching\n// the host environment.\n\nimport { execFileSync } from \"node:child_process\";\n\nexport type CheckSeverity = \"ok\" | \"warning\" | \"error\";\n\nexport interface CheckResult {\n name: string;\n severity: CheckSeverity;\n message: string;\n detail?: string;\n}\n\nexport interface EnvironmentCheckOptions {\n allowNonMacos?: boolean;\n // Injectable for tests:\n platform?: NodeJS.Platform;\n nodeVersion?: string;\n exec?: (cmd: string, args: string[]) => string;\n}\n\nclass ExecError extends Error {\n constructor(public readonly cmd: string, public readonly args: string[]) {\n super(`${cmd} ${args.join(\" \")} failed`);\n }\n}\n\nfunction defaultExec(cmd: string, args: string[]): string {\n try {\n return execFileSync(cmd, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n } catch {\n throw new ExecError(cmd, args);\n }\n}\n\nexport function runEnvironmentChecks(opts: EnvironmentCheckOptions = {}): CheckResult[] {\n const results: CheckResult[] = [];\n const platform = opts.platform ?? process.platform;\n const nodeVersion = opts.nodeVersion ?? process.versions.node;\n const exec = opts.exec ?? defaultExec;\n\n // 1. Platform — hard requirement for iOS Simulator (Apple-only).\n if (platform !== \"darwin\") {\n results.push({\n name: \"platform\",\n severity: opts.allowNonMacos ? \"warning\" : \"error\",\n message: `macOS required (got \"${platform}\"). iOS simulators only run on macOS (Apple licensing).`,\n detail: opts.allowNonMacos\n ? \"Continuing with --allow-non-macos. Tests cannot run without a macOS host.\"\n : \"Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner).\",\n });\n if (!opts.allowNonMacos) return results;\n } else {\n results.push({ name: \"platform\", severity: \"ok\", message: \"macOS detected\" });\n }\n\n // 2. Node version.\n const major = Number.parseInt(nodeVersion.split(\".\")[0] ?? \"0\", 10);\n if (major < 20) {\n results.push({\n name: \"node\",\n severity: \"error\",\n message: `Node 20+ required (got ${nodeVersion}).`,\n detail: \"Upgrade via nvm/fnm/volta.\",\n });\n return results;\n }\n results.push({ name: \"node\", severity: \"ok\", message: `Node ${nodeVersion}` });\n\n // Mac-only checks below.\n if (platform !== \"darwin\") return results;\n\n // 3. Xcode CLI tools.\n try {\n const xcodePath = exec(\"xcode-select\", [\"-p\"]).trim();\n results.push({ name: \"xcode-cli\", severity: \"ok\", message: `Xcode CLI tools at ${xcodePath}` });\n } catch {\n results.push({\n name: \"xcode-cli\",\n severity: \"error\",\n message: \"Xcode Command Line Tools not found.\",\n detail: \"Install: `xcode-select --install`\",\n });\n return results;\n }\n\n // 4. xcrun simctl.\n try {\n exec(\"xcrun\", [\"simctl\", \"help\"]);\n results.push({ name: \"simctl\", severity: \"ok\", message: \"xcrun simctl available\" });\n } catch {\n results.push({\n name: \"simctl\",\n severity: \"error\",\n message: \"xcrun simctl not available.\",\n detail: \"Install full Xcode (from Mac App Store), not just Command Line Tools.\",\n });\n return results;\n }\n\n // 5. iOS simulators present.\n try {\n const json = exec(\"xcrun\", [\"simctl\", \"list\", \"devices\", \"available\", \"--json\"]);\n const parsed = JSON.parse(json) as { devices: Record<string, unknown[]> };\n const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);\n if (total === 0) {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"No iOS simulators found.\",\n detail: \"Create one in Xcode → Window → Devices and Simulators → '+'.\",\n });\n } else {\n results.push({ name: \"simulators\", severity: \"ok\", message: `${total} simulators available` });\n }\n } catch {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"Could not enumerate simulators.\",\n });\n }\n\n // 6. Project type — informational hint about the kind of app detected.\n // unotest-mobile drives iOS via WebDriverAgent, which works against any\n // iOS app (RN/Expo, native Swift/SwiftUI, Flutter exposing a11y semantics)\n // — selectors resolve against the iOS accessibility tree, not RN-specific\n // hooks. The RN/Expo detection is a hint, not a requirement.\n try {\n const pkgRaw = exec(\"cat\", [\"package.json\"]);\n const pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n const isRn = \"react-native\" in deps || \"expo\" in deps;\n if (isRn) {\n results.push({ name: \"project-type\", severity: \"ok\", message: \"React Native / Expo detected\" });\n } else {\n results.push({\n name: \"project-type\",\n severity: \"ok\",\n message: \"no React Native / Expo dependency in package.json\",\n detail:\n \"Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. \" +\n \"Selectors resolve via the iOS accessibility tree — set accessibilityIdentifier \" +\n \"on the views you want to target.\",\n });\n }\n } catch {\n results.push({\n name: \"project-type\",\n severity: \"ok\",\n message: \"no package.json in current directory\",\n detail:\n \"That's fine for native iOS projects. If this is a Node-based project, \" +\n \"run from its root so unotest-mobile can detect React Native / Expo.\",\n });\n }\n\n return results;\n}\n","// Static content for files generated by `unotest-mobile init`. Pure\n// strings — no I/O. Caller writes them at the right path.\n\nexport const templates = {\n smokeWelcome: `// id-smoke-welcome\n// First-run sanity: harness reaches sim, WDA, and app launch handshake\n// #00aa00\nfunction test_smoke_welcome() {\n setDevice(\"A\");\n appLaunch(true);\n}\n`,\n\n scenarioTemplate: `// id-<your-scenario-id>\n// <one-line description>\n// #888888\nfunction test_<your_name>() {\n // 1. SETUP — DB / API fixtures (helpers from _helpers/)\n // wipe_e2e_users();\n // user_id = seed_user(\"e2e@example.com\", \"e2e-pass\");\n\n // 2. ENTER — bring app to initial UI state\n setDevice(\"A\");\n appLaunch(true);\n waitFor(getByTestId(\"screen-welcome\"), 15000);\n\n // 3. ACT — actions you're testing\n // signin(\"e2e@example.com\", \"e2e-pass\");\n // tap(getByTestId(\"btn-something\"));\n\n // 4. ASSERT — UI + DB / API checks\n // assertVisible(getByTestId(\"screen-target\"));\n // count = dbQuery(\"SELECT count(*)::text FROM x WHERE ...\");\n // assertEqual(count, \"1\");\n}\n`,\n\n agentsMd: `# AI Agents: how to write e2e tests for this project\n\nThis project uses [\\`@unotest/mobile\\`](https://www.npmjs.com/package/@unotest/mobile)\nfor end-to-end testing of iOS React Native flows.\n\n## Layout\n\n- \\`unotest/e2e/\\` — scenarios (one \\`test_*\\` entry function per file)\n- \\`unotest/e2e/_helpers/\\` — project-specific helpers, visible to all scenarios\n- \\`unotest/e2e/<feature>/\\` — feature-scoped subfolders (recommended)\n\n## How to write a test\n\nThe full guide — DSL functions, scenario shape, helper rules, linter codes,\ndebugging — is the \\`write-e2e-test\\` Claude Code skill at\n\\`.claude/skills/write-e2e-test.md\\`. It is the single source of truth for\nboth Claude Code and other AI agents. Read it before writing tests.\n\n## Running\n\n- \\`npx @unotest/mobile e2e <name>\\` — run \\`unotest/e2e/<name>.js\\`\n- \\`npx @unotest/mobile lint\\` — static check of all scenarios\n- \\`npx @unotest/mobile doctor\\` — re-check environment\n\nInside Claude Code, prefer the MCP \\`run_test\\` tool with\n\\`pauseOnFailure: true\\` — it pauses on the failed step so you can\n\\`inspect_runtime\\`, fix the scenario, and \\`resume\\`.\n`,\n\n envExample: `# unotest-mobile — copied to unotest/.env by \\`init\\`. Fill in below.\n\n# --- App under test --------------------------------------------------------\nAPP_BUNDLE_ID=com.example.myapp\nAPP_URL_SCHEME=myapp\nINVITE_DEEPLINK_PREFIX=myapp://invite/\n\n# --- Backend ---------------------------------------------------------------\nAPI_BASE_URL=http://localhost:3000/api\n# Optional. Absolute path used as default cwd for the \\`shell(...)\\` DSL\n# primitive. Set if your scenarios shell out to project-local CLIs\n# (pnpm cli, rails runner, etc.) that must run from the project root.\nPROJECT_ROOT=\n\n# --- Database --------------------------------------------------------------\n# Pick the driver matching your backend. Install peer-deps only as needed:\n# npm i -D pg @types/pg # for postgresql://\n# npm i -D mysql2 # for mysql://\n# (SQLite via better-sqlite3 is bundled — no separate install.)\n#\n# Docker-compose Postgres/MySQL — ensure host port is exposed\n# (\\`ports: [\"5432:5432\"]\\`). Native clients connect from the host, not from\n# inside the compose network.\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_test\n\n# --- Simulators ------------------------------------------------------------\n# Names must match \\`xcrun simctl list devices\\`.\nSIM_A_NAME=iPhone 15\nSIM_B_NAME=iPhone 15 Plus\nSIM_POOL=A,B\n\n# --- Dev tooling -----------------------------------------------------------\nMETRO_URL=http://localhost:8081\n# Expo dev-client: auto-open dev-client deep link after a clean launch.\nEXPO_DEV_CLIENT=false\nSESSION_LOG_PATH=sessions/current.jsonl\nARTIFACTS_DIR=artifacts\n\n# --- WebDriverAgent --------------------------------------------------------\n# Per-slot WDA ports. Each slot in SIM_POOL needs one. Format: \"slot=port\".\nWDA_PORTS=A=8100,B=8101\nWDA_DEFAULT_ACTION_WAIT_MS=2000\nWDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000\n\n# --- D-17 paused-runtime TTL ----------------------------------------------\n# How long a paused-on-failure runtime stays alive before auto-abort (ms).\n# Default 30 minutes. Inspecting the runtime resets the timer.\nPAUSED_RUNTIME_TTL_MS=1800000\n`,\n\n gitignoreLines: [\n \"\",\n \"# unotest-mobile\",\n \"unotest/.env\",\n \"unotest/artifacts/\",\n \"unotest/sessions/\",\n ],\n\n mcpServerEntry: {\n command: \"npx\",\n args: [\"-y\", \"@unotest/mobile\"],\n } as const,\n};\n","// `.mcp.json` merger — preserves other registered MCP servers when adding\n// the unotest-mobile entry. Pure functions over JSON content; FS is split\n// out so callers can unit-test the merge logic without touching disk.\n\nexport interface McpServerConfig {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\nexport interface McpConfig {\n mcpServers?: Record<string, McpServerConfig>;\n}\n\nexport type McpMergeAction = \"created\" | \"added\" | \"already-present\" | \"force-overwrote\";\n\nexport interface McpMergeResult {\n action: McpMergeAction;\n newContent: string;\n}\n\nexport class McpJsonParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"McpJsonParseError\";\n }\n}\n\n/**\n * Compute the new content of `.mcp.json` given the existing source (or\n * `null` if the file doesn't exist yet) and the server entry to add.\n * Returns the merged JSON text plus an action label for caller logging.\n */\nexport function mergeMcpConfig(\n existingSource: string | null,\n serverName: string,\n serverConfig: McpServerConfig,\n options: { force?: boolean } = {},\n): McpMergeResult {\n if (existingSource === null) {\n const content = JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2) + \"\\n\";\n return { action: \"created\", newContent: content };\n }\n\n let parsed: McpConfig;\n try {\n parsed = JSON.parse(existingSource) as McpConfig;\n } catch (e) {\n throw new McpJsonParseError(\n `Failed to parse .mcp.json as JSON: ${(e as Error).message}. ` +\n `Fix or remove the file and re-run init.`,\n );\n }\n\n if (!parsed.mcpServers || typeof parsed.mcpServers !== \"object\") {\n parsed.mcpServers = {};\n }\n\n const alreadyPresent = serverName in parsed.mcpServers;\n if (alreadyPresent && !options.force) {\n return { action: \"already-present\", newContent: existingSource };\n }\n\n parsed.mcpServers[serverName] = serverConfig;\n const content = JSON.stringify(parsed, null, 2) + \"\\n\";\n return {\n action: alreadyPresent ? \"force-overwrote\" : \"added\",\n newContent: content,\n };\n}\n","// Idempotent append-unique-lines for .gitignore (or any line-based file).\n// Pure function over content — caller handles FS.\n\nexport interface GitignoreUpdate {\n added: string[];\n alreadyPresent: string[];\n newContent: string;\n}\n\nexport function appendUniqueLines(\n existingContent: string | null,\n linesToAdd: string[],\n): GitignoreUpdate {\n const existing = existingContent ?? \"\";\n const existingLines = new Set(existing.split(\"\\n\").map((l) => l.trim()));\n const added: string[] = [];\n const alreadyPresent: string[] = [];\n\n for (const line of linesToAdd) {\n const trimmed = line.trim();\n if (trimmed === \"\" || existingLines.has(trimmed)) {\n if (trimmed !== \"\") alreadyPresent.push(line);\n continue;\n }\n added.push(line);\n existingLines.add(trimmed);\n }\n\n if (added.length === 0) {\n return { added, alreadyPresent, newContent: existing };\n }\n\n const sep = existing === \"\" ? \"\" : existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n const newContent = existing + sep + added.join(\"\\n\") + \"\\n\";\n return { added, alreadyPresent, newContent };\n}\n"],"mappings":"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","names":["content"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unotest/mobile",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "AI-native E2E testing for iOS React Native apps. MCP server + CLI runner + JS-DSL scenarios.",
5
5
  "license": "MIT",
6
6
  "type": "module",