v1.1.1 - big upgrade

Includes:
1) Better error handling and move support
2) An upgradeable launcher with signatures to avoid having to download 70MB just
   to get the latest 200kb of javascript
3) Data stored in a data file instead of in the program, to allow for modifications
main
Mari 3 years ago
parent d26b916890
commit 70d0bb7491
  1. 3
      .gitignore
  2. 12
      .idea/runConfigurations/build_signer.xml
  3. 24
      .idea/runConfigurations/sign.xml
  4. 1
      .idea/smva-indexer.iml
  5. 112
      default-data.json
  6. 460
      indexer.mjs
  7. 9088
      package-lock.json
  8. 30
      package.json
  9. 8
      publish-commands.sftp
  10. 36
      rollup.config.js
  11. 62
      sign-bundle.mjs
  12. 32
      signer-rollup.config.js
  13. 16
      signing-common.mjs
  14. 20
      smva-indexer-release.pub.pem
  15. 84
      updateable-launcher.mjs

3
.gitignore vendored

@ -1,3 +1,4 @@
/node_modules/
/build/
/rollup/
/smva-indexer-release.key.pem
/smva-indexer-release.params.pem

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="build-signer" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="build-signer" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sign" type="NodeJSConfigurationType" application-parameters="build/smva-indexer-bundle.js build/smva-indexer-bundle-signed.js smva-indexer-release.key.pem" path-to-js-file="build/sign-bundle.js" working-dir="$PROJECT_DIR$">
<method v="2">
<option name="NpmBeforeRunTask" enabled="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="build-signer" />
</scripts>
<node-interpreter value="project" />
<envs />
</option>
<option name="NpmBeforeRunTask" enabled="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="build-rollup" />
</scripts>
<node-interpreter value="project" />
<envs />
</option>
</method>
</configuration>
</component>

@ -5,6 +5,7 @@
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

@ -0,0 +1,112 @@
{
"fileRegExp": "saint-miluinas-vore-academy-.*\\.save",
"times": [
"Day 1",
"Day 2",
"Day 3",
"Day 4",
"Day 5",
"Day 6"
],
"voreTypes": [
"Oral",
"Anal",
"Unbirth"
],
"voreRoles": [
"Predator",
"Prey",
"Observer"
],
"voreResults": [
"Fatal",
"Always Fatal",
"Non-fatal"
],
"characterSceneTypes": [
],
"otherSceneTypes": [
],
"characters": [
"Emma",
"Leah",
"Mallory",
"Mina",
"Alice",
"Sam",
"Alesha",
"Sarah",
"Kayla",
"Cherry",
"Cherry",
"Leila",
"Beli",
"Nadia",
"Sunny",
"Charlotte",
"Mandy",
"Betty",
"Cassidy",
"Claire",
"Erika",
"Miranda",
"Janet",
"Zoey",
"Kim",
"Matilda",
"Maeve",
"Bree",
"Morgan",
"Kira",
"Izumi",
"Calina",
"Marie",
"Joan",
"Professor Juniper",
"Professor Slate",
"Professor Steria",
"Professor Florida",
"Professor Bora",
"Professor Stars",
"Nora (The Custodian)",
"Anita",
"Chandra",
"Hannah",
"Gabby",
"Pat",
"Alex",
"Mia",
"Tia",
"Olivia",
"Penny",
"Gina",
"Kris",
"Haley",
"Jackie",
"Madison",
"Sadie",
"Karen",
"Bella",
"Donna",
"Brit",
"Ashley",
"Rachal",
"Bethany",
"Carol",
"Trisha",
"Lily",
"Meghan",
"Candice",
"Emily",
"Kamile",
"Nikki",
"Tara",
"April",
"Catherine",
"Iris",
"Nicole",
"Amelia",
"Maria",
"Liz"
]
}

@ -1,56 +1,206 @@
import enquirer from "enquirer"
import {watch} from "fs"
import {access, readFile, opendir, rename, stat, writeFile} from "fs/promises"
import {watch, constants as fsConstants} from "fs"
import {access, readFile, opendir, stat, writeFile} from "fs/promises"
import ini from "ini"
import envPaths from "env-paths"
import chalk from "chalk"
import {isAbsolute, join, normalize, basename} from "path"
import {isAbsolute, join, normalize, basename, sep} from "path"
import {moveFile} from "move-file"
import isValidFilename from "valid-filename"
import makeDir from "make-dir"
import homeDir from "home-dir"
import "source-map-support/register"
import defaultData from "./default-data.json"
import packageData from "./package.json"
import buildTimestamp from "consts:buildTimestamp"
import configName from "consts:configName"
const {Toggle, Select, Input} = enquirer
const {Toggle, Select, Input, AutoComplete} = enquirer
const {R_OK, W_OK} = fsConstants
function log(message) {
process.stderr.write(message + "\n")
}
async function getDirectoryFromUser(message, initial) {
while (true) {
const newDir = normalize(await new Input({
message, initial
}).run())
if (!isAbsolute(newDir)) {
log(chalk.red("Can't use that folder: This path must be absolute. Try again."))
continue
function validateFilenamePart(part, {allowEmpty = false} = {}) {
if (typeof part !== "string") {
return `must be a string, but was ${part === null ? "null" : typeof part}`
}
if (part.length === 0) {
if (allowEmpty) {
return true
} else {
return "must not be empty, but was empty"
}
}
if (!isValidFilename(part + ".save")) {
return `cannot contain any of the following characters: <>:"/\\|?*`
}
return true
}
const DIRECTORY_OK = "OK"
const DIRECTORY_EMPTY = "Path is empty"
const DIRECTORY_NOT_ABSOLUTE = "Path must be absolute"
const DIRECTORY_NOT_VALID = "Path must contain segments which are valid filenames"
const DIRECTORY_EXISTS_NOT_DIRECTORY = "Path exists, but is not a directory"
const DIRECTORY_NOT_EXISTS = "Path does not exist"
const DIRECTORY_NOT_ACCESSIBLE = "Path is a directory, but is not both readable and writable"
function validatePath(path) {
if (typeof path !== "string" || path.length === 0) {
return DIRECTORY_EMPTY
} else if (!isAbsolute(path)) {
return DIRECTORY_NOT_ABSOLUTE
} else if (!path.split(sep).slice(1).every(isValidFilename)) {
return DIRECTORY_NOT_VALID
}
return DIRECTORY_OK
}
async function validateDirectory(path) {
const pathValidity = validatePath(path)
if (pathValidity !== DIRECTORY_OK) {
return pathValidity
}
try {
const statResult = await stat(newDir)
const statResult = await stat(path)
if (!statResult.isDirectory()) {
log(chalk.red("Can't use that folder: This path must be a directory. Try again."))
return DIRECTORY_EXISTS_NOT_DIRECTORY
}
return newDir
await access(path, R_OK | W_OK)
} catch (ex) {
if (ex.code === "ENOENT") {
return DIRECTORY_NOT_EXISTS
} else if (ex.code === "EACCES") {
return DIRECTORY_NOT_ACCESSIBLE
} else {
throw ex
}
}
return DIRECTORY_OK
}
function validateRegExp(text) {
if (typeof text !== "string") {
return `must be a string, but was ${text === null ? "null" : typeof text}`
}
try {
new RegExp(text)
return true
} catch (ex) {
return `must be a valid regular expression, but couldn't construct a RegExp with it: ${ex}`
}
}
function validateFilenamePartList(list, {minLength = 0, allowMissing = false} = {}) {
if (typeof list === "undefined") {
if (allowMissing) {
return true
} else {
return `must be present, but was absent`
}
}
if (!Array.isArray(list)) {
return `must be an array, but was ${list === null ? typeof list : "null"}`
}
if (list.length < minLength) {
return `must have at least ${minLength} ${minLength === 1 ? "element" : "elements"}, but had ${list.length}`
}
const firstNonmatching = list.findIndex((item) => !(typeof item === "string" && item.length > 0 && validateFilenamePart(item) === true))
if (firstNonmatching !== -1) {
return `element at index ${firstNonmatching} ${validateFilenamePart(list[firstNonmatching])}`
}
return true
}
function validateGameData(data) {
if (typeof data !== "object" || data === null) {
return `must be an object, but was ${typeof data}`
}
const fileRegExpError = validateRegExp(data.fileRegExp)
if (validateRegExp(data.fileRegExp) !== true) {
return `fileRegExp ${fileRegExpError}`
}
const charactersError = validateFilenamePartList(data.characters, {allowMissing: true})
if (charactersError !== true) {
return `characters ${charactersError}`
}
const timesError = validateFilenamePartList(data.times, {minLength: 1})
if (timesError !== true) {
return `times ${timesError}`
}
const voreRolesError = validateFilenamePartList(data.voreRoles, {minLength: 1})
if (voreRolesError !== true) {
return `voreRoles ${voreRolesError}`
}
const voreTypesError = validateFilenamePartList(data.voreTypes, {minLength: 1})
if (voreTypesError !== true) {
return `voreTypes ${voreTypesError}`
}
const voreResultsError = validateFilenamePartList(data.voreResults, {minLength: 1})
if (voreResultsError !== true) {
return `voreResults ${voreResultsError}`
}
const characterSceneTypesError = validateFilenamePartList(data.characterSceneTypes, {allowMissing: true})
if (characterSceneTypesError !== true) {
return `characterSceneTypes ${characterSceneTypesError}`
}
const otherSceneTypesError = validateFilenamePartList(data.otherSceneTypes, {allowMissing: true})
if (otherSceneTypesError !== true) {
return `otherSceneTypes ${otherSceneTypesError}`
}
return true
}
async function getDirectoryFromUser(message, initial) {
let dirState = null
let newDir = initial
while (dirState !== DIRECTORY_OK) {
newDir = normalize(await new Input({
message,
initial: newDir,
validate: (text) => {
const pathErr = validatePath(text)
if (pathErr === DIRECTORY_OK) {
return true
} else {
return pathErr
}
}
}).run())
try {
dirState = await validateDirectory(newDir)
} catch (ex) {
log(chalk.red(`Couldn't check on that folder: ${chalk.redBright.bold(ex)}. Try again.`))
dirState = null
}
if (dirState === DIRECTORY_NOT_EXISTS) {
const create = await new Toggle({
message: "That folder doesn't exist. Want me to create it?",
enabled: "Create it",
enabled: "Yes, create it",
disabled: "No, I'll enter the path again",
}).run()
if (!create) {
continue
}
if (create) {
try {
await makeDir(newDir)
return newDir
try {
dirState = await validateDirectory(newDir)
if (dirState !== DIRECTORY_OK) {
log(chalk.red(`Creating the new folder led to an unusable directory: ${chalk.redBright.bold(dirState)}. Try again.`))
}
} catch (ex) {
log(chalk.red(`Couldn't check on the new folder: ${chalk.redBright.bold(ex)}. Try again.`))
}
} catch (ex) {
log(chalk.red(`Couldn't create that folder: ${chalk.redBright.bold(ex)}. Try again.`))
}
} else {
log(chalk.red(`Couldn't check on that folder: ${chalk.redBright.bold(ex)}. Try again.`))
}
} else if (dirState !== null && dirState !== DIRECTORY_OK) {
log(chalk.red(`Can't use that folder: ${dirState}. Try again.`))
}
}
return newDir
}
async function loadPaths(config, home) {
@ -74,24 +224,62 @@ async function loadPaths(config, home) {
}
}
}
if (!(sourceDir && destDir)) {
log(chalk.yellow(`Configuration at ${chalk.yellowBright.bold(configPath)} is missing or incomplete. Configure now.`))
const sourceDirOK = (await validateDirectory(sourceDir)) === DIRECTORY_OK
const destDirOK = (await validateDirectory(destDir)) === DIRECTORY_OK
if (!(sourceDirOK && destDirOK)) {
log(chalk.yellow(`Configuration at ${chalk.yellowBright.bold(configPath)} is missing, incomplete, or damaged. Configuring now.`))
if (!sourceDirOK) {
sourceDir = await getDirectoryFromUser(
"Where are SMVA saves downloaded to? (e.g., your Downloads folder)",
sourceDir || join(home, "Downloads"))
}
if (!destDirOK) {
destDir = await getDirectoryFromUser(
"Where do you want to store your organized SMVA saves? (e.g., your Documents folder)",
destDir || join(home, "Documents", "SMVA Saves")
)
}
try {
await makeDir(config)
await writeFile(configPath, ini.encode({paths: {source: sourceDir, dest: destDir}}), {encoding: "utf-8"})
log(chalk.cyan(`Wrote configuration to ${chalk.cyanBright.bold(configPath)}`))
} catch (ex) {
log(chalk.red(`Could not write configuration to ${chalk.redBright.bold(configPath)}`))
}
}
const dataPath = join(config, "game-data.json")
let data = defaultData
try {
data = JSON.parse(await readFile(dataPath, {encoding: "utf-8"}))
const err = validateGameData(data)
if (err === true) {
log(chalk.cyan(`Read game data from ${chalk.cyanBright.bold(dataPath)}`))
} else {
log(chalk.red(`${chalk.redBright.bold(dataPath)} had a problem: ${chalk.redBright.bold(err)}`))
log(chalk.yellow(`Using default game data.`))
data = defaultData
}
return {sourceDir, destDir}
} catch (ex) {
switch (ex.code) {
case "ENOENT":
log(chalk.yellow(`${chalk.yellowBright.bold(dataPath)} not found. Using default game data.`))
try {
await writeFile(dataPath, JSON.stringify(defaultData, null, 4), {encoding: "utf-8"})
log(chalk.cyan(`Wrote default game data to ${chalk.cyanBright.bold(dataPath)}`))
} catch (ex) {
log(chalk.red(`Could not write default game data to ${chalk.redBright.bold(dataPath)}: ${chalk.redBright.bold(ex)}`))
}
break
default:
log(chalk.red(`Could not read game data from ${chalk.redBright.bold(dataPath)}: ${chalk.redBright.bold(ex)}`))
log(chalk.yellow(`Using default game data.`))
}
}
return {sourceDir, destDir, data}
}
async function* watchNewSaves(sourceDir) {
const sourceRe = /^saint-miluinas-vore-academy-.*\.save$/
async function* watchNewSaves(sourceDir, sourceRegexpString) {
const sourceRe = new RegExp(`^${sourceRegexpString}\$`)
let resolveNext
let nextPromise
let active = true
@ -104,6 +292,7 @@ async function* watchNewSaves(sourceDir) {
}
})
}
makeNewPromise()
watch(sourceDir, {}, (eventType, filename) => {
@ -112,7 +301,7 @@ async function* watchNewSaves(sourceDir) {
}
})
log(chalk.green(`Waiting for new saves to appear in ${chalk.greenBright.bold(sourceDir)}`))
log(chalk.green(`Waiting for new saves with filenames like ${chalk.greenBright.bold(`/${sourceRegexpString}/`)} to appear in ${chalk.greenBright.bold(sourceDir)}`))
while (true) {
const next = await nextPromise
@ -122,10 +311,10 @@ async function* watchNewSaves(sourceDir) {
}
}
async function* findExistingSaves(sourceDir) {
const sourceRe = /^saint-miluinas-vore-academy-.*\.save$/
log(chalk.green(`Scanning for existing saves in ${chalk.greenBright.bold(sourceDir)}`))
const dir = await opendir(sourceDir);
async function* findExistingSaves(sourceDir, sourceRegexpString) {
const sourceRe = new RegExp(`^${sourceRegexpString}\$`)
log(chalk.green(`Scanning for existing saves with filenames like ${chalk.greenBright.bold(`/${sourceRegexpString}/`)} in ${chalk.greenBright.bold(sourceDir)}`))
const dir = await opendir(sourceDir)
for await (const dirent of dir) {
if (dirent.isFile() && sourceRe.test(dirent.name)) {
yield dirent.name
@ -134,70 +323,113 @@ async function* findExistingSaves(sourceDir) {
log(chalk.cyan(`Finished scanning for existing saves`))
}
async function promptForFilename() {
const saveType = await new Select({
async function promptForFilename({
times,
voreTypes,
voreRoles,
voreResults,
characterSceneTypes,
otherSceneTypes,
characters
}) {
const saveType = await new AutoComplete({
message: "What type of save is this?",
choices: ["Useful Save", "Vore Scene", "Skip This Save"],
limit: 10,
choices: ["Useful Save", "Vore Scene", ...(Array.isArray(characterSceneTypes) && characterSceneTypes.length > 0 ? characterSceneTypes.map((item) => `${item} (Character Scene)`) : []), ...(Array.isArray(otherSceneTypes) && otherSceneTypes.length > 0 ? otherSceneTypes.map((item) => `${item} (Other Scene)`) : []), "Skip This Save"],
}).run()
const dayPrompt = new Select({
const timePrompt = times.length > 1 ? new AutoComplete({
message: "When is this save from?",
choices: ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6"]
})
limit: 10,
choices: times
}) : {
async run() {
return times[0]
}
}
if (saveType === "Vore Scene") {
const voreType = await new Select({
const voreType = voreTypes.length > 1 ? await new AutoComplete({
message: "What type of vore is involved in this save?",
choices: ["Oral", "Anal", "Unbirth"]
}).run()
const voreRole = await new Select({
limit: 10,
choices: voreTypes
}).run() : voreTypes[0]
const voreRole = voreRoles.length > 1 ? await new AutoComplete({
message: "What role does the player play in this scene?",
choices: ["Predator", "Prey", "Observer"]
}).run()
const voreResult = await new Select({
limit: 10,
choices: voreRoles
}).run() : voreRoles[0]
const voreResult = voreResults.length > 1 ? await new AutoComplete({
message: "What is the result of the vore?",
choices: ["Fatal", "Always Fatal", "Non-fatal"],
}).run()
const day = await dayPrompt.run()
const vorePartner = await new Input({
limit: 10,
choices: voreResults,
}).run() : voreResults[0]
const time = await timePrompt.run()
const vorePartners = Array.isArray(characters) && characters.length > 0 ? await new AutoComplete({
message: "Who is/are the partner(s) the player is with in this scene?",
}).run()
limit: 10,
choices: characters,
multiple: true,
}).run() : []
const voreVariant = await new Input({
message: "What is the variant name/comment for this scene? (leave blank to skip)",
message: `What is the variant name/comment for this scene?${vorePartners.length > 0 ? " (leave blank to skip)" : ""}`,
validate: (filename) => validateFilenamePart(filename, {allowEmpty: vorePartners.length > 0})
}).run()
return `${voreType} ${voreRole} - ${voreResult} - ${day} ${vorePartner}${voreVariant ? ` (${voreVariant})` : ""}.save`
return `${voreType} ${voreRole} - ${voreResult} - ${time} ${vorePartners.join(" + ")}${voreVariant ? vorePartners.length > 0 ? ` (${voreVariant})` : voreVariant : ""}.save`
} else if (saveType === "Useful Save") {
const day = await dayPrompt.run()
const time = await timePrompt.run()
const title = await new Input({
message: "What is the title of this useful save?",
validate: validateFilenamePart
}).run()
return `Useful Saves - ${time} - ${title}.save`
} else if (saveType.endsWith(" (Character Scene)")) {
const sceneType = saveType.substring(0, saveType.length - " (Character Scene)".length)
const time = await timePrompt.run()
const scenePartners = Array.isArray(characters) && characters.length > 0 ? await new AutoComplete({
message: "Who is/are the partner(s) the player is with in this scene?",
limit: 10,
choices: characters,
multiple: true,
validate: (list) => list.length > 0,
}).run() : []
const title = await new Input({
message: `What is the comment for this ${sceneType} scene?`,
validate: validateFilenamePart
}).run()
return `${sceneType} with ${scenePartners.join("+")} - ${time} - ${title}.save`
} else if (saveType.endsWith(" (Other Scene)")) {
const sceneType = saveType.substring(0, saveType.length - " (Other Scene)".length)
const time = await timePrompt.run()
const title = await new Input({
message: "What is the title of this useful save?"
message: `What is the comment for this ${sceneType} scene?`,
validate: validateFilenamePart
}).run()
return `Useful Saves - ${day} - ${title}.save`
return `${sceneType} - ${time} - ${title}.save`
} else {
return null
}
}
async function shouldDoRename(destPath) {
try {
await access(destPath)
async function askIfOverwrite(destPath) {
console.log(chalk.yellow(`A file named ${chalk.yellowBright.bold(destPath)} already exists.`))
} catch (ex) {
if (ex.code === "ENOENT") {
return true
} else {
console.log(chalk.red(`The file named ${chalk.redBright.bold(destPath)} could not be accessed: ${chalk.redBright.bold(ex)}`))
}
}
const result = await new Select({message: "What should we do?", choices: ["Skip it", "Rename it", "Overwrite it"]}).run();
const result = await new Select({
message: "What should we do?",
choices: ["Skip it", "Rename it", "Overwrite it"]
}).run()
switch (result) {
case "Skip it":
return false;
return false
case "Overwrite it":
return true;
return true
case "Rename it":
return (await new Input({message: "What would you like to name the file?", initial: basename(destPath, ".save")}).run()) + ".save"
return (await new Input({
message: "What would you like to name the file?",
initial: basename(destPath, ".save"),
validate: (name) => name.length > 0 && isValidFilename(name) ? true : "must be a nonempty string not ending in trailing periods or containing any of the characters <>:\"/\\|?*"
}).run()) + ".save"
}
}
function queueMovingTo(destDir) {
function queueMovingTo(destDir, filenameData) {
let resolvePromise, rejectPromise
const completionPromise = new Promise((resolve, reject) => {
resolvePromise = resolve
@ -205,6 +437,7 @@ function queueMovingTo(destDir) {
})
const filequeue = []
let processingFiles = false
let endAfterCompleting = false
async function processFiles() {
if (processingFiles) {
@ -223,37 +456,52 @@ function queueMovingTo(destDir) {
continue
}
log(chalk.green(`For save ${chalk.greenBright.bold(nextFile)}:`))
const destFilename = await promptForFilename()
const destFilename = await promptForFilename(filenameData)
if (!destFilename) {
log(chalk.yellow("Skipped."))
filequeue.shift()
continue
}
let destPath = join(destDir, destFilename)
let confirmed = false;
while (!confirmed) {
const promptResult = await shouldDoRename(destPath)
let overwrite = false
while (true) {
try {
await moveFile(nextFile, destPath, {overwrite})
log(chalk.cyan(`Moved to ${chalk.cyanBright.bold(destPath)}`))
break
} catch (ex) {
if (ex.message.startsWith("The destination file exists:")) {
const promptResult = await askIfOverwrite(destPath)
if (typeof promptResult === "string") {
destPath = join(destDir, promptResult)
} else if (!promptResult) {
destPath = null
confirmed = true
} else if (promptResult === true) {
overwrite = true
} else {
confirmed = true
break
}
} else {
log(chalk.red(`Something went wrong: ${chalk.redBright.bold(ex)}`))
const giveUp = await new Toggle({
message: "Want to keep trying?",
disabled: "Yes, try again",
enabled: "No, skip this save"
}).run()
if (giveUp) {
break
}
}
}
if (!destPath) {
log(chalk.yellow("Skipped."))
filequeue.shift()
continue
}
await rename(nextFile, destPath)
log(chalk.cyan(`Moved to ${chalk.cyanBright.bold(destPath)}`))
filequeue.shift()
}
processingFiles = false
if (endAfterCompleting) {
resolvePromise()
}
}
log(chalk.green(`Ready to move renamed saves to ${chalk.greenBright.bold(destDir)}`))
return {
push(...files) {
if (!rejectPromise) {
@ -273,19 +521,34 @@ function queueMovingTo(destDir) {
rejectPromise = null
})
},
shutDown() {
if (!resolvePromise) {
return
}
if (processingFiles) {
endAfterCompleting = true
} else {
resolvePromise()
}
},
completionPromise
}
}
async function main() {
const {config} = envPaths("SMVA-Indexer")
export async function main() {
log(chalk.green(`${packageData.name} ${chalk.greenBright.bold(`v${packageData.version}`)} - built at ${chalk.greenBright.bold(new Date(buildTimestamp).toISOString())}`))
const {config} = envPaths(configName)
const home = homeDir()
const {sourceDir, destDir} = await loadPaths(config, home)
const queue = queueMovingTo(destDir)
const {
sourceDir,
destDir,
data: {fileRegExp, ...filenameData}
} = await loadPaths(config, home)
const queue = queueMovingTo(destDir, filenameData)
const existingSavesPromise = (async () => {
try {
for await (const oldSave of findExistingSaves(sourceDir)) {
for await (const oldSave of findExistingSaves(sourceDir, fileRegExp)) {
queue.push(join(sourceDir, oldSave))
}
} catch (ex) {
@ -294,17 +557,24 @@ async function main() {
})()
const newSavesPromise = (async () => {
for await (const newSave of watchNewSaves(sourceDir)) {
try {
for await (const newSave of watchNewSaves(sourceDir, fileRegExp)) {
queue.push(join(sourceDir, newSave))
}
} catch (ex) {
log(chalk.red(`failed waiting for new saves: ${chalk.redBright.bold(ex)}`))
}
})()
Promise.allSettled([newSavesPromise, existingSavesPromise]).finally(() => queue.shutDown())
try {
await Promise.all([newSavesPromise, existingSavesPromise, queue.completionPromise])
await queue.completionPromise
} catch (err) {
console.log(chalk.redBright.bold(err));
process.exit(1);
console.log(chalk.redBright.bold(err))
process.exit(1)
}
process.exit(0);
process.exit(0)
}
main()
main.packageName = packageData.name
main.version = packageData.version
main.buildTimestamp = packageData.buildTimestamp

9088
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,19 +1,26 @@
{
"name": "smva-indexer",
"version": "1.0.1",
"version": "1.1.1",
"description": "Simple SMVA save file templating system.",
"bin": "indexer.mjs",
"scripts": {
"start": "node build/smva-indexer-bundle.js",
"build": "npm run build-rollup && npm run build-nexe",
"build-rollup": "rollup -c rollup.config.js -f commonjs indexer.mjs",
"build-nexe": "concurrently -i -n Win,Mac,Lin -c blue,white,yellow \"npm run build-win-nexe\" \"npm run build-mac-nexe\" \"npm run build-lin-nexe\"",
"build": "concurrently -n Rollup,Data,Signer -s all -c green,yellow,red \"npm run build-rollup\" \"npm run build-data\" \"npm run build-signer\" && npm run build-nexe && npm run sign",
"build-signed": "concurrently -n Rollup,Signer -c green,red \"npm run build-rollup\" \"npm run build-signer\" && npm run sign",
"build-rollup": "rollup -c rollup.config.js",
"build-signer": "rollup -c signer-rollup.config.js",
"build-data": "cpy default-data.json smva-indexer-release.pub.pem build && move-file build/default-data.json build/smva-indexer-game-data.json",
"build-nexe": "concurrently -i -s all -n Win,Mac,Lin -c blue,white,yellow \"npm run build-win-nexe\" \"npm run build-mac-nexe\" \"npm run build-lin-nexe\"",
"build-win-nexe": "nexe -t windows-x86-14.15.3 -i build/smva-indexer-bundle.js -n indexer -o build/smva-indexer-win.exe --verbose",
"build-mac-nexe": "nexe -t mac-x64-14.15.3 -i build/smva-indexer-bundle.js -n indexer -o build/smva-indexer-macos --verbose",
"build-lin-nexe": "nexe -t linux-x64-14.15.3 -i build/smva-indexer-bundle.js -n indexer -o build/smva-indexer-linux --verbose",
"build-win": "npm run build-rollup && npm run build-win-nexe",
"build-mac": "npm run build-rollup && npm run build-mac-nexe",
"build-lin": "npm run build-rollup && npm run build-lin-nexe"
"build-lin": "npm run build-rollup && npm run build-lin-nexe",
"sign": "NODE_PATH=node_modules node build/sign-bundle.js build/smva-indexer-bundle.js build/smva-indexer-bundle-signed.js smva-indexer-release.key.pem",
"publish": "sftp -b publish-commands.sftp kink-games.deliciousreya.net",
"release": "npm run build && npm run publish",
"generate-release-keys": "openssl genpkey -genparam -algorithm dsa -pkeyopt dsa_paramgen_bits:2048 -outform PEM --out smva-indexer-release.params.pem && openssl genpkey -paramfile smva-indexer-release.params.pem -aes-256-cbc -out smva-indexer-release.key.pem && openssl pkey -inform PEM -in smva-indexer-release.key.pem -outform PEM -pubout -out smva-indexer-release.pub.pem"
},
"keywords": [],
"author": {
@ -23,19 +30,28 @@
"license": "MIT",
"dependencies": {
"chalk": "^5.0.0",
"data-urls": "^3.0.1",
"enquirer": "^2.3.6",
"env-paths": "^3.0.0",
"filenamify": "^5.1.0",
"home-dir": "^1.0.0",
"ini": "^2.0.0",
"make-dir": "^3.1.0",
"source-map-support": "^0.5.21"
"move-file": "^3.0.0",
"source-map-support": "^0.5.21",
"valid-filename": "^4.0.0"
},
"devDependencies": {
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-url": "^6.1.0",
"concurrently": "^7.0.0",
"cpy-cli": "^3.1.1",
"move-file-cli": "^3.0.0",
"nexe": "^4.0.0-beta.19",
"rollup": "^2.67.2"
"rollup": "^2.67.2",
"rollup-plugin-consts": "^1.0.2"
}
}

@ -0,0 +1,8 @@
chdir "Shadows/Games/Web Games/Saint Miluina's Vore Academy/indexer"
lchdir build
put smva-indexer-win.exe
put smva-indexer-macos
put smva-indexer-linux
put smva-indexer-bundle-signed.js
put smva-indexer-game-data.json
put smva-indexer-release.pub.pem

@ -1,23 +1,45 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import alias from '@rollup/plugin-alias';
import consts from 'rollup-plugin-consts';
import url from '@rollup/plugin-url';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import {signatureGuardStart, signatureGuardEnd} from "./signing-common.mjs"
export default {
input: 'indexer.js',
input: 'updateable-launcher.mjs',
output: {
format: 'cjs',
file: 'build/smva-indexer-bundle.js',
sourcemap: 'inline',
banner: Buffer.from(signatureGuardStart).toString("utf-8") + "(Unsigned)" + Buffer.from(signatureGuardEnd).toString("utf-8")
},
external: ['fs/promises', 'fs', 'path', 'process', 'os', 'tty'],
plugins: [nodeResolve({
exportConditions: ["node"],
}), commonjs(), alias({
plugins: [
alias({
entries: [
{ find: 'node:path', replacement: 'path' },
{ find: 'node:process', replacement: 'process' },
{ find: 'node:os', replacement: 'os' },
{ find: 'node:tty', replacement: 'tty' },
{ find: 'node:fs', replacement: 'fs' },
]
})]
}),
consts({
buildTimestamp: new Date().getTime(),
configName: "SMVA-Indexer"
}),
url({
limit: 4096,
include: ["./smva-indexer-release.pub.pem"],
emitFiles: false,
}),
commonjs(),
json({
compact: true,
}),
nodeResolve({
exportConditions: ["node"],
preferBuiltins: true,
}),]
};

@ -0,0 +1,62 @@
import {readFile, open} from "fs/promises"
import enquirer from "enquirer"
import {createPrivateKey, createSign} from "crypto"
import chalk from "chalk"
import {algorithm, signatureGuardEnd, signatureGuardStart} from "./signing-common.mjs"
const {Invisible} = enquirer
async function getPassword() {
return await new Invisible({
message: "Enter the passphrase for smva-indexer-release.key.pem:"
}).run()
}
async function decryptSecretKey(keyPath) {
return createPrivateKey({ key: await readFile(keyPath), passphrase: await getPassword() })
}
async function signBundle(bundlePath, destinationPath, keyPath) {
const bundle = await readFile(bundlePath)
if (!bundle.subarray(0, signatureGuardStart.length).equals(signatureGuardStart)) {
throw new Error("No placeholder for the signature guard start found")
}
const signatureLength = bundle.subarray(signatureGuardStart.length).indexOf(signatureGuardEnd)
if (signatureLength === -1) {
throw new Error("No placeholder for the signature guard end found")
}
const sign = createSign(algorithm)
const signedContent = bundle.subarray(signatureGuardStart.length + signatureLength + signatureGuardEnd.length)
const secretKey = await decryptSecretKey(keyPath)
sign.update(signedContent)
const signature = sign.sign(secretKey, 'base64')
let destination
try {
destination = await open(destinationPath, "w")
await destination.write(signatureGuardStart)
await destination.write(signature, null, "utf-8")
await destination.write(signatureGuardEnd)
await destination.write(signedContent)
} finally {
await destination?.close()
}
}
async function main() {
const [bundlePath, destinationPath, keyPath] = process.argv.slice(2)
if (!bundlePath || !destinationPath || !keyPath) {
process.stderr.write(chalk.red("Required arguments: bundlePath destinationPath keyPath\n"))
process.exit(1)
}
try {
await signBundle(bundlePath, destinationPath, keyPath)
process.stderr.write(chalk.cyan(`Successfully signed ${chalk.cyanBright.bold(bundlePath)} as ${chalk.cyanBright.bold(destinationPath)} with ${chalk.cyanBright.bold(keyPath)}\n`))
} catch (ex) {
process.stderr.write(chalk.red(`Failed to sign ${chalk.redBright.bold(bundlePath)} as ${chalk.redBright.bold(destinationPath)} with ${chalk.redBright.bold(keyPath)}: ${chalk.redBright.bold(ex)}\n`))
}
}
if (!global.main) {
global.main = true
main()
}

@ -0,0 +1,32 @@
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
export default {
input: 'sign-bundle.mjs',
output: {
format: 'cjs',
file: 'build/sign-bundle.js',
sourcemap: 'inline',
},
external: ['fs/promises', 'fs', 'path', 'process', 'os', 'tty', 'minisign'],
plugins: [
alias({
entries: [
{ find: 'node:path', replacement: 'path' },
{ find: 'node:process', replacement: 'process' },
{ find: 'node:os', replacement: 'os' },
{ find: 'node:tty', replacement: 'tty' },
{ find: 'node:fs', replacement: 'fs' },
]
}),
commonjs(),
json({
compact: true,
}),
nodeResolve({
exportConditions: ["node"],
preferBuiltins: true,
}),]
};

@ -0,0 +1,16 @@
import {isAbsolute, join} from "path"
export function absolutize(path) {
if (isAbsolute(path)) {
return path
} else {
return join(process.cwd(), path)
}
}
const encoder = new TextEncoder()
export const algorithm = "SHA3-512"
export const signatureGuardStart = encoder.encode("//// signature: <")
export const signatureGuardEnd = encoder.encode("> ////\n\n")

@ -0,0 +1,20 @@
-----BEGIN PUBLIC KEY-----
MIIDQjCCAjUGByqGSM44BAEwggIoAoIBAQCtdpbx5B26JDovYLaz8xTDDgnUk6kr
b6tBYIrYuAuqigNTvvUeg7UOGMQuZcrqiPgwEtrnKdVhV1WIY4wMYfM80cSeNuH5
ySHIS/JzMXls2704oeJeQtULIhxIvCLtaoQzHEAZrU3M+EBXnBviaztmWJQnlETh
z9CXWiRV1WlNWKo3xh7p18hw8NtpBMR7m0h6el9BVEXl+vXcmr+nzUIxAcVnurC6
x4AKoUDhZ1ZBux+FagKjP10/Xgas9q0WHA9FMhQlWoj2G7Yki1+ab7SxXgNF2QWg
qmk6paq81vexML4ZuWkFEsNWUgUthEBXtdiYrJCkTfrMxMPyOGCJ8VrdAh0ApeOr
1X5AZOvw9rqzu7VuLbLstInrW87e98nQAQKCAQA6wpsWw6vR6bjTCqEP8veH71Co
rghx/wQV4SJnZYxHF056LojxYfJd9XOjcr2Wi7o/EaZ1aeuQU5BlV4W98AUgWS0r
Ex8KEWuQ1jcbxF2E6APFmK2MVcFfisNpeNF6srkIQwwYyZZwkQcdZ0VOfknYGm6t
0lF98pl4eiPc5o9O+x+JyRiAKKo0RdzgIs0VLX/C48JWZBu7AD54Rn6hW3xiNn2n
oJQUkVuQTdDcrm69wtxpAGlteVmrHeS8MsijW35JyH7/t4K7PGgreEQTjFI7c9eR
RnAzM8aw0x4yzoKPWd2pi26XZtqRnw41l2nrPIjE0qicRFE+2MQgeAggDdqxA4IB
BQACggEAcwfo6hAcV2mzSAETAz9qveBu3emtUjd78nbdfDowkJhzzuCUiBwvZGoe
V8NkNOMFYmQbTtledb5v6llVtbKYS3y30rsj8g4HCxqB9zmIPMnEW8ZhtE4LjKXa
7oMUMB6pJWOWIwyyAT+BFNqJ7ObbB1amXhPlcJe1eSg1NN26bQ4jqiEzg3kCbAOC
hWR0hZinopAekCwYzseR7zhU2PimXvcdYhg3J1nmuOpPeFfUqKM7qzsRLZny3eOz
igTFEbo0XDx09Hfx6eLEEi4eAt6UOT6rozdJvqba+fjQCT3xWc0FzI/Mq2i/LQqq
DXT6eKt+fG92xURh3KPJbt2E9H/T3Q==
-----END PUBLIC KEY-----

@ -0,0 +1,84 @@
import {main as bakedInMain} from './indexer.mjs'
import {readFile} from 'fs/promises'
import {join} from "path"
import chalk from "chalk"
import parseDataUrl from "data-urls"
import publicKeyDataUrl from "./smva-indexer-release.pub.pem"
import envPaths from "env-paths"
import configName from "consts:configName"
import {signatureGuardStart, signatureGuardEnd, algorithm, absolutize} from "./signing-common.mjs"
import {createPublicKey, createVerify} from "crypto"
const publicKey = createPublicKey({
key: parseDataUrl(publicKeyDataUrl).body,
format: "pem"
})
async function validateSignature(input) {
if (!input.subarray(0, signatureGuardStart.length).equals(signatureGuardStart)) {
throw new Error(`File does not start with ${JSON.stringify(signatureGuardStart.toString())}`)
}
const signatureLength = input.subarray(signatureGuardStart.length).indexOf(signatureGuardEnd)
if (signatureLength === -1) {
throw new Error(`Signature does not end with ${JSON.stringify(signatureGuardEnd.toString())}`)
}
const verify = createVerify(algorithm)
const signedContent = input.subarray(signatureGuardStart.length + signatureLength + signatureGuardEnd.length)
verify.update(signedContent)
const signature = input.subarray(signatureGuardStart.length, signatureGuardStart.length + signatureLength).toString("utf-8")
if (!verify.verify(publicKey, signature, "base64")) {
throw new Error(`Signature was incorrect`)
}
}
async function tryFiles(paths) {
for (const path of paths) {
try {
const input = await readFile(path)
if (!process.env.SMVA_INDEXER_IGNORE_SIGNATURES) {
await validateSignature(input)
}
const {main: result} = await import(absolutize(path))
if (result !== bakedInMain) {
process.stderr.write(chalk.cyan(`Loaded code for ${chalk.cyanBright.bold(`v${result.version}`)} from ${chalk.cyanBright.bold(path)}\n`))
return result
}
} catch (ex) {
if (ex.code === "ENOENT" || ex.code === "EACCES") {
// That's okay, just move on to the next file
} else {
process.stderr.write(chalk.red(`Error while checking for an updated bundle at ${chalk.redBright.bold(path)}: ${chalk.redBright.bold(ex)}\n`))
}
}
}
// None of the files both existed AND were valid.
return null
}
const bundleFilename = "smva-indexer-bundle-signed.js"
async function start() {
const bundlesToTry = []
const cwdBundle = join(process.cwd(), bundleFilename)
if (!bundlesToTry.includes(cwdBundle) && !process.env.SMVA_INDEXER_IGNORE_CWD_BUNDLE) {
bundlesToTry.push(cwdBundle)
}
const besideBundle = join(module.path, bundleFilename)
if (!bundlesToTry.includes(besideBundle) && !process.env.SMVA_INDEXER_IGNORE_NEARBY_BUNDLE) {
bundlesToTry.push(besideBundle)
}
const configBundle = join(envPaths(configName).config, bundleFilename)
if (!bundlesToTry.includes(configBundle) && !process.env.SMVA_INDEXER_IGNORE_CONFIG_BUNDLE) {
bundlesToTry.push(configBundle)
}
const loadedMain = process.env.SMVA_INDEXER_IGNORE_ALL_BUNDLES ? null : await tryFiles(bundlesToTry);
await (loadedMain || bakedInMain)()
}
if (!global.main) {
global.main = true
start()
}
export const main = bakedInMain;
Loading…
Cancel
Save