Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions .github/workflows/store-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,28 @@ permissions:

jobs:
package:
name: Package ${{ matrix.productName }}
runs-on: windows-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- variant: prompterpro
productName: PrompterPro
identityName: ReleasedPtyLtd.PrompterPro
productId: ""
- variant: simpleprompt
productName: SimplePrompt
identityName: ReleasedPtyLtd.SimplePrompt
productId: 9MT1X5BNTHQS
env:
MS_STORE_PRODUCT_ID: ${{ vars.MS_STORE_PRODUCT_ID }}
MS_STORE_IDENTITY_NAME: ${{ vars.MS_STORE_IDENTITY_NAME }}
MS_STORE_PUBLISHER: ${{ vars.MS_STORE_PUBLISHER }}
MS_STORE_PUBLISHER_DISPLAY_NAME: ${{ vars.MS_STORE_PUBLISHER_DISPLAY_NAME }}
PROMPTER_VARIANT: ${{ matrix.variant }}
VITE_APP_VARIANT: ${{ matrix.variant }}
MS_STORE_PRODUCT_ID: ${{ matrix.productId || vars.MS_STORE_PRODUCT_ID }}
MS_STORE_IDENTITY_NAME: ${{ matrix.identityName }}
MS_STORE_PUBLISHER: CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897
MS_STORE_PUBLISHER_DISPLAY_NAME: Released Pty Ltd

steps:
- name: Check out repository
Expand Down Expand Up @@ -80,8 +95,8 @@ jobs:
- name: Upload MSIX bundle artifact
uses: actions/upload-artifact@v7
with:
name: PrompterPro-${{ env.MS_STORE_VERSION }}-x64-arm64
path: out/store/*.msixbundle
name: ${{ matrix.productName }}-${{ env.MS_STORE_VERSION }}-x64-arm64
path: out/store/${{ matrix.productName }}_*.msixbundle
if-no-files-found: error
compression-level: 0

Expand Down Expand Up @@ -129,6 +144,6 @@ jobs:
--sellerId $env:SELLER_ID `
--clientId $env:AZURE_AD_APPLICATION_CLIENT_ID `
--clientSecret $env:AZURE_AD_APPLICATION_SECRET
$package = Get-ChildItem "out/store/*.msixbundle" |
$package = Get-ChildItem "out/store/${{ matrix.productName }}_*.msixbundle" |
Select-Object -First 1
msstore publish $package.FullName -id $env:MS_STORE_PRODUCT_ID
41 changes: 33 additions & 8 deletions desktop/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ const MODEL_DIRECTORY_NAME =
"sherpa-onnx-streaming-zipformer-en-20M-2023-02-17";
const BACKGROUND_COLOR = "#0b0d0f";
const LEGACY_USER_DATA_DIRECTORY_NAME = "Prompter";
const executableName = path.basename(
process.execPath,
path.extname(process.execPath),
);
const desktopBrand =
executableName.toLowerCase() === "simpleprompt"
? {
name: "SimplePrompt",
appUserModelId: "ReleasedPtyLtd.SimplePrompt",
userDataDirectoryName: "SimplePrompt",
}
: {
name: "PrompterPro",
appUserModelId: "ReleasedPtyLtd.PrompterPro",
userDataDirectoryName: LEGACY_USER_DATA_DIRECTORY_NAME,
};

let mainWindow = null;
let localServer = null;
Expand All @@ -27,7 +43,7 @@ let readyToQuit = false;
// scripts, device preferences, and other local browser data survive upgrades.
app.setPath(
"userData",
path.join(app.getPath("appData"), LEGACY_USER_DATA_DIRECTORY_NAME),
path.join(app.getPath("appData"), desktopBrand.userDataDirectoryName),
);

function isLoopbackUrl(value) {
Expand Down Expand Up @@ -68,13 +84,17 @@ function configurePermissions() {
function configureScriptStorage() {
ipcMain.handle("prompter:scripts:load", (event) => {
if (!isAppUrl(event.senderFrame?.url)) {
throw new Error("Script storage is only available to PrompterPro.");
throw new Error(
`Script storage is only available to ${desktopBrand.name}.`,
);
}
return loadScriptStore(app.getPath("userData"));
});
ipcMain.handle("prompter:scripts:save", (event, scripts) => {
if (!isAppUrl(event.senderFrame?.url)) {
throw new Error("Script storage is only available to PrompterPro.");
throw new Error(
`Script storage is only available to ${desktopBrand.name}.`,
);
}
const saveOperation = scriptSaveQueue
.catch(() => undefined)
Expand All @@ -87,6 +107,7 @@ function configureScriptStorage() {
async function startLocalServer() {
process.env.PORT = "0";
process.env.PROMPTER_PRODUCTION = "1";
process.env.PROMPTER_PRODUCT_NAME = desktopBrand.name;
process.env.SHERPA_ONNX_MODEL_DIR = app.isPackaged
? path.join(process.resourcesPath, MODEL_DIRECTORY_NAME)
: path.join(app.getAppPath(), ".models", MODEL_DIRECTORY_NAME);
Expand All @@ -104,7 +125,9 @@ async function startLocalServer() {

const address = localServer.address();
if (!address || typeof address === "string") {
throw new Error("PrompterPro could not reserve a local server port.");
throw new Error(
`${desktopBrand.name} could not reserve a local server port.`,
);
}
appUrl = `http://127.0.0.1:${address.port}`;
}
Expand Down Expand Up @@ -169,10 +192,12 @@ async function startArm64Sidecar() {
}

async function createWindow() {
if (!appUrl) throw new Error("The local PrompterPro server is not ready.");
if (!appUrl) {
throw new Error(`The local ${desktopBrand.name} server is not ready.`);
}

mainWindow = new BrowserWindow({
title: "PrompterPro",
title: desktopBrand.name,
width: 1440,
height: 960,
minWidth: 1040,
Expand Down Expand Up @@ -215,7 +240,7 @@ if (!hasSingleInstanceLock) {

app.whenReady()
.then(async () => {
app.setAppUserModelId("ReleasedPtyLtd.PrompterPro");
app.setAppUserModelId(desktopBrand.appUserModelId);
configurePermissions();
await startLocalServer();
configureScriptStorage();
Expand All @@ -224,7 +249,7 @@ if (!hasSingleInstanceLock) {
.catch((error) => {
const message =
error instanceof Error ? error.message : "Unknown startup error";
dialog.showErrorBox("PrompterPro could not start", message);
dialog.showErrorBox(`${desktopBrand.name} could not start`, message);
app.quit();
});

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"main": "desktop/main.mjs",
"scripts": {
"dev": "concurrently -k -n API,WEB -c cyan,magenta \"tsx watch server/index.ts\" \"vite\"",
"site:render": "tsx site/render-static.tsx",
"site:dev": "npm run site:render && vite site",
"site:build": "npm run site:render && tsc -p site/tsconfig.json --pretty false && vite build site",
"build": "tsc -b && vite build",
"build:server": "node scripts/build-server.mjs",
"build:desktop": "npm run build && npm run build:server",
Expand Down
5 changes: 5 additions & 0 deletions public/simpleprompt-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 26 additions & 12 deletions scripts/package-windows.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
DEFAULT_STORE_IDENTITY,
renderStoreManifest,
} from "./store-manifest.mjs";
import { getProductVariant } from "./product-variants.mjs";

const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const rootDirectory = path.resolve(scriptDirectory, "..");
Expand All @@ -34,6 +35,13 @@ const outputDirectory = process.env.PROMPTER_PACKAGE_OUTPUT_DIR
const packageJson = JSON.parse(
await readFile(path.join(rootDirectory, "package.json"), "utf8"),
);
const productVariant = getProductVariant(
process.env.PROMPTER_VARIANT || process.env.VITE_APP_VARIANT,
);
const productAssetsDirectory = path.join(
storeDirectory,
productVariant.assetDirectory,
);
const allowedAppEntries = new Set([
"desktop",
"dist",
Expand Down Expand Up @@ -121,25 +129,25 @@ for (const architecture of architectures) {
console.log(`Packaging the ${architecture.name} Electron application...`);
const packagePaths = await packager({
dir: rootDirectory,
name: packageJson.productName,
executableName: packageJson.productName,
name: productVariant.productName,
executableName: productVariant.productName,
Comment thread
nickbeau marked this conversation as resolved.
platform: "win32",
arch: architecture.name,
out: outputDirectory,
overwrite: true,
prune: true,
asar: architecture.asar,
icon: path.join(storeDirectory, "assets", "AppIcon.ico"),
icon: path.join(productAssetsDirectory, "AppIcon.ico"),
extraResource: [modelDirectory],
afterPrune: [removeUnneededRuntimeModules],
appVersion: packageJson.version,
buildVersion: packageJson.version,
win32metadata: {
CompanyName: packageJson.author,
FileDescription: packageJson.description,
ProductName: packageJson.productName,
InternalName: packageJson.productName,
OriginalFilename: `${packageJson.productName}.exe`,
FileDescription: productVariant.description,
ProductName: productVariant.productName,
InternalName: productVariant.productName,
OriginalFilename: `${productVariant.productName}.exe`,
"requested-execution-level": "asInvoker",
},
ignore: shouldIgnorePackageFile,
Expand All @@ -153,15 +161,15 @@ for (const architecture of architectures) {

const packageDirectory = packagePaths[0];
await assertPeArchitecture(
path.join(packageDirectory, `${packageJson.productName}.exe`),
path.join(packageDirectory, `${productVariant.productName}.exe`),
architecture.name,
);
await assertPackagedAppAllowList(
packageDirectory,
architecture.name,
);
await cp(
path.join(storeDirectory, "assets"),
productAssetsDirectory,
path.join(packageDirectory, "Assets"),
{ recursive: true },
);
Expand All @@ -178,7 +186,7 @@ const msixOutputDirectory = path.join(outputDirectory, "store");
await mkdir(msixOutputDirectory, { recursive: true });
const bundlePath = path.join(
msixOutputDirectory,
`${packageJson.productName}_${toSafeVersion(version)}_x64_arm64.msixbundle`,
`${productVariant.productName}_${toSafeVersion(version)}_x64_arm64.msixbundle`,
);
const winAppCli = path.join(
rootDirectory,
Expand Down Expand Up @@ -358,15 +366,21 @@ async function addStoreManifest(packageDirectory, architecture) {
architecture: architecture.name,
identityName:
process.env.MS_STORE_IDENTITY_NAME ||
DEFAULT_STORE_IDENTITY.identityName,
productVariant.store.identityName || DEFAULT_STORE_IDENTITY.identityName,
minimumWindowsVersion: architecture.minimumWindowsVersion,
publisher:
process.env.MS_STORE_PUBLISHER ||
DEFAULT_STORE_IDENTITY.publisher,
productVariant.store.publisher || DEFAULT_STORE_IDENTITY.publisher,
publisherDisplayName:
process.env.MS_STORE_PUBLISHER_DISPLAY_NAME ||
productVariant.store.publisherDisplayName ||
DEFAULT_STORE_IDENTITY.publisherDisplayName,
version: process.env.MS_STORE_VERSION || packageJson.version,
displayName: productVariant.productName,
executableName: productVariant.productName,
applicationId: productVariant.applicationId,
description: productVariant.description,
backgroundColor: productVariant.backgroundColor,
});
await writeFile(
path.join(packageDirectory, "Package.appxmanifest"),
Expand Down
22 changes: 22 additions & 0 deletions scripts/product-variants.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface ProductVariant {
readonly key: "prompterpro" | "simpleprompt";
readonly productName: "PrompterPro" | "SimplePrompt";
readonly assetDirectory: string;
readonly applicationId: string;
readonly description: string;
readonly backgroundColor: string;
readonly store: Readonly<{
identityName: string;
publisher: string;
publisherDisplayName: string;
productId: string;
packageFamilyName?: string;
packageSid?: string;
}>;
}

export const PRODUCT_VARIANTS: Readonly<
Record<"prompterpro" | "simpleprompt", ProductVariant>
>;

export function getProductVariant(value?: string): ProductVariant;
49 changes: 49 additions & 0 deletions scripts/product-variants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const sharedPublisher = Object.freeze({
publisher: "CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897",
publisherDisplayName: "Released Pty Ltd",
});

export const PRODUCT_VARIANTS = Object.freeze({
prompterpro: Object.freeze({
key: "prompterpro",
productName: "PrompterPro",
assetDirectory: "assets",
applicationId: "Prompter",
description:
"An offline, voice-following teleprompter for polished video recording.",
backgroundColor: "#0b0d0f",
store: Object.freeze({
...sharedPublisher,
identityName: "ReleasedPtyLtd.PrompterPro",
productId: "",
}),
}),
simpleprompt: Object.freeze({
key: "simpleprompt",
productName: "SimplePrompt",
assetDirectory: "simpleprompt/assets",
applicationId: "SimplePrompt",
description:
"A private, voice-following teleprompter for clear video recording.",
backgroundColor: "#0b0d0f",
store: Object.freeze({
...sharedPublisher,
identityName: "ReleasedPtyLtd.SimplePrompt",
productId: "9MT1X5BNTHQS",
packageFamilyName: "ReleasedPtyLtd.SimplePrompt_q0b077qanz1d8",
packageSid:
"S-1-15-2-2676728462-2596204801-2700890954-455437082-720273692-286854053-1939976512",
}),
}),
});

export function getProductVariant(value = "prompterpro") {
const key = value.trim().toLowerCase();
const variant = PRODUCT_VARIANTS[key];
if (!variant) {
throw new Error(
`Unknown product variant "${value}". Expected prompterpro or simpleprompt.`,
);
}
return variant;
}
7 changes: 6 additions & 1 deletion scripts/smoke-arm64-sidecar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import WebSocket from "ws";
import { getProductVariant } from "./product-variants.mjs";

const require = createRequire(import.meta.url);
const sherpa = require("sherpa-onnx-node");
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const rootDirectory = path.resolve(scriptDirectory, "..");
const productVariant = getProductVariant(
process.env.PROMPTER_VARIANT || process.env.VITE_APP_VARIANT,
);
const packageOutputDirectory = process.env.PROMPTER_PACKAGE_OUTPUT_DIR
? path.resolve(rootDirectory, process.env.PROMPTER_PACKAGE_OUTPUT_DIR)
: path.join(rootDirectory, "out");
const arm64Directory = path.join(
packageOutputDirectory,
"PrompterPro-win32-arm64",
`${productVariant.productName}-win32-arm64`,
);
const nodeExecutable = path.join(
arm64Directory,
Expand Down Expand Up @@ -44,6 +48,7 @@ const sidecar = spawn(nodeExecutable, [serverEntry], {
...process.env,
PORT: "0",
PROMPTER_PRODUCTION: "1",
PROMPTER_PRODUCT_NAME: productVariant.productName,
SHERPA_ONNX_MODEL_DIR: modelDirectory,
},
stdio: ["ignore", "pipe", "pipe", "ipc"],
Expand Down
Loading