diff --git a/.gitignore b/.gitignore index df89f86..2603f70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -hello-world.s9pk -image.tar -scripts/*.js +*.s9pk +startos/*.js +node_modules/ .DS_Store .vscode/ -docker-images \ No newline at end of file +docker-images +javascript \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 56fa461..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "hello-world"] - path = hello-world - url = https://github.com/Start9Labs/hello-world diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f6fe710..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM alpine:3.17 - -RUN apk update -RUN apk add --no-cache tini && \ - rm -f /var/cache/apk/* - -ARG ARCH -ADD ./hello-world/target/${ARCH}-unknown-linux-musl/release/hello-world /usr/local/bin/hello-world -RUN chmod +x /usr/local/bin/hello-world -ADD ./docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh -RUN chmod a+x /usr/local/bin/docker_entrypoint.sh diff --git a/Makefile b/Makefile index 2d6e598..53b3cbc 100644 --- a/Makefile +++ b/Makefile @@ -1,75 +1,35 @@ -PKG_ID := $(shell yq e ".id" manifest.yaml) -PKG_VERSION := $(shell yq e ".version" manifest.yaml) -TS_FILES := $(shell find ./ -name \*.ts) -HELLO_WORLD_SRC := $(shell find ./hello-world/src) hello-world/Cargo.toml hello-world/Cargo.lock +PACKAGE_ID := $(shell grep -o "id: '[^']*'" startos/manifest.ts | sed "s/id: '\([^']*\)'/\1/") -# delete the target of a rule if it has changed and its recipe exits with a nonzero exit status -.DELETE_ON_ERROR: +# Phony targets +.PHONY: all clean install -all: verify - -verify: $(PKG_ID).s9pk - @embassy-sdk verify s9pk $(PKG_ID).s9pk +# Default target +all: ${PACKAGE_ID}.s9pk @echo " Done!" - @echo " Filesize: $(shell du -h $(PKG_ID).s9pk) is ready" - -install: -ifeq (,$(wildcard ~/.embassy/config.yaml)) - @echo; echo "You must define \"host: http://embassy-server-name.local\" in ~/.embassy/config.yaml config file first"; echo -else - embassy-cli package install $(PKG_ID).s9pk -endif - -clean: - rm -rf docker-images - rm -f image.tar - rm -f $(PKG_ID).s9pk - rm -f scripts/*.js + @echo " Filesize: $(shell du -h $(PACKAGE_ID).s9pk) is ready" -clean-manifest: - @sed -i '' '/^[[:blank:]]*#/d;s/#.*//' manifest.yaml - @echo; echo "Comments successfully removed from manifest.yaml file."; echo +# Build targets +${PACKAGE_ID}.s9pk: $(shell start-cli s9pk list-ingredients) + start-cli s9pk pack -# BEGIN REBRANDING -rebranding: - @read -p "Enter new package ID name (must be a single word): " NEW_PKG_ID; \ - read -p "Enter new package title: " NEW_PKG_TITLE; \ - find . \( -name "*.md" -o -name ".gitignore" -o -name "manifest.yaml" -o -name "*Service.yml" \) -type f -not -path "./hello-world/*" -exec sed -i '' -e "s/hello-world/$$NEW_PKG_ID/g; s/Hello World/$$NEW_PKG_TITLE/g" {} +; \ - echo; echo "Rebranding complete."; echo " New package ID name is: $$NEW_PKG_ID"; \ - echo " New package title is: $$NEW_PKG_TITLE"; \ - sed -i '' -e '/^# BEGIN REBRANDING/,/^# END REBRANDING/ s/^#*/#/' Makefile - @echo; echo "Note: Rebranding code has been commented out in Makefile"; echo -# END REBRANDING +javascript/index.js: $(shell git ls-files startos) tsconfig.json node_modules package.json + npm run build -scripts/embassy.js: $(TS_FILES) - deno bundle scripts/embassy.ts scripts/embassy.js +node_modules: package.json package-lock.json + npm ci -docker-images/aarch64.tar: Dockerfile docker_entrypoint.sh hello-world/target/aarch64-unknown-linux-musl/release/hello-world -ifeq ($(ARCH),x86_64) -else - mkdir -p docker-images - docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=aarch64 --platform=linux/arm64 -o type=docker,dest=docker-images/aarch64.tar . -endif +package-lock.json: package.json + npm i -docker-images/x86_64.tar: Dockerfile docker_entrypoint.sh hello-world/target/x86_64-unknown-linux-musl/release/hello-world -ifeq ($(ARCH),aarch64) -else - mkdir -p docker-images - docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=x86_64 --platform=linux/amd64 -o type=docker,dest=docker-images/x86_64.tar . -endif - -$(PKG_ID).s9pk: manifest.yaml instructions.md icon.png LICENSE scripts/embassy.js docker-images/aarch64.tar docker-images/x86_64.tar -ifeq ($(ARCH),aarch64) - @echo "embassy-sdk: Preparing aarch64 package ..." -else ifeq ($(ARCH),x86_64) - @echo "embassy-sdk: Preparing x86_64 package ..." -else - @echo "embassy-sdk: Preparing Universal Package ..." -endif - @embassy-sdk pack - -hello-world/target/aarch64-unknown-linux-musl/release/hello-world: $(HELLO_WORLD_SRC) - docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/hello-world:/home/rust/src messense/rust-musl-cross:aarch64-musl cargo build --release +# Clean target +clean: + rm -rf ${PACKAGE_ID}.s9pk + rm -rf javascript + rm -rf node_modules -hello-world/target/x86_64-unknown-linux-musl/release/hello-world: $(HELLO_WORLD_SRC) - docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/hello-world:/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release +# Install target +install: + @if [ ! -f ~/.startos/config.yaml ]; then echo "You must define \"host: http://server-name.local\" in ~/.startos/config.yaml config file first."; exit 1; fi + @echo "\nInstalling to $$(grep -v '^#' ~/.startos/config.yaml | cut -d'/' -f3) ...\n" + @[ -f $(PACKAGE_ID).s9pk ] || ( $(MAKE) && echo "\nInstalling to $$(grep -v '^#' ~/.startos/config.yaml | cut -d'/' -f3) ...\n" ) + @start-cli package install -s $(PACKAGE_ID).s9pk diff --git a/README.md b/README.md index 2853b08..da01557 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,11 @@ -# Wrapper for hello-world +# Hello World for StartOS -Hello World is a simple, minimal project that serves as a template for creating a service that runs on embassyOS. This repository creates the `s9pk` package that is installed to run `hello-world` on [embassyOS](https://github.com/Start9Labs/embassy-os/). Learn more about service packaging in the [Developer Docs](https://start9.com/latest/developer-docs/). +A bare bones project serving as a template for [StartOS Service Packaging](https://docs.star9.com/packaging-guide). -## Dependencies +## Building from source -Install the system dependencies below to build this project by following the instructions in the provided links. You can also find detailed steps to setup your environment in the service packaging [documentation](https://github.com/Start9Labs/service-pipeline#development-environment). +`make` -- [docker](https://docs.docker.com/get-docker) -- [docker-buildx](https://docs.docker.com/buildx/working-with-buildx/) -- [yq](https://mikefarah.gitbook.io/yq) -- [deno](https://deno.land/) -- [make](https://www.gnu.org/software/make/) -- [embassy-sdk](https://github.com/Start9Labs/embassy-os/tree/master/backend) +## Creating your own project -## Build environment -Prepare your embassyOS build environment. In this example we are using Ubuntu 20.04. -1. Install docker -``` -curl -fsSL https://get.docker.com | bash -sudo usermod -aG docker "$USER" -exec sudo su -l $USER -``` -2. Set buildx as the default builder -``` -docker buildx install -docker buildx create --use -``` -3. Enable cross-arch emulated builds in docker -``` -docker run --privileged --rm linuxkit/binfmt:v0.8 -``` -4. Install yq -``` -sudo snap install yq -``` -5. Install deno -``` -sudo snap install deno -``` -6. Install essentials build packages -``` -sudo apt-get install -y build-essential openssl libssl-dev libc6-dev clang libclang-dev ca-certificates -``` -7. Install Rust -``` -curl https://sh.rustup.rs -sSf | sh -# Choose nr 1 (default install) -source $HOME/.cargo/env -``` -8. Build and install embassy-sdk -``` -cd ~/ && git clone --recursive https://github.com/Start9Labs/embassy-os.git -cd embassy-os/backend/ -./install-sdk.sh -embassy-sdk init -``` -Now you are ready to build the `hello-world` package! - -## Cloning - -Clone the project locally: - -``` -git clone https://github.com/Start9Labs/hello-world-wrapper.git -cd hello-world-wrapper -git submodule update --init --recursive -``` - -## Building - -To build the `hello-world` package for all platforms using embassy-sdk version >=0.3.3, run the following command: - -``` -make -``` - -To build the `hello-world` package for a single platform using embassy-sdk version <=0.3.2, run: - -``` -# for amd64 -make ARCH=x86_64 -``` -or -``` -# for arm64 -make ARCH=aarch64 -``` - -## Installing (on embassyOS) - -Run the following commands to determine successful install: -> :information_source: Change embassy-server-name.local to your Embassy address - -``` -embassy-cli auth login -# Enter your embassy password -embassy-cli --host https://embassy-server-name.local package install hello-world.s9pk -``` - -If you already have your `embassy-cli` config file setup with a default `host`, you can install simply by running: - -``` -make install -``` - -> **Tip:** You can also install the hello-world.s9pk using **Sideload Service** under the **System > Manage** section. - -### Verify Install - -Go to your Embassy Services page, select **Hello World**, configure and start the service. Then, verify its interfaces are accessible. - -**Done!** +Follow the [Quick Start Guide](https://docs.star9.com/packaging-guide/quick-start/) for StartOS service packaging, replacing references to `hello-world-startos` with this service ID. diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh deleted file mode 100755 index aab61f3..0000000 --- a/docker_entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -exec tini hello-world diff --git a/hello-world b/hello-world deleted file mode 160000 index 48ce8a5..0000000 --- a/hello-world +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 48ce8a519b14eb6c32d770af66656e04a7866228 diff --git a/instructions.md b/instructions.md index c636962..d9aca7e 100644 --- a/instructions.md +++ b/instructions.md @@ -1,5 +1,7 @@ -# Instructions for Hello World +# Hello World for StartOS -Instructions go here. These appear to the user in the UI on the Service page under **Instructions**. +A bare bones project serving as a template for [StartOS Service Packaging](https://docs.star9.com/packaging-guide). -You are allowed to include basic [Markdown formatting](https://www.markdownguide.org/basic-syntax). +## Building from source + +Follow the [Quick Start Guide](https://docs.star9.com/packaging-guide/quick-start/) for StartOS service packaging, replacing references to `hello-world-startos` with this service ID. diff --git a/manifest.yaml b/manifest.yaml deleted file mode 100644 index afe4b68..0000000 --- a/manifest.yaml +++ /dev/null @@ -1,146 +0,0 @@ -# Example written in yaml (toml and json are also acceptable) - -# The package identifier used by the OS. This must be unique amongst all other known packages -id: hello-world - # A human readable service title -title: "Hello World" -# Service version - accepts up to four digits, where the last confirms to revisions necessary for EmbassyOS - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of the service. -version: 1.0.0.1 -# Release notes for the update - can be a string, paragraph or URL -release-notes: "Revamped for EmabssyOS 0.3.3" -# The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package. -license: mit -# The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks (more below). This key must exist. But could be embedded into the source repository. -wrapper-repo: "https://github.com/Start9Labs/hello-world-wrapper" -# The original project repository URL. There is no upstream repo in this example -upstream-repo: "https://github.com/Start9Labs/hello-world" -# URL to the support site / channel for the project. This key can be omitted if none exists, or it can link to the original project repository issues. -support-site: "https://docs.start9.com/" -# URL to the marketing site for the project. This key can be omitted if none exists, or it can link to the original project repository. -marketing-site: "https://start9.com/" -# The series of commands to build the project into an s9pk for arm64/v8. In this case we are using a Makefile with the simple build command "make". -build: ["make"] -# Human readable descriptors for the service. These are used throughout the EmbassyOS user interface, primarily in the marketplace. -description: - # This is the first description visible to the user in the marketplace. - short: Example service - # This description will display with additional details in the service's individual marketplace page - long: | - Hello World is a bare-bones service that launches a web interface to say "Hello World", and nothing more. -# These assets are static files necessary for packaging the service for Start9 (into an s9pk). Each value is a path to the specified asset. If an asset is missing from this list, or otherwise denoted, it will be defaulted to the values denoted below. -assets: - # Default = LICENSE.md - license: LICENSE - # Default = icon.png (.svg allowed) - icon: icon.png - # Default = INSTRUCTIONS.md - instructions: instructions.md - # Default = image.tar - # docker-images: image.tar -# ----- This section commented out until we support long-running containers ----- -# The main action for initializing the service. This can be script to utilize the eOS scripting apis, or docker. -# main: -# type: script -# # Defines the containers needed to run the main and mounted volumes -# containers: -# main: -# # Identifier for the main image volume, which will be used when other actions need to mount to this volume. -# image: main -# # Specifies where to mount the data volume(s), if there are any. Mounts for pointer dependency volumes are also denoted here. These are necessary if data needs to be read from / written to these volumes. -# mounts: -# # Specifies where on the service's file system its persistence directory should be mounted prior to service startup -# main: /root -# ----- END commented section ----- -main: - # Docker is currently the only action implementation - type: docker - # Identifier for the main image volume, which will be used when other actions need to mount to this volume. - image: main - # The executable binary for starting the initialization action. For docker actions, this is typically a "docker_entrypoint.sh" file. See the Dockerfile and the docker_entrypoint.sh in this project for additional details. - entrypoint: "docker_entrypoint.sh" - # Any arguments that should be passed into the entrypoint executable - args: [] - # Specifies where to mount the data volume(s), if there are any. Mounts for pointer dependency volumes are also denoted here. These are necessary if data needs to be read from / written to these volumes. - mounts: - # Specifies where on the service's file system its persistence directory should be mounted prior to service startup - main: /root -# This is where health checks would be defined - see a more advanced example in https://github.com/Start9Labs/embassy-pages-wrapper -health-checks: {} -config: ~ -properties: ~ -# type: script -# This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked -volumes: - # This is the image where files from the project asset directory will go - main: - type: data -# This specifies how to configure the port mapping for exposing the service over TOR and LAN (if applicable). Many interfaces can be specified depending on the needs of the service. If it can be launched over a Local Area Network connection, specify a `lan-config`. Otherwise, at minimum, a `tor-config` must be specified. -interfaces: - # This key is the internal name that the OS will use to configure the interface - main: - # A human readable name for display in the UI - name: User Interface - # A descriptive description of what the interface does - description: A simple user interface that is expected to display the text "Hello Word" - tor-config: - # Port mappings are from the external port to the internal container port - port-mapping: - 80: "80" - # Port mappings are from the external port to the internal container port - lan-config: - 443: - ssl: true - internal: 80 - # Denotes if the service has a user interface to display - ui: true - # Denotes the protocol specifications used by this interface - protocols: - - tcp - - http -dependencies: {} -# Specifies how backups should be run for this service. The default EmbassyOS provided option is to use the duplicity backup library on a system image (compat) -backup: - create: - # Currently, only docker actions are supported. - type: docker - # The docker image to use. In this case, a pre-loaded system image called compat - image: compat - # Required if the action uses a system image. The default value is false. - system: true - # The executable to run the command to begin the backup create process - entrypoint: compat - # Arguments to pass into the entrypoint executable. In this example, the full command run will be: `compat duplicity hello-world /mnt/backup /root/data` - args: - - duplicity - - create - - /mnt/backup - # For duplicity, the backup mount point needs to be something other than `/root`, so we default to `/root/data` - - /root/data - mounts: - # BACKUP is the default volume that is used for backups. This is whatever backup drive is mounted to the device, or a network filesystem. - # The value here donates where the mount point will be. The backup drive is mounted to this location. - BACKUP: "/mnt/backup" - main: "/root/data" - # The action to execute the backup restore functionality. Details for the keys below are the same as above. - restore: - type: docker - image: compat - system: true - entrypoint: compat - args: - - duplicity - - restore - - /mnt/backup - - /root/data - mounts: - BACKUP: "/mnt/backup" - main: "/root/data" -migrations: - from: - "*": - type: script - args: ["from"] - to: - "*": - type: script - args: ["to"] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..daec4f3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,209 @@ +{ + "name": "hello-world-startos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hello-world-startos", + "dependencies": { + "@start9labs/start-sdk": "0.3.6-beta.9" + }, + "devDependencies": { + "@types/node": "^22.1.0", + "@vercel/ncc": "^0.38.1", + "prettier": "^3.2.5", + "typescript": "^5.4.3" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@start9labs/start-sdk": { + "version": "0.3.6-beta.9", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.3.6-beta.9.tgz", + "integrity": "sha512-c9jlLlwUHLqxAZiAGiIeo2ag5orc8/L+6L/xVGRpNlqAEzb8PJPUPCaLtqC1guXstUMLxLgja1csj0p6tuZsBg==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dev": true, + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", + "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-matches": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.2.1.tgz", + "integrity": "sha512-qdnMgTHsGCEGGK6QiaNMY2vD9eQtRp2Q+pAxcOAzxHJKDKTBYsc1ISTg1zp8H2+EmtCB0eko/1TwYUA5/mUGug==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e7af9a --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "hello-world-startos", + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "0.3.6-beta.9" + }, + "devDependencies": { + "@types/node": "^22.1.0", + "@vercel/ncc": "^0.38.1", + "prettier": "^3.2.5", + "typescript": "^5.4.3" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/scripts/deps.ts b/scripts/deps.ts deleted file mode 100644 index 6519c00..0000000 --- a/scripts/deps.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.5/mod.ts"; -export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.5/util.ts"; \ No newline at end of file diff --git a/scripts/embassy.ts b/scripts/embassy.ts deleted file mode 100644 index 74f56eb..0000000 --- a/scripts/embassy.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { setConfig } from "./procedures/setConfig.ts"; -export { getConfig } from "./procedures/getConfig.ts"; -export { properties } from "./procedures/properties.ts"; -export { migration } from "./procedures/migrations.ts"; -export { main } from "./procedures/main.ts"; \ No newline at end of file diff --git a/scripts/procedures/getConfig.ts b/scripts/procedures/getConfig.ts deleted file mode 100644 index d90ab5d..0000000 --- a/scripts/procedures/getConfig.ts +++ /dev/null @@ -1,5 +0,0 @@ -// To utilize the default config system built, this file is required. It defines the *structure* of the configuration file. These structured options display as changeable UI elements within the "Config" section of the service details page in the Embassy UI. - -import { compat, types as T } from "../deps.ts"; - -export const getConfig: T.ExpectedExports.getConfig = compat.getConfig({}); diff --git a/scripts/procedures/main.ts b/scripts/procedures/main.ts deleted file mode 100644 index a426122..0000000 --- a/scripts/procedures/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { types as T, util } from "../deps.ts"; - -export const main = async (effects: T.Effects) => { - // args defaulted to [] - not necessary to include if empty - await effects.runDaemon({ command: "docker_entrypoint.sh", args: [] }).wait(); - return util.ok; -} \ No newline at end of file diff --git a/scripts/procedures/migrations.ts b/scripts/procedures/migrations.ts deleted file mode 100644 index 14691a8..0000000 --- a/scripts/procedures/migrations.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { compat, types as T } from "../deps.ts"; - -export const migration: T.ExpectedExports.migration = compat.migrations - .fromMapping({}, "1.0.0.1" ); diff --git a/scripts/procedures/properties.ts b/scripts/procedures/properties.ts deleted file mode 100644 index dff99aa..0000000 --- a/scripts/procedures/properties.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { compat, types as T } from "../deps.ts"; - -export const properties: T.ExpectedExports.properties = compat.properties; diff --git a/scripts/procedures/setConfig.ts b/scripts/procedures/setConfig.ts deleted file mode 100644 index 43d308f..0000000 --- a/scripts/procedures/setConfig.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This is where any configuration rules related to the configuration would go. These ensure that the user can only create a valid config. - -import { compat, } from "../deps.ts"; - -export const setConfig = compat.setConfig; diff --git a/startos/actions/index.ts b/startos/actions/index.ts new file mode 100644 index 0000000..1866c78 --- /dev/null +++ b/startos/actions/index.ts @@ -0,0 +1,9 @@ +import { sdk } from '../sdk' +import { showSecretPhrase } from './showSecretPhrase' +import { setName } from './setName' +import { nameToLogs } from './nameToLogs' + +export const actions = sdk.Actions.of() + .addAction(setName) + .addAction(showSecretPhrase) + .addAction(nameToLogs) diff --git a/startos/actions/nameToLogs.ts b/startos/actions/nameToLogs.ts new file mode 100644 index 0000000..bfe74a6 --- /dev/null +++ b/startos/actions/nameToLogs.ts @@ -0,0 +1,36 @@ +import { sdk } from '../sdk' +import { yamlFile } from '../file-models/config.yml' + +export const nameToLogs = sdk.Action.withoutInput( + // id + 'name-to-logs', + + // metadata + async ({ effects }) => ({ + name: 'Print name to Logs', + description: 'Prints "Hello [Name]" to the service logs.', + warning: null, + allowedStatuses: 'only-running', + group: null, + visibility: (await sdk.store + .getOwn(effects, sdk.StorePath.nameLastUpdatedAt) + .const()) + ? 'enabled' + : { + disabled: 'Cannot print name to logs until you update your name.', + }, + }), + + // the execution function + async ({ effects }) => { + const name = (await yamlFile.read.const(effects))!.name + console.info(`Hello ${name}`) + + return { + version: '1', + title: 'Success', + message: `"Hello ${name}" has been logged. Open the Hello World service logs to view it.`, + result: null, + } + }, +) diff --git a/startos/actions/setName.ts b/startos/actions/setName.ts new file mode 100644 index 0000000..a7b7aba --- /dev/null +++ b/startos/actions/setName.ts @@ -0,0 +1,57 @@ +import { sdk } from '../sdk' +import { yamlFile } from '../file-models/config.yml' +import { getSecretPhrase } from '../utils' + +const { InputSpec, Value } = sdk + +export const inputSpec = InputSpec.of({ + name: Value.text({ + name: 'Name', + description: + 'When you launch the Hello World UI, it will display "Hello [Name]"', + required: true, + default: 'World', + }), +}) + +export const setName = sdk.Action.withInput( + // id + 'set-name', + + // metadata + async ({ effects }) => ({ + name: 'Set Name', + description: 'Set your name so Hello World can say hello to you', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // form input specification + inputSpec, + + // optionally pre-fill the input form + async ({ effects }) => yamlFile.read.const(effects), + + // the execution function + async ({ effects, input }) => { + const yaml = await yamlFile.read.const(effects) + + if (yaml?.name === input.name) return + + await Promise.all([ + yamlFile.merge(input), + sdk.store.setOwn( + effects, + sdk.StorePath.secretPhrase, + getSecretPhrase(input.name), + ), + sdk.store.setOwn( + effects, + sdk.StorePath.nameLastUpdatedAt, + new Date().toISOString(), + ), + ]) + }, +) diff --git a/startos/actions/showSecretPhrase.ts b/startos/actions/showSecretPhrase.ts new file mode 100644 index 0000000..092eb66 --- /dev/null +++ b/startos/actions/showSecretPhrase.ts @@ -0,0 +1,33 @@ +import { sdk } from '../sdk' + +export const showSecretPhrase = sdk.Action.withoutInput( + // id + 'show-secret-phrase', + + // metadata + async ({ effects }) => ({ + name: 'Show Secret Phrase', + description: 'Reveal the secret phrase for Hello World', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // the execution function + async ({ effects }) => ({ + version: '1', + title: 'Secret Phrase', + message: + 'Below is your secret phrase. Use it to gain access to extraordinary places', + result: { + type: 'single', + value: await sdk.store + .getOwn(effects, sdk.StorePath.secretPhrase) + .const(), + copyable: true, + qr: true, + masked: true, + }, + }), +) diff --git a/startos/backups.ts b/startos/backups.ts new file mode 100644 index 0000000..6ccffb6 --- /dev/null +++ b/startos/backups.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const { createBackup, restoreBackup } = sdk.setupBackups( + async ({ effects }) => sdk.Backups.volumes('main'), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts new file mode 100644 index 0000000..7221c4b --- /dev/null +++ b/startos/dependencies.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies( + async ({ effects }) => ({}), +) diff --git a/startos/file-models/config.yml.ts b/startos/file-models/config.yml.ts new file mode 100644 index 0000000..44d8da4 --- /dev/null +++ b/startos/file-models/config.yml.ts @@ -0,0 +1,11 @@ +import { matches, FileHelper } from '@start9labs/start-sdk' +const { object, string } = matches + +const shape = object({ + name: string.optional().onMismatch(undefined), +}) + +export const yamlFile = FileHelper.yaml( + '/media/startos/volumes/main/config.yml', + shape, +) diff --git a/startos/index.ts b/startos/index.ts new file mode 100644 index 0000000..8d32c10 --- /dev/null +++ b/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup, restoreBackup } from './backups' +export { main } from './main' +export { packageInit, packageUninit, containerInit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versions } from './versions' +export const manifest = buildManifest(versions, sdkManifest) diff --git a/startos/init.ts b/startos/init.ts new file mode 100644 index 0000000..5939cda --- /dev/null +++ b/startos/init.ts @@ -0,0 +1,37 @@ +import { sdk } from './sdk' +import { exposedStore } from './store' +import { setDependencies } from './dependencies' +import { setInterfaces } from './interfaces' +import { versions } from './versions' +import { actions } from './actions' +import { getSecretPhrase } from './utils' +import { yamlFile } from './file-models/config.yml' + +// **** Install **** +const install = sdk.setupInstall(async ({ effects }) => { + const name = 'World' + + await yamlFile.write({ name }) + + await sdk.store.setOwn( + effects, + sdk.StorePath.secretPhrase, + getSecretPhrase(name), + ) +}) + +// **** Uninstall **** +const uninstall = sdk.setupUninstall(async ({ effects }) => {}) + +/** + * Plumbing. DO NOT EDIT. + */ +export const { packageInit, packageUninit, containerInit } = sdk.setupInit( + versions, + install, + uninstall, + setInterfaces, + setDependencies, + actions, + exposedStore, +) diff --git a/startos/interfaces.ts b/startos/interfaces.ts new file mode 100644 index 0000000..6e88797 --- /dev/null +++ b/startos/interfaces.ts @@ -0,0 +1,24 @@ +import { sdk } from './sdk' +import { uiPort } from './utils' + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') + const uiMultiOrigin = await uiMulti.bindPort(uiPort, { + protocol: 'http', + }) + const ui = sdk.createInterface(effects, { + name: 'Web UI', + id: 'ui', + description: 'The web interface of Hello World', + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + + const uiReceipt = await uiMultiOrigin.export([ui]) + + return [uiReceipt] +}) diff --git a/startos/main.ts b/startos/main.ts new file mode 100644 index 0000000..cd8969c --- /dev/null +++ b/startos/main.ts @@ -0,0 +1,41 @@ +import { sdk } from './sdk' +import { T } from '@start9labs/start-sdk' +import { uiPort } from './utils' + +export const main = sdk.setupMain(async ({ effects, started }) => { + /** + * ======================== Setup (optional) ======================== + * + * In this section, we fetch any resources or run any desired preliminary commands. + */ + console.info('Starting Hello World!') + + /** + * ======================== Additional Health Checks (optional) ======================== + * + * In this section, we define *additional* health checks beyond those included with each daemon (below). + */ + const healthReceipts: T.HealthReceipt[] = [] + + /** + * ======================== Daemons ======================== + * + * In this section, we create one or more daemons that define the service runtime. + * + * Each daemon defines its own health check, which can optionally be exposed to the user. + */ + return sdk.Daemons.of(effects, started, healthReceipts).addDaemon('primary', { + subcontainer: { imageId: 'hello-world' }, + command: ['hello-world'], + mounts: sdk.Mounts.of().addVolume('main', null, '/data', false), + ready: { + display: 'Web Interface', + fn: () => + sdk.healthCheck.checkPortListening(effects, uiPort, { + successMessage: 'The web interface is ready', + errorMessage: 'The web interface is not ready', + }), + }, + requires: [], + }) +}) diff --git a/startos/manifest.ts b/startos/manifest.ts new file mode 100644 index 0000000..5aa1a0e --- /dev/null +++ b/startos/manifest.ts @@ -0,0 +1,35 @@ +import { setupManifest } from '@start9labs/start-sdk' + +export const manifest = setupManifest({ + id: 'hello-world', + title: 'Hello World', + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/hello-world-wrapper', + upstreamRepo: 'https://github.com/Start9Labs/hello-world', + supportSite: 'https://docs.start9.com/', + marketingSite: 'https://start9.com/', + donationUrl: 'https://donate.start9.com/', + description: { + short: 'Bare bones example of a StartOS service', + long: 'Hello World is a template service that provides examples of basic StartOS features.', + }, + assets: [], + volumes: ['main'], + images: { + 'hello-world': { + source: { + dockerTag: 'start9/hello-world', + }, + }, + }, + hardwareRequirements: {}, + alerts: { + install: 'Optional alert to display before installing the service', + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: {}, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 0000000..26c3015 --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,13 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' +import { Store } from './store' + +/** + * Plumbing. DO NOT EDIT. + * + * The exported "sdk" const is used throughout this package codebase. + */ +export const sdk = StartSdk.of() + .withManifest(manifest) + .withStore() + .build(true) diff --git a/startos/store.ts b/startos/store.ts new file mode 100644 index 0000000..7f7b4e4 --- /dev/null +++ b/startos/store.ts @@ -0,0 +1,30 @@ +import { setupExposeStore } from '@start9labs/start-sdk' + +/** + * @description The Store is used for persisting arbitrary data that are needed by the wrapper + * package but are NOT persisted by the upstream service. Do NOT persist data here that are + * already being persisted by the service itself. + * + * Store data should be kept to a minimum. Stateless packages are easier to maintain + * and eliminate unexpected behavior. + * @type {Record} + * @example + * ``` + * export type Store = { + * key1: string + * key2: boolean + * key3: number + * key4: { + * key5: string[] + * } + * } + * ``` + */ +export type Store = { + secretPhrase: string + nameLastUpdatedAt: string | null +} + +export const exposedStore = setupExposeStore((pathBuilder) => [ + pathBuilder.nameLastUpdatedAt, +]) diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 0000000..497edac --- /dev/null +++ b/startos/utils.ts @@ -0,0 +1,8 @@ +// Here we define any constants or functions that are shared by multiple components +// throughout the package codebase. This file will be unnecessary for many packages. + +export function getSecretPhrase(name: string): string { + return `Knock knock. Who's there? ${name}!` +} + +export const uiPort = 80 diff --git a/startos/versions/index.ts b/startos/versions/index.ts new file mode 100644 index 0000000..4b82476 --- /dev/null +++ b/startos/versions/index.ts @@ -0,0 +1,4 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { v_0_3_6_0 } from './v0.3.6.0' + +export const versions = VersionGraph.of(v_0_3_6_0) diff --git a/startos/versions/v0.3.6.0.ts b/startos/versions/v0.3.6.0.ts new file mode 100644 index 0000000..31fa807 --- /dev/null +++ b/startos/versions/v0.3.6.0.ts @@ -0,0 +1,14 @@ +import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk' +import { sdk } from '../sdk' +import { setName } from '../actions/setName' + +export const v_0_3_6_0 = VersionInfo.of({ + version: '0.3.6:0', + releaseNotes: 'Revamped for StartOS 0.3.6', + migrations: { + up: async ({ effects }) => { + await sdk.action.requestOwn(effects, setName, 'critical') + }, + down: IMPOSSIBLE, + }, +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a2945a5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts", "node_modules/**/startos"], + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}