feat: ship skill and MCP workflows with signed updater for v0.0.7
This commit is contained in:
1 parent
1b52df0603
commit
a01085c656
136 files changed
+18646
-217
No files matched your search
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const fsp = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const { execFileSync, spawnSync } = require('node:child_process')
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..')
|
||||
const REGISTRY_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\8a4ee941-d893-569b-8bcb-2a55d270c04b'
|
||||
const STAMP = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const RUN_ROOT = path.join(ROOT, '.tmp', 'updates-installer-marker', STAMP)
|
||||
const LATEST_REPORT = path.join(ROOT, '.tmp', 'updates-installer-marker', 'latest-report.json')
|
||||
|
||||
function parseRegistryValue(output, name) {
|
||||
const expression = new RegExp(`^\\s*${name}\\s+REG_\\w+\\s+(.+?)\\s*$`, 'mi')
|
||||
const match = expression.exec(output)
|
||||
return match ? match[1].replace(/^"(.*)"$/, '$1') : null
|
||||
}
|
||||
|
||||
function queryRegistryValue(name) {
|
||||
const output = execFileSync('reg.exe', ['query', REGISTRY_KEY, '/v', name], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 10_000,
|
||||
maxBuffer: 64 * 1024
|
||||
})
|
||||
return parseRegistryValue(output, name)
|
||||
}
|
||||
|
||||
function registryExecutablePath(value) {
|
||||
const quoted = /^\s*"([^"]+\.exe)"/i.exec(value)
|
||||
if (quoted) return quoted[1]
|
||||
const plain = /^\s*(.+?\.exe)(?:\s|$|,)/i.exec(value)
|
||||
return plain ? plain[1] : null
|
||||
}
|
||||
|
||||
function relativeImport(from, to) {
|
||||
return `./${path.relative(path.dirname(from), to).replace(/\\/g, '/')}`
|
||||
}
|
||||
|
||||
function createRunnerSource(filename) {
|
||||
const installer = path.join(ROOT, 'src', 'main', 'updates', 'installer.ts')
|
||||
const physical = path.join(ROOT, 'src', 'main', 'updates', 'physical-files.ts')
|
||||
return `
|
||||
import { app } from 'electron'
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { createNsisInstaller, queryNsisRegistryValue } from ${JSON.stringify(relativeImport(filename, installer))}
|
||||
import { physicalRealPath, physicalStat } from ${JSON.stringify(relativeImport(filename, physical))}
|
||||
|
||||
function decodeConfig() {
|
||||
return JSON.parse(Buffer.from(process.env.YCODE_MARKER_CONFIG || '', 'base64').toString('utf8'))
|
||||
}
|
||||
|
||||
async function captureError(operation) {
|
||||
try {
|
||||
await operation()
|
||||
return null
|
||||
} catch (error) {
|
||||
return {
|
||||
code: error && typeof error === 'object' && 'code' in error ? error.code : null,
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = decodeConfig()
|
||||
const patched = await fsPromises.stat(config.appPath)
|
||||
const physicalAsar = await physicalStat(config.appPath)
|
||||
const physicalResources = await physicalStat(path.join(config.installationDirectory, 'resources'))
|
||||
|
||||
const validAdapter = createNsisInstaller({
|
||||
platform: 'win32',
|
||||
isPackaged: true,
|
||||
execPath: config.executablePath,
|
||||
appPath: config.appPath,
|
||||
cacheDirectory: config.cacheDirectory,
|
||||
resolveRealPath: physicalRealPath,
|
||||
readStat: physicalStat,
|
||||
queryRegistryValue: queryNsisRegistryValue
|
||||
})
|
||||
const environment = await validAdapter.validateEnvironment(1)
|
||||
|
||||
const directoryFixtureAdapter = createNsisInstaller({
|
||||
platform: 'win32',
|
||||
isPackaged: true,
|
||||
execPath: config.directoryFixture.executablePath,
|
||||
appPath: config.directoryFixture.appPath,
|
||||
cacheDirectory: config.cacheDirectory,
|
||||
resolveRealPath: physicalRealPath,
|
||||
readStat: physicalStat,
|
||||
queryRegistryValue: async name => {
|
||||
if (name === 'UninstallString') return '"' + config.directoryFixture.uninstallerPath + '" /currentuser'
|
||||
if (name === 'DisplayIcon') return config.directoryFixture.executablePath + ',0'
|
||||
return null
|
||||
}
|
||||
})
|
||||
const directoryError = await captureError(() => directoryFixtureAdapter.validateEnvironment(0))
|
||||
|
||||
const wrongRegistryAdapter = createNsisInstaller({
|
||||
platform: 'win32',
|
||||
isPackaged: true,
|
||||
execPath: config.executablePath,
|
||||
appPath: config.appPath,
|
||||
cacheDirectory: config.cacheDirectory,
|
||||
resolveRealPath: physicalRealPath,
|
||||
readStat: physicalStat,
|
||||
queryRegistryValue: async name => {
|
||||
if (name === 'UninstallString') return '"C:\\\\Wrong\\\\Uninstall YCode.exe" /currentuser'
|
||||
if (name === 'DisplayIcon') return config.executablePath + ',0'
|
||||
return null
|
||||
}
|
||||
})
|
||||
const wrongRegistryError = await captureError(() => wrongRegistryAdapter.validateEnvironment(0))
|
||||
|
||||
const result = {
|
||||
electronVersion: process.versions.electron,
|
||||
nodeVersion: process.versions.node,
|
||||
installationDirectory: environment.installationDirectory,
|
||||
executablePath: environment.executablePath,
|
||||
appPath: config.appPath,
|
||||
patchedFs: { isFile: patched.isFile(), isDirectory: patched.isDirectory() },
|
||||
physicalAsar: { isFile: physicalAsar.isFile(), isDirectory: physicalAsar.isDirectory() },
|
||||
physicalResources: { isFile: physicalResources.isFile(), isDirectory: physicalResources.isDirectory() },
|
||||
directoryError,
|
||||
wrongRegistryError,
|
||||
installerLaunched: false
|
||||
}
|
||||
console.log('YCODE_MARKER_RESULT=' + Buffer.from(JSON.stringify(result)).toString('base64'))
|
||||
app.exit(0)
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error)
|
||||
app.exit(1)
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.platform !== 'win32') throw new Error('Installer marker verification requires Windows')
|
||||
await fsp.mkdir(RUN_ROOT, { recursive: true })
|
||||
|
||||
const uninstallString = queryRegistryValue('UninstallString')
|
||||
const displayIcon = queryRegistryValue('DisplayIcon')
|
||||
const displayVersion = queryRegistryValue('DisplayVersion')
|
||||
const uninstallerPath = uninstallString ? registryExecutablePath(uninstallString) : null
|
||||
const executablePath = displayIcon
|
||||
? registryExecutablePath(displayIcon.replace(/,\s*-?\d+\s*$/, ''))
|
||||
: null
|
||||
if (!uninstallerPath || !executablePath) throw new Error('Current HKCU YCode registry paths are incomplete')
|
||||
const installationDirectory = path.dirname(executablePath)
|
||||
if (path.dirname(uninstallerPath).toLowerCase() !== installationDirectory.toLowerCase()) {
|
||||
throw new Error('Current HKCU uninstaller and icon paths disagree')
|
||||
}
|
||||
const appPath = path.join(installationDirectory, 'resources', 'app.asar')
|
||||
const cacheDirectory = path.join(RUN_ROOT, 'cache')
|
||||
await fsp.mkdir(cacheDirectory, { recursive: true })
|
||||
|
||||
const directoryFixtureRoot = path.join(RUN_ROOT, 'directory-marker-fixture')
|
||||
const directoryFixtureResources = path.join(directoryFixtureRoot, 'resources')
|
||||
await fsp.mkdir(path.join(directoryFixtureResources, 'app.asar'), { recursive: true })
|
||||
const directoryFixtureExe = path.join(directoryFixtureRoot, 'YCode.exe')
|
||||
const directoryFixtureUninstaller = path.join(directoryFixtureRoot, 'Uninstall YCode.exe')
|
||||
await fsp.writeFile(directoryFixtureExe, 'fixture')
|
||||
await fsp.writeFile(directoryFixtureUninstaller, 'fixture')
|
||||
|
||||
const runnerEntry = path.join(RUN_ROOT, 'runner-entry.ts')
|
||||
const runnerOutput = path.join(RUN_ROOT, 'runner.cjs')
|
||||
await fsp.writeFile(runnerEntry, createRunnerSource(runnerEntry), 'utf8')
|
||||
const esbuild = path.join(ROOT, 'node_modules', 'esbuild', 'bin', 'esbuild')
|
||||
execFileSync(process.execPath, [
|
||||
esbuild,
|
||||
runnerEntry,
|
||||
'--bundle',
|
||||
'--platform=node',
|
||||
'--format=cjs',
|
||||
'--external:electron',
|
||||
`--outfile=${runnerOutput}`,
|
||||
'--log-level=warning'
|
||||
], { cwd: ROOT, stdio: 'inherit' })
|
||||
|
||||
const electronBinary = require('electron')
|
||||
const config = {
|
||||
installationDirectory,
|
||||
executablePath,
|
||||
appPath,
|
||||
cacheDirectory,
|
||||
directoryFixture: {
|
||||
executablePath: directoryFixtureExe,
|
||||
uninstallerPath: directoryFixtureUninstaller,
|
||||
appPath: path.join(directoryFixtureResources, 'app.asar')
|
||||
}
|
||||
}
|
||||
const env = {
|
||||
...process.env,
|
||||
YCODE_MARKER_CONFIG: Buffer.from(JSON.stringify(config)).toString('base64')
|
||||
}
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
delete env.ELECTRON_NO_ASAR
|
||||
const child = spawnSync(electronBinary, [runnerOutput], {
|
||||
cwd: RUN_ROOT,
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 45_000,
|
||||
maxBuffer: 4 * 1024 * 1024
|
||||
})
|
||||
if (child.error) throw child.error
|
||||
if (child.status !== 0) {
|
||||
throw new Error(`Electron marker runner failed (${child.status}): ${child.stderr || child.stdout}`)
|
||||
}
|
||||
const marker = child.stdout.split(/\r?\n/).find(line => line.startsWith('YCODE_MARKER_RESULT='))
|
||||
if (!marker) throw new Error(`Electron marker runner returned no result: ${child.stderr || child.stdout}`)
|
||||
const result = JSON.parse(Buffer.from(marker.slice('YCODE_MARKER_RESULT='.length), 'base64').toString('utf8'))
|
||||
|
||||
assert.equal(result.installationDirectory.toLowerCase(), installationDirectory.toLowerCase())
|
||||
assert.equal(result.patchedFs.isFile, false)
|
||||
assert.equal(result.patchedFs.isDirectory, true)
|
||||
assert.equal(result.physicalAsar.isFile, true)
|
||||
assert.equal(result.physicalAsar.isDirectory, false)
|
||||
assert.equal(result.physicalResources.isFile, false)
|
||||
assert.equal(result.physicalResources.isDirectory, true)
|
||||
assert.equal(result.directoryError?.code, 'installer-marker')
|
||||
assert.match(result.directoryError?.message || '', /app\.asar/)
|
||||
assert.equal(result.wrongRegistryError?.code, 'installer-registry')
|
||||
assert.equal(result.installerLaunched, false)
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
passed: true,
|
||||
registryDisplayVersion: displayVersion,
|
||||
uninstallerPath,
|
||||
executablePath,
|
||||
appPath,
|
||||
runnerOutput,
|
||||
result
|
||||
}
|
||||
const reportPath = path.join(RUN_ROOT, 'report.json')
|
||||
await fsp.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
|
||||
await fsp.mkdir(path.dirname(LATEST_REPORT), { recursive: true })
|
||||
await fsp.writeFile(LATEST_REPORT, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
|
||||
console.log(`PASS installer marker: patched fs directory, original-fs file, directory and registry rejection`)
|
||||
console.log(`report: ${reportPath}`)
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in new issue
Block a user