diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index 097c8d6..c5825d5 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -2,6 +2,8 @@ name: Build Android APK on: push: branches: [ main ] + pull_request: + branches: [ main ] workflow_dispatch: jobs: @@ -16,7 +18,7 @@ jobs: with: node-version: 20 cache: 'npm' - cache-dependency-path: App_369/package-lock.json + cache-dependency-path: package-lock.json - name: 🏗 Setup Expo uses: expo/expo-github-action@v8 @@ -26,9 +28,9 @@ jobs: token: ${{ secrets.EXPO_TOKEN }} - name: 📦 Install dependencies - working-directory: ./App_369 - run: npm install + run: npm ci - name: 🚀 Build APK - working-directory: ./App_369 - run: eas build --platform android --profile preview --non-interactive + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + run: npm run build:apk diff --git a/DEPLOY_APK.md b/DEPLOY_APK.md new file mode 100644 index 0000000..05fc34d --- /dev/null +++ b/DEPLOY_APK.md @@ -0,0 +1,73 @@ +# APK Distribution Runbook + +This runbook covers generating and publishing the Android APK so users can install ASTRA MATRIX from your landing page. + +## 1) Build the APK with EAS + +Use the preview profile for downloadable APK output: + +```bash +npm ci +npm run build:apk +``` + +Use production profile when you are ready for release-grade signing controls: + +```bash +npm run build:apk:production +``` + +## 2) Retrieve the build artifact + +After `eas build` completes, download the generated APK and rename it: + +```bash +astra-matrix-latest.apk +``` + +## 2.5) Publish artifact + metadata in one command + +Use the helper script to copy a real APK into the landing download path and generate checksum metadata: + +```bash +scripts/publish-apk-release.sh /path/to/your-real.apk 1.0.0 2026-05-05T08:00:00Z "Initial public APK release" +``` + +This updates: + +- `public/download/astra-matrix-latest.apk` +- `public/download/latest.json` + +The landing page reads `latest.json` to show version/build date/size/SHA256. + +## 3) Publish on your server + +Upload the APK to your web root so it is publicly available at: + +```text +/download/astra-matrix-latest.apk +``` + +Your landing page already links to this path. + +## 4) Recommended web server headers for APK + +Set these response headers for the APK file: + +- `Content-Type: application/vnd.android.package-archive` +- `Content-Disposition: attachment; filename="astra-matrix-latest.apk"` + +## 5) Post-release smoke test + +From an Android device: + +1. Open landing page +2. Tap **Download APK** +3. Install package +4. Launch app and verify startup scene renders without crash + +## 6) Security and signing reminders + +- Keep EAS signing credentials backed up and access-controlled. +- Never embed private keys or long-lived API secrets directly in the mobile client. +- If Gemini integration is required in production, use a backend proxy/API and not a raw client-side secret. diff --git a/app.json b/app.json index d31ed86..5872721 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,7 @@ { "expo": { "name": "App_369", + "owner": "psybrninja", "slug": "app-369", "version": "1.0.0", "orientation": "portrait", @@ -26,6 +27,11 @@ }, "web": { "favicon": "./assets/favicon.png" + }, + "extra": { + "eas": { + "projectId": "440a38e6-afe8-42f9-82a5-f9f987e5c44d" + } } } } diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png new file mode 100644 index 0000000..fd819af Binary files /dev/null and b/assets/adaptive-icon.png differ diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..73d9b5f Binary files /dev/null and b/assets/favicon.png differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..cb5a239 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/splash.png b/assets/splash.png new file mode 100644 index 0000000..4846829 Binary files /dev/null and b/assets/splash.png differ diff --git a/eas.json b/eas.json index 4277c79..4f7f683 100644 --- a/eas.json +++ b/eas.json @@ -13,7 +13,11 @@ "buildType": "apk" } }, - "production": {} + "production": { + "android": { + "buildType": "apk" + } + } }, "submit": { "production": {} diff --git a/package.json b/package.json index b4555aa..f591fa9 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,14 @@ "description": "", "main": "node_modules/expo/AppEntry.js", "scripts": { - "start": "node src/cli/index.js", + "start": "expo start", + "start:cli": "node src/cli/index.js", "gui": "expo start", "android": "expo start --android", - "build:apk": "eas build --platform android --profile preview", + "web": "expo start --web", + "build:web": "expo export --platform web", + "build:apk": "eas build --platform android --profile preview --non-interactive", + "build:apk:production": "eas build --platform android --profile production --non-interactive", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { diff --git a/public/download/latest.json b/public/download/latest.json new file mode 100644 index 0000000..e29cf86 --- /dev/null +++ b/public/download/latest.json @@ -0,0 +1,9 @@ +{ + "version": "0.0.0-placeholder", + "buildDate": "1970-01-01T00:00:00Z", + "fileName": "astra-matrix-latest.apk", + "fileSizeBytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "minAndroidSdk": 24, + "releaseNotes": "Placeholder artifact. Upload a real APK to publish a release." +} diff --git a/public/index.html b/public/index.html index 9933098..6051e50 100644 --- a/public/index.html +++ b/public/index.html @@ -3,31 +3,235 @@ - ASTRA MATRIX | Awaken Your Spark + ASTRA MATRIX | Android Download + -

ASTRA MATRIX

-

The construct is fabricated. The Demiurge is watching.

- -

+
+

ASTRA MATRIX

+

The Android build is distributed as a signed APK package.

+

Use the button below on your Android device to download and install.

+ +
+
+ Release + loading... + Build date + loading... + File + /download/astra-matrix-latest.apk + Size + loading... + SHA256 + loading... +
+

+
+ + +

+ +
+

Install steps (Android)

+
    +
  1. Download the APK from this page.
  2. +
  3. Open the downloaded file in your browser's downloads.
  4. +
  5. If prompted, allow installs from this source.
  6. +
  7. Tap Install and launch ASTRA MATRIX.
  8. +
+
+ +
+

Troubleshooting

+ +
+
diff --git a/scripts/publish-apk-release.sh b/scripts/publish-apk-release.sh new file mode 100755 index 0000000..38631b0 --- /dev/null +++ b/scripts/publish-apk-release.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + scripts/publish-apk-release.sh [build_date_utc] [release_notes] + +Example: + scripts/publish-apk-release.sh ~/Downloads/app-release.apk 1.2.0 2026-05-05T07:30:00Z "Stability improvements" +USAGE +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +if [ "$#" -lt 2 ]; then + usage + exit 1 +fi + +APK_SOURCE="$1" +VERSION="$2" +BUILD_DATE="${3:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}" +RELEASE_NOTES="${4:-Release ${VERSION}}" +TARGET_DIR="public/download" +TARGET_APK="${TARGET_DIR}/astra-matrix-latest.apk" +TARGET_META="${TARGET_DIR}/latest.json" + +if [ ! -f "${APK_SOURCE}" ]; then + echo "APK source file not found: ${APK_SOURCE}" >&2 + exit 1 +fi + +mkdir -p "${TARGET_DIR}" +if [ "$(realpath "${APK_SOURCE}")" != "$(realpath "${TARGET_APK}")" ]; then + cp "${APK_SOURCE}" "${TARGET_APK}" +fi + +FILE_SIZE_BYTES="$(wc -c < "${TARGET_APK}" | tr -d ' ')" +SHA256="$(sha256sum "${TARGET_APK}" | awk '{print $1}')" + +json_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +VERSION_ESCAPED="$(json_escape "${VERSION}")" +BUILD_DATE_ESCAPED="$(json_escape "${BUILD_DATE}")" +NOTES_ESCAPED="$(json_escape "${RELEASE_NOTES}")" + +cat > "${TARGET_META}" < 0 + ? scene.choices.slice(0, 3).map((choice, index) => ({ + id: choice.id || `choice_${index + 1}`, + label: choice.label || `Option ${index + 1}` + })) + : fallbackScene.choices; + + return { + text: scene.text || fallbackScene.text, + choices: safeChoices, + xpReward: Number.isFinite(scene.xpReward) ? scene.xpReward : fallbackScene.xpReward, + gnosisGain: Number.isFinite(scene.gnosisGain) ? scene.gnosisGain : fallbackScene.gnosisGain + }; } async generateNextScene(userAction, currentSceneText) { const aiName = state.getVar('aiName') || "Your Guide"; const currentLevel = state.player.level; const quarter = currentLevel <= 10 ? "Discovery" : currentLevel <= 20 ? "Resistance" : currentLevel <= 30 ? "Infiltration" : "Escape/Awakening"; + const fallbackScene = this.buildOfflineScene(userAction, currentSceneText); + + if (!this.isAiEnabled || !this.model) { + const scene = this.normalizeScene(fallbackScene, fallbackScene); + state.addXP(scene.xpReward); + state.player.gnosis += scene.gnosisGain; + return scene; + } const prompt = ` You are the entity "${aiName}". Guide the player (the divine spark) through the construct known as Reality ('toy-train town') created by the 'Demiurge'. @@ -39,13 +115,22 @@ class NarrativeService { "gnosisGain": 10 } `; - const result = await this.model.generateContent(prompt); - const response = await result.response; - const json = JSON.parse(response.text()); - - state.addXP(json.xpReward); - state.player.gnosis += (json.gnosisGain || 0); - return json; + try { + const result = await this.model.generateContent(prompt); + const response = await result.response; + const parsed = JSON.parse(response.text()); + const scene = this.normalizeScene(parsed, fallbackScene); + state.addXP(scene.xpReward); + state.player.gnosis += scene.gnosisGain; + return scene; + } catch (error) { + this.lastFailureAt = Date.now(); + console.error('NarrativeService: AI generation failed, using offline fallback', error); + const scene = this.normalizeScene(fallbackScene, fallbackScene); + state.addXP(scene.xpReward); + state.player.gnosis += scene.gnosisGain; + return scene; + } } } diff --git a/src/core/SceneEngine.js b/src/core/SceneEngine.js index 1b9b528..a8ab73e 100644 --- a/src/core/SceneEngine.js +++ b/src/core/SceneEngine.js @@ -44,7 +44,10 @@ class SceneEngine { async handleChoice(choice) { this.dynamicScene = null; if (choice.action === 'perform_restore') await corruption.performRestore(); - if (choice.action === 'sync_github') await github.authenticate('dummy-token'); + if (choice.action === 'sync_github') { + const githubToken = process.env.GITHUB_TOKEN || null; + await github.authenticate(githubToken); + } if (choice.action === 'apply_expansion') await github.applyExpansion(); if (choice.nextScene) { diff --git a/src/core/services/GitHubService.js b/src/core/services/GitHubService.js index 36e6670..e57e0e7 100644 --- a/src/core/services/GitHubService.js +++ b/src/core/services/GitHubService.js @@ -1,16 +1,61 @@ -const { Octokit } = require('octokit'); -const simpleGit = require('simple-git'); const state = require('../State'); +function getRuntimeRequire() { + try { + // Using eval avoids static module resolution in Metro during mobile bundling. + return eval('require'); + } catch (error) { + return null; + } +} + class GitHubService { constructor() { this.octokit = null; - this.git = simpleGit(); + this.git = null; + this.Octokit = null; + this.isNodeRuntime = typeof process !== 'undefined' && !!(process.versions && process.versions.node); this.repoOwner = "ALaustrup"; this.repoName = "App_369"; } + loadOctokit() { + if (!this.isNodeRuntime) return null; + if (this.Octokit) return this.Octokit; + try { + const runtimeRequire = getRuntimeRequire(); + if (!runtimeRequire) return null; + this.Octokit = runtimeRequire('octokit').Octokit; + return this.Octokit; + } catch (error) { + console.warn('GitHubService: Octokit unavailable in this runtime'); + return null; + } + } + + loadGitClient() { + if (!this.isNodeRuntime) return null; + if (this.git) return this.git; + try { + const runtimeRequire = getRuntimeRequire(); + if (!runtimeRequire) return null; + this.git = runtimeRequire('simple-git')(); + return this.git; + } catch (error) { + console.warn('GitHubService: simple-git unavailable in this runtime'); + return null; + } + } + async authenticate(token) { + if (!token || token === 'dummy-token') { + console.warn('GitHubService: Valid token not provided; running in offline mode'); + return false; + } + + const Octokit = this.loadOctokit(); + if (!Octokit) return false; + try { this.octokit = new Octokit({ auth: token }); const { data: user } = await this.octokit.rest.users.getAuthenticated(); @@ -37,10 +82,16 @@ class GitHubService { } async commitProgress(message) { + const git = this.loadGitClient(); + if (!git) { + console.warn('GitHubService: commitProgress not supported in this environment'); + return false; + } + try { - await this.git.add('.'); - await this.git.commit(message); - await this.git.push('origin', 'main'); + await git.add('.'); + await git.commit(message); + await git.push('origin', 'main'); console.log(`GitHubService: Progress committed - "${message}"`); return true; } catch (error) { diff --git a/src/core/services/Web3Service.js b/src/core/services/Web3Service.js index c3850ee..cdf4e88 100644 --- a/src/core/services/Web3Service.js +++ b/src/core/services/Web3Service.js @@ -1,17 +1,40 @@ -const { ethers } = require('ethers'); const state = require('../State'); +function getRuntimeRequire() { + try { + return eval('require'); + } catch (error) { + return null; + } +} + class Web3Service { constructor() { this.provider = null; this.signer = null; + this.ethers = null; + } + + loadEthers() { + if (this.ethers) return this.ethers; + try { + const runtimeRequire = getRuntimeRequire(); + if (!runtimeRequire) return null; + this.ethers = runtimeRequire('ethers'); + return this.ethers; + } catch (error) { + console.warn('Web3Service: ethers unavailable in this runtime'); + return null; + } } async connectWallet(privateKey = null) { try { if (privateKey) { + const ethersLib = this.loadEthers(); + if (!ethersLib) return null; // CLI / Manual private key connection - const wallet = new ethers.Wallet(privateKey); + const wallet = new ethersLib.Wallet(privateKey); state.player.walletAddress = wallet.address; this.signer = wallet; return wallet.address; diff --git a/vercel.json b/vercel.json index b438f58..e354a18 100644 --- a/vercel.json +++ b/vercel.json @@ -1,9 +1,26 @@ { "version": 2, "public": true, - "buildCommand": "npx expo export --platform web", - "outputDirectory": "dist", + "buildCommand": "echo \"Static landing deployment\"", + "outputDirectory": "public", + "headers": [ + { + "source": "/download/astra-matrix-latest.apk", + "headers": [ + { "key": "Content-Type", "value": "application/vnd.android.package-archive" }, + { "key": "Content-Disposition", "value": "attachment; filename=\"astra-matrix-latest.apk\"" }, + { "key": "Cache-Control", "value": "no-cache" } + ] + }, + { + "source": "/download/latest.json", + "headers": [ + { "key": "Cache-Control", "value": "no-store, must-revalidate" } + ] + } + ], "rewrites": [ + { "source": "/download/(.*)", "destination": "/download/$1" }, { "source": "/(.*)", "destination": "/index.html" } ] }