Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
717c5c1
Add RomM install script
pshickeydev Aug 15, 2026
809052e
converted credential questions to password type with random-string de…
eschultz Aug 18, 2026
1de96a1
bumped 7-char random secrets to 16 (passwords) and 32 (signing keys)
eschultz Aug 18, 2026
797cb40
converted gb-multiple storage specs to {n}gb and documented spec grammar
eschultz Aug 18, 2026
2a27a05
raised palworld requirements to 4CORE/30GB/16GBRAM
eschultz Aug 18, 2026
acd06a5
✨ Flagged first-boot-only admin credential questions installonly (sna…
mill3000 Aug 17, 2026
a0b4fd6
Merge branch 'staging' into new-app-romm
pshickeydev Aug 21, 2026
ad6ef3e
Correct the declared requirements on handbrake, paperless-ngx, and wa…
eschultz Aug 26, 2026
c9a93b6
Merge pull request #49 from eshtek/fix/catalog-validation-errors
eschultz Aug 26, 2026
497238c
Validate install scripts on every pull request
eschultz Aug 26, 2026
52a92fb
Merge pull request #50 from eshtek/ci/validate-install-scripts
eschultz Aug 26, 2026
469e627
Add install script v6 support to the validator
jerodfritz Sep 1, 2026
73b5416
Merge pull request #51 from eshtek/feat/install-script-v6
jerodfritz Sep 1, 2026
abadbd7
Add widget_context type mirror missed from the v6 scaffolding PR
jerodfritz Sep 1, 2026
6068328
Mirror the backend's v6 superRefine rules in the validator
jerodfritz Sep 1, 2026
a2553e7
Merge pull request #52 from eshtek/feat/install-script-v6-widget-context
jerodfritz Sep 1, 2026
feabab6
Add FileFlows v6 install script
jerodfritz Sep 1, 2026
491c44d
Rework the conversions widget: frame-based progress aligned with the …
JoshuaLMejia Sep 1, 2026
cf1d805
Add Scrutiny disk-health widget on a v6 install script
JoshuaLMejia Sep 1, 2026
32d3fac
Add Plex now-playing widget on a v6 install script
JoshuaLMejia Sep 1, 2026
62e2d18
Merge pull request #45 from eshtek/feature/password-question-generate…
jerodfritz Sep 1, 2026
33f132c
Merge pull request #46 from eshtek/refactor/gb-storage-specs
jerodfritz Sep 1, 2026
cb94309
Merge staging into feature/install-only-question-flag
jerodfritz Sep 1, 2026
c023c4a
Merge pull request #47 from eshtek/feature/install-only-question-flag
jerodfritz Sep 1, 2026
b5a2baf
Merge pull request #53 from eshtek/feat/fileflows
jerodfritz Sep 1, 2026
0655423
Merge pull request #54 from eshtek/feat/scrutiny-widget
jerodfritz Sep 1, 2026
5e3953c
Merge pull request #55 from eshtek/feat/plex-now-playing-widget
jerodfritz Sep 1, 2026
81e1836
Update romm.json
MobiusGT Sep 2, 2026
6dbff6d
added donetick in prep for release
MobiusGT Sep 2, 2026
0914fac
Update donetick.json
MobiusGT Sep 2, 2026
f849822
Merge pull request #43 from pshickeydev/new-app-romm
MobiusGT Sep 2, 2026
718a7b4
update question default
MobiusGT Sep 10, 2026
683e46a
Merge pull request #60 from acknologia/update-to-romm-staging
MobiusGT Sep 10, 2026
17b9ef4
Move the hexos-test harness apps to v6 with their actions and widgets
eschultz Oct 4, 2026
9098073
Merge pull request #65 from eshtek/feature/hexos-test-v6-harness
eschultz Oct 4, 2026
46beb5a
Widgets on widgetsSchema 3, the app's card, and a now-playing widget …
AverageCakeSlice Oct 9, 2026
19dc45f
Remove FileFlows app integration (#72)
jerodfritz Oct 9, 2026
a02e342
Merge branch 'prod' into staging
jerodfritz Oct 9, 2026
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
32 changes: 32 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Install scripts are executed on users' machines, and a script that is merely
# valid JSON can still abort an install — an unknown $LOCATION throws, a
# $QUESTION with no question behind it resolves to nothing, an icon path that
# is not in the repo 404s in the catalog UI. This runs on GitHub-hosted runners
# with no secrets, so it is safe on pull_request from forks.
name: Validate install scripts

on:
pull_request:
push:
branches: [staging, prod]

jobs:
validate:
runs-on: ubuntu-latest
# Seconds of work; anything approaching this is a hang, not a slow run.
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- name: Install tooling dependencies
run: bun install
working-directory: _lib
# Typecheck first, deliberately: bun executes TypeScript by stripping
# types, so a type error in _lib never stops the validator from running —
# it just makes its verdict untrustworthy. Checking the checker before
# believing it keeps a broken validator from reporting a green catalog.
- name: Typecheck the tooling
run: bun run typecheck
working-directory: _lib
- name: Validate install scripts
run: bun _lib/validate.ts
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__pycache__/
*.pyc
*.DS_Store
*.DS_Store
node_modules/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ Install scripts are JSON configurations that automate app deployment on HexOS

For schema documentation, macros reference, and contribution guidelines, see [docs.hexos.com](https://docs.hexos.com).

## Requirement specifications

`requirements.specifications` values are parametric — any sensible number works without a platform change. The accepted patterns:

| Pattern | Meaning | Examples |
|---------|---------|----------|
| `{n}CORE` | Minimum CPU cores | `2CORE`, `4CORE`, `6CORE` |
| `{n}MB` / `{n}GB` | Approximate storage needed for installation | `256MB`, `1GB`, `30GB` |
| `{n}MBRAM` / `{n}GBRAM` | Minimum memory | `512MBRAM`, `8GBRAM` |
| `GPU` | GPU recommended | `GPU` |

Every value must match `^(\d+)(MBRAM|GBRAM|MB|GB|CORE)$` or be the literal `GPU` — anything else fails the catalog sync for that app. Use the largest natural unit (`1GB`, not `1024MB`; `8GBRAM`, not `8192MBRAM`) so the rendered text reads cleanly.

```json
"requirements": {
"specifications": ["4CORE", "30GB", "16GBRAM"]
}
```

## Contributing

1. Fork this repository
Expand Down
24 changes: 24 additions & 0 deletions _lib/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions _lib/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Hand-synced mirror of the install-script contract in hexos-platform
// (packages/shared/eshtek/apps.ts, packages/shared/eshtek/preferences.ts,
// packages/shared/eshtek/hooks.ts, packages/backend/src/lib/macros.ts).
//
// The catalog is public and the platform is private, so CI cannot import the
// real types. Everything the validator enforces is duplicated here instead, in
// one place, so a drift fix is a single edit rather than a hunt through checks.

/** LocationPreferenceId — the only names $LOCATION() can resolve. */
export const LOCATIONS = [
"ApplicationsPerformance",
"ApplicationsCapacity",
"Downloads",
"Documents",
"Media",
"Photos",
"Music",
"Movies",
"Shows",
"Videos",
"VirtualizationPerformance",
"VirtualizationCapacity",
"InstallMedia",
"VirtualDisks",
] as const;

/** APP_SPEC_REGEX. */
export const SPEC_REGEX = /^(\d+)(MBRAM|GBRAM|MB|GB|CORE)$|^GPU$/;

/** AppPermission. */
export const PERMISSIONS = ["READ_WRITE_LOCATIONS"] as const;

/** InstallationQuestionType. */
export const QUESTION_TYPES = ["text", "number", "select", "boolean", "password"] as const;

/** HookEvent. */
export const HOOK_EVENTS = [
"onBeforeInstall",
"onAfterInstall",
"onBeforeUpgrade",
"onAfterUpgrade",
"action",
] as const;

/** Install-script versions the backend still parses (appsInstallScriptSchema). */
export const SUPPORTED_VERSIONS = [4, 5, 6] as const;

/** Macros taking parenthesised arguments. */
export const CALL_MACROS = [
"IF",
"QUESTION",
"MEMORY",
"RANDOM_STRING",
"HOST_PATH",
"MOUNTED_HOST_PATH",
"LOCATION",
"APP_INSTALLED",
"GPU_CONFIG",
] as const;

/** Macros substituted as bare tokens, with no argument list. */
export const BARE_MACROS = ["SERVER_LAN_IP", "SERVER_HOST_ID"] as const;

// --- v6 hook contract ---

/** HookKind — descriptive taxonomy (surfaces, icons, grouping). */
export const HOOK_KINDS = ["connect", "maintain", "custom"] as const;

/** HookEventSpec — what goes in v6 hooks' `events` array. */
export const HOOK_EVENT_SPECS = [
"userAction",
"onBeforeInstall",
"onAfterInstall",
"onBeforeUpgrade",
"onAfterUpgrade",
] as const;

/** HookRerun — re-run semantics for user-triggerable hooks. */
export const HOOK_RERUNS = ["idempotent", "converge", "refuse"] as const;

/** HookSurface — where a hook can appear. */
export const HOOK_SURFACES = ["installPicker", "card", "fileBrowser", "widget"] as const;

/** Known hook condition types. Unknown types are accepted (fail closed at runtime). */
export const HOOK_CONDITION_TYPES = [
"appInstalled",
"appRunning",
"appVersion",
"capabilityPresent",
"script",
] as const;

/** HookConditionRole. */
export const HOOK_CONDITION_ROLES = ["visibility", "availability"] as const;

/** Hook target types. */
export const HOOK_TARGET_TYPES = ["files"] as const;

// --- v6 widget contract ---

/**
* Supported widgetsSchema version. In 3 a widget is the app's dashboard card,
* so its `slots` and `background` sit on the declaration itself; the `sizes`
* of 2 are gone.
*/
export const SUPPORTED_WIDGETS_SCHEMA = 3;

/** Widget slot types. An image is never a slot: the card's artwork is its background. */
export const WIDGET_SLOT_TYPES = ["text", "stat", "list", "progress"] as const;

/** The shortest widget `refresh` the platform honours, in seconds: one deck poll (MIN_REFRESH_S). */
export const WIDGET_MIN_REFRESH_S = 5;

/** Most slots a widget declares. */
export const WIDGET_MAX_SLOTS = 3;
72 changes: 72 additions & 0 deletions _lib/hook_context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/**
* HookContext — the runtime context passed to every hook entrypoint.
*
* MIRROR: this interface is a hand-maintained copy of the public surface of
* hexos-platform/packages/backend/src/interface/hooks.ts (class HookContext).
* Update BOTH files when the contract grows — nothing checks this for you.
*
* The actual class is provided by hexos-platform at execution time.
* Hook scripts import this interface for autocompletion and type safety.
*
Expand All @@ -15,6 +19,27 @@
* }
* ```
*/
/**
* One mount entry from the app's live config — the same shape every script
* surface receives (hooks, actions, widgets).
*/
export interface ContextMount {
/** Host path (/mnt/...). */
hostPath: string;
/**
* The same directory as the app sees it inside its container — null for
* chart-style primary storage (config/data/logs) whose container path the
* chart fixes internally. Match those by hostPath instead.
*/
containerPath: string | null;
/**
* The same directory as THIS script can read it (the local node's host
* bind-mount). Use for harvesting app config files, e.g. Plex's
* Preferences.xml — no more hand-walking /host/mnt.
*/
localPath: string;
}

export interface HookContext {
/** "app" or "vm" */
readonly resourceType: string;
Expand All @@ -41,6 +66,31 @@ export interface HookContext {
*/
readonly inputs: Record<string, unknown>;

/**
* File-targeted actions only: the files the user selected. `containerPath`
* is the same file as THIS app sees it (pre-resolved from its mounts), or
* null when the file isn't under any of the app's mounts — scripts for
* requiresTargetMount:false actions must handle null (e.g. deliver the
* file themselves via the platform mounts).
*/
readonly target?: {
type: "files";
files: Array<{
path: string;
containerPath: string | null;
name: string;
extension: string;
size?: number;
}>;
};

/**
* The app's live mounts, pre-resolved (empty when the app doesn't exist
* yet, e.g. onBeforeInstall, or mount discovery failed — scripts must
* tolerate an empty list).
*/
readonly mounts: ContextMount[];

/**
* Type-safe input accessor. Throws if the input is missing.
*
Expand All @@ -67,6 +117,13 @@ export interface HookContext {
* ]);
* ```
*/
/**
* Base URL of another installed app (e.g. "http://192.168.1.50:5055"),
* or null when it isn't installed or has no known port. Available to app
* actions; lifecycle hooks may receive null depending on platform version.
*/
getInstalledAppUrl(appId: string): Promise<string | null>;

registerCheckpoints(checkpoints: Array<{ id: string; message: string }>): Promise<void>;

/**
Expand Down Expand Up @@ -94,6 +151,21 @@ export interface HookContext {
*/
updateCheckpointMessage(id: string, message: string): Promise<void>;

/**
* Drive the task's progress bar directly (clamped 0-99) without touching
* checkpoints. For long-running work whose real progress the script can
* measure — e.g. frames encoded out of a known total. A later
* emitCheckpoint WITHOUT an explicit progress reverts the bar to
* checkpoint-fraction math, so pass explicit progress on the final
* checkpoint when using this.
*
* @example
* ```ts
* await ctx.setProgress(42);
* ```
*/
setProgress(percent: number): Promise<void>;

/**
* Fail the hook with a structured error. The message is shown as the primary error,
* and context items are rendered as labeled key-value pairs in the UI.
Expand Down
13 changes: 13 additions & 0 deletions _lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "hexos-app-catalog-lib",
"private": true,
"type": "module",
"scripts": {
"validate": "cd .. && bun _lib/validate.ts",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.6.0"
}
}
13 changes: 13 additions & 0 deletions _lib/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "bundler",
"types": ["bun"],
"strict": true,
"noUncheckedIndexedAccess": false,
"skipLibCheck": true,
"noEmit": true
},
"include": ["*.ts"]
}
Loading
Loading