diff --git a/CHANGELOG.md b/CHANGELOG.md index eb2a171..71041fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ### Fixed +- Pinokio now recognizes its nested successful-install marker, so a completed + one-click install exposes Start instead of returning to Install. +- The keyless `dev-fresh.sh` startup summary now names Esri World Imagery with + keyless terrain and identifies OpenStreetMap as the fallback. - All three VIIRS sources now reach the Active Fires layer. Merging a source's detections used argument spread, which exceeds the engine's argument limit on the two largest sources and dropped them entirely — leaving roughly a third of diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1cde30a..f86f627 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2253,6 +2253,10 @@ silently demoting every later lookup for the session. ## Auth + Launch - Recommended launcher: `./scripts/dev-fresh.sh` (also: `dev-secure.sh` for stricter bindings, `dev-cctv.sh` for CCTV source-pack tuning) +- A successful Pinokio install writes the owner-only `pinokio/.installed` + marker. The nested launcher menu resolves that marker from its own directory: + an absent marker exposes Install, a present marker exposes Start, and a + running server with a captured ready URL exposes Open God's Eye View. - Build gate: `npm run build` - Network access: local-only by default (`HOST=localhost` in dev-fresh.sh); LAN is an explicit opt-in via `HOST=0.0.0.0` (launcher prints a key-exposure warning + LAN URL; see SECURITY.md) - OpenSky default mode: OAuth (`OPENSKY_AUTH_MODE=oauth`; `anon` works without credentials) diff --git a/pinokio/pinokio.js b/pinokio/pinokio.js index d1af951..d781c1a 100644 --- a/pinokio/pinokio.js +++ b/pinokio/pinokio.js @@ -2,8 +2,8 @@ module.exports = { version: '3.6', title: "God's Eye View", description: 'A live 3D intelligence console for planet Earth.', - menu: async (_kernel, info) => { - const installed = info.exists('.installed'); + menu: async (kernel, info) => { + const installed = await kernel.exists(__dirname, '.installed'); const installing = info.running('install.js'); const starting = info.running('start.js'); const updating = info.running('update.js'); diff --git a/scripts/dev-fresh.sh b/scripts/dev-fresh.sh index 9f21f42..0b01932 100755 --- a/scripts/dev-fresh.sh +++ b/scripts/dev-fresh.sh @@ -329,7 +329,7 @@ if [[ -n "${GOOGLE_MAPS_API_KEY}" ]]; then elif [[ -n "${CESIUM_ION_TOKEN}" ]]; then echo "Startup map: Google Photorealistic 3D Tiles (Cesium ion)" else - echo "Startup map: OpenStreetMap with keyless terrain" + echo "Startup map: Esri World Imagery with keyless terrain (OpenStreetMap fallback)" fi [[ -n "${CESIUM_ION_TOKEN}" ]] && echo "Cesium ion token: configured — Google 3D, Bing, and world-terrain stacks available" || echo "Cesium ion token: not set" [[ -n "${TOMTOM_API_KEY}" ]] && echo "TomTom key (live traffic flow): configured" || echo "TomTom key (live traffic flow): not set — simulated traffic" diff --git a/src/devFreshDotenv.test.mjs b/src/devFreshDotenv.test.mjs index c488325..e41689b 100644 --- a/src/devFreshDotenv.test.mjs +++ b/src/devFreshDotenv.test.mjs @@ -80,13 +80,38 @@ test('dev-fresh passes names-only boot provenance before resolving file fallback assert.match(source, /put_env GEV_KEY_SETUP_EXTERNAL_KEYS "\$\{KEY_SETUP_EXTERNAL_KEYS_CSV\}"/); }); +const bashTest = process.platform === 'win32' ? test.skip : test; + +bashTest('dev-fresh reports the real keyless startup map and fallback', async () => { + const script = await fs.readFile(new URL('../scripts/dev-fresh.sh', import.meta.url), 'utf8'); + const start = script.indexOf('if [[ -n "${GOOGLE_MAPS_API_KEY}" ]]; then\n echo "Startup map:'); + const end = script.indexOf('\nfi', start); + assert.ok(start >= 0 && end > start, 'startup-map status block not found in dev-fresh.sh'); + const block = script.slice(start, end + 3); + assert.doesNotMatch(block, /Startup map: OpenStreetMap with keyless terrain/); + + const { execFile } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const run = promisify(execFile); + const keyless = await run('bash', ['-c', block], { + env: { + PATH: process.env.PATH, + GOOGLE_MAPS_API_KEY: '', + CESIUM_ION_TOKEN: '', + }, + }); + assert.equal( + keyless.stdout.trim(), + 'Startup map: Esri World Imagery with keyless terrain (OpenStreetMap fallback)', + ); +}); + // Stock macOS ships bash 3.2, where expanding an EMPTY array under `set -u` // is a fatal "unbound variable" — the `:-` guard on the provenance CSV is what // keeps the keyless `dev-fresh.sh` launch alive there. Newer bash never fails // this way, so the idiom itself is asserted textually and the block's behavior // is exercised for both the keyless and the populated case. -const bashTest = process.platform === 'win32' ? test.skip : test; bashTest('the external-keys provenance block survives set -u keyless and joins names when keys are exported', async () => { const script = await fs.readFile(new URL('../scripts/dev-fresh.sh', import.meta.url), 'utf8'); const start = script.indexOf('KEY_SETUP_EXTERNAL_KEYS=()'); diff --git a/src/pinokioLauncherContract.test.mjs b/src/pinokioLauncherContract.test.mjs index f373679..d4fe889 100644 --- a/src/pinokioLauncherContract.test.mjs +++ b/src/pinokioLauncherContract.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { realpathSync } from 'node:fs'; -import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { access, copyFile, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; @@ -62,12 +62,9 @@ test('Pinokio start has one fail-closed launcher process', () => { test('Pinokio install records success explicitly instead of trusting node_modules', async () => { const install = require('../pinokio/install.js'); const fs = await import('node:fs/promises'); - const menuSource = await fs.readFile(new URL('../pinokio/pinokio.js', import.meta.url), 'utf8'); const installSource = await fs.readFile(new URL('../scripts/pinokio-install.mjs', import.meta.url), 'utf8'); assert.equal(install.run.at(-1).params.message, 'node scripts/pinokio-install.mjs'); assert.equal(install.run[0].when, "{{!kernel.exists(cwd, 'ENVIRONMENT')}}"); - assert.match(menuSource, /info\.exists\('\.installed'\)/); - assert.doesNotMatch(menuSource, /exists\('\.\.\/node_modules'\)/); assert.match(installSource, /includeKeychain: false/); assert.match(installSource, /authoritativeEnvironment: true/); assert.match(installSource, /applyPinokioEnvironment\(\)/); @@ -80,6 +77,79 @@ test('Pinokio install records success explicitly instead of trusting node_module } }); +test('Pinokio menu resolves the nested install marker and exposes each lifecycle state', async (t) => { + const fixture = await mkdtemp(path.join(os.tmpdir(), 'gev-pinokio-menu-')); + t.after(() => rm(fixture, { recursive: true, force: true })); + const launcherDir = path.join(fixture, 'app', 'pinokio'); + const launcherPath = path.join(launcherDir, 'pinokio.js'); + const markerPath = path.join(launcherDir, '.installed'); + await mkdir(launcherDir, { recursive: true }); + await copyFile(new URL('../pinokio/pinokio.js', import.meta.url), launcherPath); + + const existsCalls = []; + const kernel = { + exists: async (...chunks) => { + existsCalls.push(chunks); + try { + await access(path.resolve(...chunks)); + return true; + } catch { + return false; + } + }, + }; + const runtime = { running: null, url: null }; + const info = { + exists: () => assert.fail('menu must resolve the marker through kernel.exists'), + running: (href) => runtime.running === href, + local: () => runtime.url ? { url: runtime.url } : {}, + }; + const { menu } = require(launcherPath); + const render = async ({ installed, running = null, url = null }) => { + if (installed) await writeFile(markerPath, 'ready\n'); + else await rm(markerPath, { force: true }); + runtime.running = running; + runtime.url = url; + const items = await menu(kernel, info); + assert.equal(items.filter((item) => item.default).length, 1); + return items.map(({ text, href, default: isDefault = false }) => ({ text, href, default: isDefault })); + }; + + assert.deepEqual(await render({ installed: false }), [ + { text: 'Install', href: 'install.js', default: true }, + ]); + assert.deepEqual(await render({ installed: true }), [ + { text: 'Start', href: 'start.js', default: true }, + { text: 'Update', href: 'update.js', default: false }, + { text: 'Repair installation', href: 'reset.js', default: false }, + ]); + for (const [running, text] of [ + ['install.js', 'Installing'], + ['update.js', 'Updating'], + ['reset.js', 'Resetting'], + ]) { + assert.deepEqual(await render({ installed: true, running }), [ + { text, href: running, default: true }, + ]); + } + assert.deepEqual(await render({ installed: true, running: 'start.js' }), [ + { text: 'Starting', href: 'start.js', default: true }, + ]); + assert.deepEqual(await render({ + installed: true, + running: 'start.js', + url: 'http://127.0.0.1:4173/', + }), [ + { text: "Open God's Eye View", href: 'http://127.0.0.1:4173/', default: true }, + { text: 'Server', href: 'start.js', default: false }, + ]); + assert.ok(existsCalls.length >= 7); + const resolvedLauncherDir = realpathSync(launcherDir); + for (const chunks of existsCalls) { + assert.deepEqual(chunks, [resolvedLauncherDir, '.installed']); + } +}); + test('Pinokio install recognizes direct execution through a linked app directory', async (t) => { const fixture = await mkdtemp(path.join(os.tmpdir(), 'gev-pinokio-entry-')); t.after(() => rm(fixture, { recursive: true, force: true }));