diff --git a/.github/workflows/mobile-apps-script-tests.yml b/.github/workflows/mobile-apps-script-tests.yml new file mode 100644 index 000000000..75aaf4d76 --- /dev/null +++ b/.github/workflows/mobile-apps-script-tests.yml @@ -0,0 +1,27 @@ +name: mobile-apps-script-tests + +on: + pull_request: + branches: + - main + paths: + - "plugins/mobile-apps/**" + - ".github/workflows/mobile-apps-script-tests.yml" + +jobs: + test-mobile-apps-version-check: + name: test-mobile-apps-version-check + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: setup-node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 20 + + # Network behavior is mocked; CI validates release parsing and update + # guidance without depending on GitHub availability or current versions. + - name: test-version-check + run: node --test plugins/mobile-apps/scripts/tests/check-version.test.js diff --git a/plugins/mobile-apps/AGENTS.md b/plugins/mobile-apps/AGENTS.md index 115b2b662..a22923881 100644 --- a/plugins/mobile-apps/AGENTS.md +++ b/plugins/mobile-apps/AGENTS.md @@ -2,7 +2,7 @@ This file provides guidance to AI Agents when working with the **mobile-app** plugin. -> **Status:** v0 — 23 skills + 5 agents authored. The latest Expo standalone template snapshot is bundled under `template/`. Read [README.md](./README.md) for the command list. +> **Status:** v0 — 24 skills + 5 agents authored. The latest Expo standalone template snapshot is bundled under `template/`. Read [README.md](./README.md) for the command list. ## What This Plugin Is @@ -73,6 +73,7 @@ Do not add preparation rewrites for `scheme`, `package`, `bundleIdentifier`, `sr - `DONE_WITH_CONCERNS` requires at least one concern. If none, use `DONE`. - Special early-return signals (`INDUSTRY_CONFIRM_REQUESTED:`, `DESIGN_VIBE_REQUESTED:`) pre-date this protocol and remain in effect — they are special-cased "ask the user one question and re-spawn me" handoffs, not terminal returns. - The canonical orchestrator handler lives in [`skills/create-mobile-app/SKILL.md`](./skills/create-mobile-app/SKILL.md) Step 3.0. Future skills that spawn agents should reference it rather than duplicating the switch. +13. **Package versions come from manifests** — Skills and agents must parse the active project's `package.json` before making package-version claims. Before project creation, use `template/package.json`. Never hardcode an Expo SDK generation or package pin in skill prose; use `npx expo install --check` for compatibility. ## Decisions made @@ -84,6 +85,7 @@ Do not add preparation rewrites for `scheme`, `package`, `bundleIdentifier`, `sr - ✅ Auth: `/create-mobile-app` resolves the tenant from the selected Power Platform environment (`scripts/resolve-environment.js`), writes that tenant to `auth.config.json`, then lets the user paste an app registration client ID, create one from the Power Apps Wrap page and paste it, or skip auth for later. `/set-app-registration-native` is a manual helper for the same Wrap-page + pasted-client-ID flow. - ✅ `/add-native` v0 scope: camera, location, push, biometrics, secure-store (already in template) - ✅ Template is supplied as a fresh `pa-wrap-tools/templates/expo-app-standalone` folder before `/create-mobile-app` runs; users materialize it with `degit`, run `npm install`, then invoke the skill from that folder. The skill validates/prepares the folder and runs `npx power-apps init`. +- ✅ Every top-level skill runs the fail-open `scripts/check-version.js` preflight through `shared/shared-instructions.md`. Remote checks are cached for seven days outside the app; CI verifies every skill retains the shared-instructions reference. `/version-check` uses the same release check, resolves the npm feed's `latest` native host version once, pins preview/apply commands to it, reads its safe ceiling from bundled `template/app.json`, and applies only approved sequential migrations within that ceiling. Straightforward `.rej` conflicts are merged with customization markers and pre-existing diffs preserved; ambiguous behavioral conflicts get one bounded retry before user escalation. - ✅ `brand/` directory convention: `/design-system` (Step 6.75) writes `brand/design-system.md` (spec), `brand/tokens.ts` (importable Tamagui tokens), and `brand/design-system.html` (visual gallery). Screen-builders MUST read `brand/design-system.md` if present; `## Negatives` = HARD RULES. `/create-mobile-app` Step 9b imports `brand/tokens.ts` via `skills/design-system/references/tamagui-integration.md`. Projects without `brand/` fall back to `## Design Direction` only — no breakage. - ✅ Offline profile creation is **author-only in v0.1** — `/setup-offline-profile` and `/enable-tables-offline` POST `mobileofflineprofile` / `mobileofflineprofileitem` / `mobileofflineprofileitemassociation` to Dataverse and write `offline-profile.json` to the project, but do NOT scaffold offline runtime code (SQLite store, sync engine, write queue) into the generated app. Runtime support is gated on upstream `@microsoft/power-apps-native-host` confirmation. - ✅ Custom filter mode (`recorddistributioncriteria=3`, `profileitemrule` → `savedquery`) is **deferred to v0.5**. v0.1 supports Related-rows-only / All-records / Organization-rows radio options only. diff --git a/plugins/mobile-apps/README.md b/plugins/mobile-apps/README.md index 366fdd47f..ab154f5e3 100644 --- a/plugins/mobile-apps/README.md +++ b/plugins/mobile-apps/README.md @@ -253,6 +253,7 @@ Example edit flows: | `/list-connections` | ✅ v0 | Finds or creates a Power Platform connection ID, or resolves a solution connection reference, for `npx power-apps add-data-source`. Use when adding non-Dataverse connectors or re-binding after a 401. | | `/edit-app` | ✅ v0 | Post-generation app editor — updates affected sections of `native-app-plan.md`, applies Dataverse/native/design/connector changes, rebuilds affected screens, runs verification, updates `memory-bank.md`, and regenerates `preview.html` when UI changed. `--plan-only` preserves the old docs-only behavior. | | `/deploy` | ✅ v0 | Build + push — `npm run build` then `npx power-apps push` to the env in `power.config.json`. **Does not** drive `expo run:ios` or `expo run:android` (out of scope for v0). | +| `/version-check` | ✅ v0 | Checks for a newer complete plugin release, resolves the npm feed's latest native host version, previews with that exact version, smart-merges straightforward root-config conflicts, and runs approved updates only through the template version supported by the installed plugin. | | `/open-wrap-url` | ✅ v0 | Opens the Wrap URL in browser for an app ID using `https://make.powerapps.com/environments//wrap?appID=`. Requires both `--app-id` and `--env-id`. | | `/report-issue` | ✅ v0 | Read-only diagnostic — collects env / Expo / Node versions, project context, recent errors, and renders a copy-paste-ready GitHub issue body. Sanitizes secrets. | | `/design-system` | ✅ v0 | End-to-end design system — collects brand inputs (logo, brand doc, website, free text, canvas app, code app, Figma), runs a 3-style visual picker, writes `brand/design-system.md` + `brand/tokens.ts`, renders branded screen previews. Auto-invoked at Step 6.75 of `/create-mobile-app`; also standalone. | diff --git a/plugins/mobile-apps/scripts/check-version.js b/plugins/mobile-apps/scripts/check-version.js new file mode 100644 index 000000000..c47b4cc3b --- /dev/null +++ b/plugins/mobile-apps/scripts/check-version.js @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +/** + * Compares the installed mobile-app plugin version with the published main + * manifest and prints update commands only when a newer release exists. This + * check is fail-open so network access never blocks app work. + */ + +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const PLUGIN_MANIFEST_PATHS = ['.plugin/plugin.json', '.claude-plugin/plugin.json']; +const MARKETPLACE_NAME = 'power-platform-skills'; +const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; +const REMOTE_MANIFEST_URL = + 'https://raw.githubusercontent.com/microsoft/power-platform-skills/main/plugins/mobile-apps/.plugin/plugin.json'; + +function compareSemver(localVersion, remoteVersion) { + const localParts = localVersion.split('.').map(Number); + const remoteParts = remoteVersion.split('.').map(Number); + for (let index = 0; index < 3; index += 1) { + if ((remoteParts[index] || 0) > (localParts[index] || 0)) return 1; + if ((remoteParts[index] || 0) < (localParts[index] || 0)) return -1; + } + return 0; +} + +function formatUpdateMessage(pluginName, localVersion, remoteVersion, marketplaceName) { + const qualifiedName = marketplaceName ? `${pluginName}@${marketplaceName}` : pluginName; + let message = `Plugin update available: ${pluginName} ${localVersion} -> ${remoteVersion}.\n`; + if (marketplaceName) { + message += + `GitHub Copilot CLI:\n` + + ` copilot plugin marketplace update ${marketplaceName}\n` + + ` copilot plugin update ${qualifiedName}\n` + + `Claude Code:\n` + + ` claude plugin marketplace update ${marketplaceName}\n` + + ` claude plugin update ${qualifiedName}`; + } else { + message += + `GitHub Copilot CLI: copilot plugin update ${qualifiedName}\n` + + `Claude Code: claude plugin update ${qualifiedName}`; + } + return message; +} + +function firstExistingPath(root, relativePaths) { + for (const relativePath of relativePaths) { + const filePath = path.join(root, relativePath); + if (fs.existsSync(filePath)) return filePath; + } + return null; +} + +async function fetchRemotePlugin(fetchImpl = fetch) { + const response = await fetchImpl(REMOTE_MANIFEST_URL, { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(5000), + }); + if (!response.ok) { + throw new Error(`Remote manifest request failed with HTTP ${response.status}`); + } + return response.json(); +} + +function resolveCachePath(env = process.env, homeDirectory = os.homedir()) { + const cacheRoot = env.XDG_CACHE_HOME || env.LOCALAPPDATA || path.join(homeDirectory, '.cache'); + return path.join(cacheRoot, 'power-platform-skills', 'mobile-app-version-check.json'); +} + +function readCache(cachePath) { + try { + return JSON.parse(fs.readFileSync(cachePath, 'utf8')); + } catch { + return null; + } +} + +function isFresh(cache, now = Date.now()) { + return Boolean( + Number.isFinite(cache?.checkedAt) && + now - cache.checkedAt >= 0 && + now - cache.checkedAt < CACHE_TTL_MS + ); +} + +function writeCache(cachePath, remoteVersion, now = Date.now()) { + try { + fs.mkdirSync(path.dirname(cachePath), { recursive: true }); + fs.writeFileSync( + cachePath, + `${JSON.stringify({ checkedAt: now, remoteVersion: remoteVersion || null })}\n`, + 'utf8' + ); + } catch { + // Read-only homes must not turn an advisory check into a skill failure. + } +} + +async function getRemoteVersion({ + cachePath = resolveCachePath(), + fetchImpl = fetch, + now = Date.now(), +} = {}) { + const cache = readCache(cachePath); + if (isFresh(cache, now)) return cache.remoteVersion; + + try { + const remotePlugin = await fetchRemotePlugin(fetchImpl); + if (!remotePlugin?.version) return cache?.remoteVersion || null; + writeCache(cachePath, remotePlugin.version, now); + return remotePlugin.version; + } catch { + // Cache failed attempts too, otherwise every skill invocation would wait on an offline network. + writeCache(cachePath, cache?.remoteVersion || null, now); + return cache?.remoteVersion || null; + } +} + +if (require.main === module) { + void (async () => { + try { + const pluginRoot = path.resolve(__dirname, '..'); + const pluginJsonPath = firstExistingPath(pluginRoot, PLUGIN_MANIFEST_PATHS); + if (!pluginJsonPath) return; + + const localPlugin = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8')); + if (!localPlugin.version) return; + + const remoteVersion = await getRemoteVersion(); + if (!remoteVersion) return; + + if (compareSemver(localPlugin.version, remoteVersion) > 0) { + console.log( + formatUpdateMessage( + localPlugin.name || 'mobile-app', + localPlugin.version, + remoteVersion, + MARKETPLACE_NAME + ) + ); + } + } catch { + // Plugin update discovery is advisory and must never block template checks. + } + })(); +} + +module.exports = { + CACHE_TTL_MS, + REMOTE_MANIFEST_URL, + compareSemver, + fetchRemotePlugin, + formatUpdateMessage, + getRemoteVersion, + isFresh, + resolveCachePath, +}; \ No newline at end of file diff --git a/plugins/mobile-apps/scripts/tests/check-version.test.js b/plugins/mobile-apps/scripts/tests/check-version.test.js new file mode 100644 index 000000000..e64de8901 --- /dev/null +++ b/plugins/mobile-apps/scripts/tests/check-version.test.js @@ -0,0 +1,210 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { + CACHE_TTL_MS, + REMOTE_MANIFEST_URL, + compareSemver, + fetchRemotePlugin, + formatUpdateMessage, + getRemoteVersion, + isFresh, + resolveCachePath, +} = require('../check-version'); + +test('compareSemver identifies newer remote versions', () => { + assert.equal(compareSemver('0.2.0', '0.2.0'), 0); + assert.equal(compareSemver('0.2.0', '0.2.1'), 1); + assert.equal(compareSemver('0.2.0', '0.3.0'), 1); + assert.equal(compareSemver('1.0.0', '0.9.0'), -1); +}); + +test('fetchRemotePlugin reads the published mobile-app manifest', async () => { + const fetchImpl = async (url) => { + assert.equal(url, REMOTE_MANIFEST_URL); + return { + ok: true, + json: async () => ({ name: 'mobile-app', version: '0.3.0' }), + }; + }; + + assert.deepEqual(await fetchRemotePlugin(fetchImpl), { + name: 'mobile-app', + version: '0.3.0', + }); +}); + +test('fetchRemotePlugin rejects unsuccessful responses', async () => { + const fetchImpl = async () => ({ ok: false, status: 503 }); + await assert.rejects(() => fetchRemotePlugin(fetchImpl), /HTTP 503/); +}); + +test('uses a cached version for seven days without fetching', async (context) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mobile-version-check-')); + const cachePath = path.join(root, 'cache.json'); + const now = Date.UTC(2026, 7, 18); + context.after(() => fs.rmSync(root, { recursive: true, force: true })); + fs.writeFileSync(cachePath, JSON.stringify({ checkedAt: now, remoteVersion: '0.3.0' })); + + const remoteVersion = await getRemoteVersion({ + cachePath, + now: now + CACHE_TTL_MS - 1, + fetchImpl: async () => assert.fail('fresh cache must skip the network'), + }); + + assert.equal(remoteVersion, '0.3.0'); + assert.equal(isFresh({ checkedAt: now, remoteVersion: '0.3.0' }, now + CACHE_TTL_MS), false); +}); + +test('refreshes a stale cache and persists the published version', async (context) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mobile-version-check-')); + const cachePath = path.join(root, 'nested', 'cache.json'); + const now = Date.UTC(2026, 7, 18); + context.after(() => fs.rmSync(root, { recursive: true, force: true })); + + const remoteVersion = await getRemoteVersion({ + cachePath, + now, + fetchImpl: async () => ({ + ok: true, + json: async () => ({ version: '0.4.0' }), + }), + }); + + assert.equal(remoteVersion, '0.4.0'); + assert.deepEqual(JSON.parse(fs.readFileSync(cachePath, 'utf8')), { + checkedAt: now, + remoteVersion: '0.4.0', + }); +}); + +test('falls back to a stale cached update when refresh fails', async (context) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mobile-version-check-')); + const cachePath = path.join(root, 'cache.json'); + context.after(() => fs.rmSync(root, { recursive: true, force: true })); + fs.writeFileSync(cachePath, JSON.stringify({ checkedAt: 1, remoteVersion: '0.3.0' })); + + assert.equal( + await getRemoteVersion({ + cachePath, + now: CACHE_TTL_MS + 1, + fetchImpl: async () => { + throw new Error('offline'); + }, + }), + '0.3.0' + ); + assert.deepEqual(JSON.parse(fs.readFileSync(cachePath, 'utf8')), { + checkedAt: CACHE_TTL_MS + 1, + remoteVersion: '0.3.0', + }); +}); + +test('caches failed attempts so offline skills do not repeatedly fetch', async (context) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mobile-version-check-')); + const cachePath = path.join(root, 'cache.json'); + const now = Date.UTC(2026, 7, 18); + context.after(() => fs.rmSync(root, { recursive: true, force: true })); + + assert.equal( + await getRemoteVersion({ + cachePath, + now, + fetchImpl: async () => { + throw new Error('offline'); + }, + }), + null + ); + assert.equal( + await getRemoteVersion({ + cachePath, + now: now + 1, + fetchImpl: async () => assert.fail('fresh failure cache must skip the network'), + }), + null + ); +}); + +test('uses a user cache directory instead of modifying the app', () => { + assert.equal( + resolveCachePath({ XDG_CACHE_HOME: '/cache' }, '/home/user'), + path.join('/cache', 'power-platform-skills', 'mobile-app-version-check.json') + ); +}); + +test('formatUpdateMessage suggests both supported plugin managers', () => { + const message = formatUpdateMessage( + 'mobile-app', + '0.2.0', + '0.3.0', + 'power-platform-skills' + ); + + assert.match(message, /mobile-app 0\.2\.0 -> 0\.3\.0/); + assert.match(message, /copilot plugin marketplace update power-platform-skills/); + assert.match(message, /copilot plugin update mobile-app@power-platform-skills/); + assert.match(message, /claude plugin marketplace update power-platform-skills/); + assert.match(message, /claude plugin update mobile-app@power-platform-skills/); +}); + +test('every top-level mobile skill loads shared instructions', () => { + const skillsRoot = path.resolve(__dirname, '..', '..', 'skills'); + const missing = fs + .readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(skillsRoot, entry.name, 'SKILL.md')) + .filter((skillPath) => fs.existsSync(skillPath)) + .filter((skillPath) => !fs.readFileSync(skillPath, 'utf8').includes('shared-instructions.md')) + .map((skillPath) => path.relative(skillsRoot, skillPath)); + + assert.deepEqual(missing, []); +}); + +test('mobile instructions do not hardcode Expo or React Native versions', () => { + const pluginRoot = path.resolve(__dirname, '..', '..'); + const roots = ['skills', 'shared', 'agents'].map((name) => path.join(pluginRoot, name)); + const findings = []; + + function walk(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) walk(entryPath); + else if (/\.md$/i.test(entry.name)) { + const content = fs.readFileSync(entryPath, 'utf8'); + const hasSdkGeneration = /\b(?:Expo SDK|SDK)\s+\d+\b/i.test(content); + const hasPackagePin = + /`(?:expo|expo-[a-z0-9-]+|react-native)`\s*(?:@|:|\||v(?:ersion)?\s+)\s*`?\^?\d+\.\d+/i.test( + content + ); + if (hasSdkGeneration || hasPackagePin) { + findings.push(path.relative(pluginRoot, entryPath)); + } + } + } + } + + for (const root of roots) walk(root); + assert.deepEqual(findings, []); +}); + +test('version-check resolves latest host from npm and pins upgrade commands', () => { + const skillPath = path.resolve(__dirname, '..', '..', 'skills', 'version-check', 'SKILL.md'); + const skill = fs.readFileSync(skillPath, 'utf8'); + const npmLookup = 'npm view @microsoft/power-apps-native-host@latest version --json'; + const pinnedPackage = '@microsoft/power-apps-native-host@'; + + assert.ok(skill.includes(npmLookup)); + assert.ok(skill.indexOf(npmLookup) < skill.indexOf('upgrade-template --dry-run')); + assert.match(skill, new RegExp(`${pinnedPackage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} upgrade-template --dry-run`)); + assert.match(skill, new RegExp(`${pinnedPackage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} upgrade-template`)); + assert.doesNotMatch( + skill, + /npx[^\n]*--package @microsoft\/power-apps-native-host@latest[^\n]*upgrade-template/ + ); +}); \ No newline at end of file diff --git a/plugins/mobile-apps/shared/shared-instructions.md b/plugins/mobile-apps/shared/shared-instructions.md index 4f9a6ba92..5664e7af7 100644 --- a/plugins/mobile-apps/shared/shared-instructions.md +++ b/plugins/mobile-apps/shared/shared-instructions.md @@ -8,9 +8,17 @@ All skills reference this single file. When new shared instructions are added, u ## Version Check -**📋 [version-check.md](./version-check.md)** +At the start of every skill, run: -Run at the start of every skill execution (at most once per day). Notifies the user if a tool version is below the supported minimum (Node 22+, npm 10+, Expo SDK 55+, etc.). +```bash +node "${PLUGIN_ROOT}/scripts/check-version.js" +``` + +Show any output, then continue the requested skill. The checker is fail-open and caches remote results (including failed attempts) for seven days in the user's cache directory, so invoking it every time does not repeatedly access the network or modify the app. A previously discovered newer plugin remains visible while offline until the user updates it. + +## Package Version Source of Truth + +Before stating, comparing, installing, or relying on any package version, parse the active project's `package.json` and read the package from `dependencies` + `devDependencies`. Do not infer versions from this plugin's prose, an SDK generation, or memory. If no project exists yet, read `${PLUGIN_ROOT}/template/package.json`. Use `npx expo install --check` for Expo compatibility instead of assuming all `expo-*` packages share a fixed SDK version. --- diff --git a/plugins/mobile-apps/shared/version-check.md b/plugins/mobile-apps/shared/version-check.md deleted file mode 100644 index e5f12eefa..000000000 --- a/plugins/mobile-apps/shared/version-check.md +++ /dev/null @@ -1,115 +0,0 @@ -# Version Check — Power Apps Native Code App - -Single source of truth for minimum tool versions. Every skill should reference this file from its prereqs phase rather than hard-coding versions. - -## Guiding principle — **scope-aware checks, not blanket gates** - -This plugin uses scope-aware checks based on the mobile workflow: - -- **Deploy** = `npm run build` + `npx power-apps push`. No local Xcode / Android Studio involvement. -- **Local dev** = the user runs `npm run dev` (= `expo start`) directly. Metro starts and prints a QR for native dev clients. -- **Local native compile** (platform-specific native run commands) is the user's choice and lives **outside** this plugin's skills. Not a prerequisite, not validated, not driven. - -Result: the only required tooling is what Node/npm, `npx power-apps`, and the relevant helper scripts need. Xcode/JDK/Android Studio are explicitly out of scope. - -This is also a standard **Expo managed workflow** project. That means: - -- `ios/` and `android/` folders are **generated artifacts** — produced by `npx expo prebuild` from `app.json` plugins. They are **git-ignored**. Developers never hand-edit them. -- Adding a native capability = `npx expo install ` + add the plugin name to `app.json` plugins array. The plugin's "config plugin" patches `Info.plist` / `AndroidManifest.xml` automatically on the next prebuild (which happens transparently inside `npm run build` and native run workflows when needed). -- Tamagui, Expo Router, MSAL, secure-store, camera, etc. all ship as plugins — you `npm install` them like any other package. **No Xcode project surgery, no `build.gradle` edits.** - -### What the user owns vs what Expo generates - -| Layer | Edited by user? | Source of truth | -|---|---|---| -| `app/`, `src/`, `assets/`, `tamagui.config.ts` | ✅ yes | application code | -| `app.json` (plugins, permissions, infoPlist, intentFilters) | ✅ yes | declarative native config | -| `package.json` (deps + Expo plugin packages) | ✅ yes | npm | -| `ios/` | ❌ never | generated by `expo prebuild` | -| `android/` | ❌ never | generated by `expo prebuild` | -| `Info.plist`, `AndroidManifest.xml` | ❌ never | generated from `app.json` | - -If `/add-native` adds `expo-camera`, the only changes that get committed are: `package.json` (dep) + `app.json` (plugin entry + iOS `NSCameraUsageDescription`). - -## Always required (any target) - -| Tool | Min version | Check command | Why | -|---|---|---|---| -| Node.js | `22.0.0` | `node --version` | Expo SDK 55 + React Native 0.83 require Node 22+ | -| npm | `10.0.0` | `npm --version` | Expo install / lockfile v3 | -| Expo CLI (via npx) | `0.21.0` | `npx expo --version` | SDK 55 prebuild support | -| TypeScript (project-local) | `5.4.0` | `npx tsc --version` | Required by generated service types | -| POSIX shell (Windows only) | bash 4+ / zsh 5+ | `echo $BASH_VERSION || echo $ZSH_VERSION` | Skills use `cp -R`, `rm -rf`, `mkdir -p`, `grep`, `sed`, `find`. Native PowerShell / cmd.exe lack these. Use **Git Bash** or **WSL** on Windows. See [shared-instructions.md → Shell Requirement](./shared-instructions.md#shell-requirement-windows-users). | - -## Required only when the relevant skill runs - -| Tool | Required by | Min version | Check | -|---|---|---|---| -| Azure CLI (`az`) | ADO npm token setup, `/add-dataverse` token acquisition | `2.60.0` | `az --version` | - -If the user is just editing screens or running web, `az` does not need to be installed. - -## Required only for **local native builds** — OUT OF SCOPE for plugin skills - -The following are needed **only** if the user manually runs platform-specific native run commands to compile a local native binary. **No skill in this plugin runs those commands or probes for these tools.** Documented here purely as user reference. - -| Tool | Min version | Platforms | Why | -|---|---|---|---| -| JDK | `17` | Android | Gradle 8.x requirement for RN 0.83 | -| Android Studio + SDK | latest | Android | Emulator + platform tools | -| Xcode | `15.0` | iOS (macOS only) | iOS 18 SDK | -| CocoaPods | `1.14.0` | iOS (macOS only) | Pod install for native modules | -| Watchman | any | macOS / Linux dev | File-watching perf (optional) | - -If the user asks how to run on a real device or simulator: point them at the Expo docs (https://docs.expo.dev/get-started/set-up-your-environment/) and let them install whatever they need. Do not gate any plugin skill on these. - - -## Required SDKs (in `package.json`) - - -| Package | Pinned version | Notes | -|---|---|---| -| `expo` | `55.0.26` | SDK 55 | -| `react-native` | `0.83.6` | matched to Expo SDK 55 | -| All `expo-*` packages | `55.x.y` | router, auth-session, secure-store, dev-client, etc. — version line tied to SDK | -| `@microsoft/power-apps` | `1.2.7` | Power Apps code app SDK | -| `@microsoft/power-apps-native-host` | `^0.2.20` | native runtime host | -| `@microsoft/power-apps-native-offline` | `^0.1.3` | offline runtime package | -| `tamagui`, `@tamagui/*` | `2.4.5` | design system primitives with Config v5, all `@tamagui/*` packages tracked together | - -## Standard prereq snippet for skills - -Most skills only need the always-required tier. Copy this into Step 1. - -Use shell/project-file checks directly: - -```bash -# Always required -node --version # expect v22+ - -# Conditionally required — only if THIS skill needs it -# az account show # for /add-dataverse - -# Project-local (only if inside a project) -test -f power.config.json && echo "OK: code app project" -node -e "console.log(JSON.stringify(require('./app.json').expo, null, 2))" 2>/dev/null # SDK + plugins -``` - -**Do NOT call `xcodebuild`, `java -version`, or check for Android Studio in any skill.** Local native compile is the user's choice, not a plugin concern. - -## Install pointers - -| Tool | Install | -|---|---| -| Node 22 | `nvm install 22 && nvm use 22` (https://nvm.sh) | -| `az` | https://aka.ms/InstallAzureCLI | - -*Out-of-scope tools (user-managed if they want local native builds):* JDK 17 (https://adoptium.net), Android Studio (https://developer.android.com/studio), Xcode (Mac App Store), CocoaPods (`brew install cocoapods`). - - -## When to update this file - -- New skill needs a tool not listed → add it here, then reference from the skill (don't hard-code in the skill). -- An SDK pin changes in [`plugins/mobile-apps/template/package.json`](https://github.com/microsoft/power-platform-skills/tree/main/plugins/mobile-apps/template/package.json) → update the SDK table here. -- A new Power Platform CLI feature requires a newer minimum → bump the table, surface a one-line warning in the affected skills. - diff --git a/plugins/mobile-apps/skills/create-mobile-app/SKILL.md b/plugins/mobile-apps/skills/create-mobile-app/SKILL.md index 6e40fda1b..6cf90916a 100644 --- a/plugins/mobile-apps/skills/create-mobile-app/SKILL.md +++ b/plugins/mobile-apps/skills/create-mobile-app/SKILL.md @@ -109,7 +109,7 @@ az account show --query "user.name" -o tsv # Azure CLI logged in (neede git --version # optional ``` -**Do NOT probe Xcode, Java, Android Studio, or CocoaPods here.** This plugin's flow is plan → scaffold → code → local Expo dev server. Build + deploy (`npm run build` / `npx power-apps push`) is a separate user-driven step via the `/deploy` skill. Local native compile is the user's choice and lives outside this skill (run the platform-specific native command directly when needed). See [`shared/version-check.md`](${CLAUDE_SKILL_DIR}/../../shared/version-check.md) — only the **Always required** tier matters here. +**Do NOT probe Xcode, Java, Android Studio, or CocoaPods here.** This plugin's flow is plan → scaffold → code → local Expo dev server. Build + deploy (`npm run build` / `npx power-apps push`) is a separate user-driven step via the `/deploy` skill. Local native compile is the user's choice and lives outside this skill (run the platform-specific native command directly when needed). | Missing | Action | |---|---| diff --git a/plugins/mobile-apps/skills/deploy/SKILL.md b/plugins/mobile-apps/skills/deploy/SKILL.md index 5b8b2425b..bdbb40157 100644 --- a/plugins/mobile-apps/skills/deploy/SKILL.md +++ b/plugins/mobile-apps/skills/deploy/SKILL.md @@ -187,6 +187,5 @@ If they want to compile a native binary locally, they run the platform-specific ## Reference -- [`shared/version-check.md`](${CLAUDE_SKILL_DIR}/../../shared/version-check.md) — min versions (only Always-required tier matters here) - [`shared/memory-bank.md`](${CLAUDE_SKILL_DIR}/../../shared/memory-bank.md) — Build history schema - [`shared/references/offline-profile-reconciliation.md`](${CLAUDE_SKILL_DIR}/../../shared/references/offline-profile-reconciliation.md) — Step 2.5 offline coverage gate diff --git a/plugins/mobile-apps/skills/version-check/SKILL.md b/plugins/mobile-apps/skills/version-check/SKILL.md new file mode 100644 index 000000000..cd3ce6446 --- /dev/null +++ b/plugins/mobile-apps/skills/version-check/SKILL.md @@ -0,0 +1,99 @@ +--- +name: version-check +description: Use when the user wants to check the mobile-app plugin or a Power Apps mobile app for updates, upgrade its supported native host template, or determine whether the installed skill and Expo template are current. +user-invocable: true +allowed-tools: Read, Edit, Bash, Glob, AskUserQuestion +model: sonnet +--- + +**📋 Shared instructions: [shared-instructions.md](${CLAUDE_SKILL_DIR}/../../shared/shared-instructions.md)** — read first. + +# Check and Upgrade Mobile App + +Checks for a newer plugin and native host package, then previews and applies approved template migrations up to the version bundled with this plugin. + +## Invariants + +- `supported` is `expo.extra.powerappsNative.templateVersion` from `${CLAUDE_SKILL_DIR}/../../template/app.json`; `current` is the same field in the app's `app.json`. Both must be positive integers. +- `declaredHost` comes from the app's `package.json`; `latestHost` comes from the npm feed's `latest` dist-tag. Resolve once and pin every command in this run to that exact version. +- The `latestHost` dry-run is authoritative; local version equality does not prove the app is current. +- Never self-update the plugin, downgrade an app, manually change template/migration state or lockfiles, or touch `app/`, `src/`, `android/`, or `ios/`. +- Every mutation requires one prior approval and a clean dry-run. Preserve all pre-existing work; restore only edits made by this skill. +- Auto-merge only unambiguous conflicts. Stop on interrupted journals, failed validation, unexpected writes, invalid version movement, or unresolved semantic choices. + +Use these commands from the app root: + +```bash +node "${CLAUDE_SKILL_DIR}/../../scripts/check-version.js" +npm view @microsoft/power-apps-native-host@latest version --json +npx --yes --package @microsoft/power-apps-native-host@ upgrade-template --dry-run +npx --yes --package @microsoft/power-apps-native-host@ upgrade-template +``` + +## Workflow + +### 1. Check the plugin + +Run the plugin check. If it prints an update notice, show it verbatim and STOP; never run its commands automatically. Ask the user to update the complete plugin and rerun `/version-check`. Silence is fail-open: continue even if remote discovery was unavailable. + +### 2. Inspect the app + +Find the nearest `package.json` + `app.json` from the requested path/current directory and parse JSON. Require `@microsoft/power-apps-native-host` in dependencies or devDependencies and save its declared range as `declaredHost`. + +- Missing app template version: STOP and request the known legacy source version for `--from-version `; never infer or write it. +- `current > supported`: STOP, recommend updating the plugin, and never downgrade. +- Active `.powerapps-native/upgrade-journal.json`: STOP and surface it for host recovery. +- Existing `*.rej`: record as conflict evidence; do not delete or immediately escalate. + +Print `current` and `supported`, then continue even when equal. + +### 3. Check the npm feed and preview + +From the app root, run `npm view @microsoft/power-apps-native-host@latest version --json`. This honors the project's npm configuration and authenticated feed. Parse the JSON and require one exact semantic version string; never print npm credentials or tokens. If lookup/authentication fails or the result is invalid, STOP with the npm error and do not guess or use a cached package version. + +Print `declaredHost` and `latestHost`. Use `@microsoft/power-apps-native-host@` for every subsequent `npx --package` command in this run; do not resolve `@latest` again after presenting the plan. + +Run the pinned dry-run and show the host version, target profile, file plan, warnings, and conflicts. + +| Result | Action | +|---|---| +| Non-conflict failure | STOP without changes. | +| Already matches target profile | Report current and STOP. | +| Changes with `current === supported` | STOP; an update exists beyond this plugin's verified ceiling. Recommend updating the plugin. | +| Conflict-free changes with `current < supported` | Confirm dirty work is committed/backed up, then ask once to apply the preview with host `` and continue sequential migrations through `supported`. Decline = STOP. | +| Patch conflicts with `current < supported` | Continue to conflict handling; use its single approval instead. | + +### 4. Resolve conflicts + +For previewed conflicts: + +1. Capture baseline status, complete target files, and their existing diffs. +2. Ask once to use host `` to materialize/refresh rejects, assist the merge, and apply after a clean preview; include backup confirmation for dirty work. Explain that the first normal upgrade is expected to fail after writing `.power-apps-.rej`. Decline = STOP. +3. Run the normal upgrade once. If it changes anything beyond migration reject files, STOP and surface the unexpected writes. +4. Read `CUSTOMIZATION.md`, each reject, and each complete target. Keep rejects until success. +5. Apply the smallest merge under this policy: + +| Auto-merge | Escalate | +|---|---| +| Additive JSON/config, dependency/script updates, mechanical import/wrapper composition, changes outside customization markers, or nearby edits where both intents coexist. Preserve formatting, comments, identity, custom dependencies, and every `DO NOT REMOVE OR RENAME THE COMMENT` marker. | Incompatible values with no clear precedence; deletion/semantic rewrite of custom behavior; uncertain auth, signing, identity, secrets, or native ownership; coupled conflicts requiring a product decision. Leave the target at its captured content, keep rejects, explain the choices, and recommend a resolution. | + +Never force success by editing template version, migration state, lockfiles, or generated native projects. + +Rerun dry-run. If one clear correction remains, repair and retry once. If still conflicted, restore only this skill's edits, preserve prior work/rejects, and escalate exact choices. Otherwise show resolved conflicts, preserved customizations, and the clean preview, then continue under the existing approval. + +### 5. Apply each migration + +Run the normal upgrade pinned to ``. It installs compatible dependencies and validates Expo. After success: + +1. Reparse `app.json`; require `previous < new <= supported`. +2. Require no journal. Inspect stale rejects and STOP only for unresolved changes. +3. Dry-run again and show the result. +4. If no changes remain, finish (even below `supported`; the published host is authoritative). +5. If changes remain below `supported`, apply the next migration under existing approval. +6. If changes remain at `supported`, STOP and recommend updating the plugin. + +On any failed check, STOP without repairing host-managed files. If host validation fails after an assisted merge, the host rolls back its operations; restore this skill's merge edits to captured content, preserve prior work/rejects, and report the failure. + +### 6. Report + +Summarize declared/latest host versions, old/new template versions, migrations, files changed, auto-resolved conflicts, preserved customizations, warnings, and follow-up. Show assisted-merge diffs and run documented app tests. Ask the user to review the complete diff. Never claim support beyond `supported`. \ No newline at end of file