Issue 17: StartOS 0.4.0 packaging

- Dockerfile: multi-stage build (Node frontend + Python backend)
- StartOS manifest, interfaces, backups, version graph
- start.sh: auto-generates session secret, creates first approver on boot
- Backend serves built frontend as SPA in production
- Single data volume for SQLite DB, platform backups cover it
This commit is contained in:
Johnny 5
2026-06-07 19:23:26 +00:00
parent a70bdeaa5e
commit 89eddd2ad0
22 changed files with 636 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
javascript/
+40
View File
@@ -0,0 +1,40 @@
# --- Frontend build stage ---
FROM node:20-slim AS frontend-build
WORKDIR /build/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --include=dev
COPY frontend/ ./
RUN npx vite build
# --- Backend stage ---
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python deps
COPY backend/pyproject.toml ./
COPY backend/ten31portal/ ./ten31portal/
COPY backend/alembic/ ./alembic/
COPY backend/alembic.ini ./
RUN pip install --no-cache-dir .
# Copy built frontend
COPY --from=frontend-build /build/frontend/dist ./static/
# Startup script
COPY deploy/start.sh ./start.sh
RUN chmod +x ./start.sh
# Data volume mount point
RUN mkdir -p /data
ENV TEN31_DB_PATH=/data/portal.db
ENV TEN31_SESSION_SECRET=change-me
EXPOSE 8000
CMD ["./start.sh"]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

+345
View File
@@ -0,0 +1,345 @@
{
"name": "ten31portal-startos",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ten31portal-startos",
"version": "0.1.0",
"dependencies": {
"@start9labs/start-sdk": "^0.4.0-beta.58"
},
"devDependencies": {
"@types/node": "^22.19.0",
"@vercel/ncc": "^0.38.4",
"typescript": "^5.9.3"
}
},
"node_modules/@iarna/toml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==",
"license": "ISC"
},
"node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodable/entities": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
"integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nodable"
}
],
"license": "MIT"
},
"node_modules/@start9labs/start-sdk": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0.tgz",
"integrity": "sha512-PFfO7tV9nzQFZL3KXaZyf16C5VZtM+dCDlRhLHpmwssTKtcjyCEhBrB9locuS2yFqu69rj+5kLFzCWDHeRRibg==",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
"@noble/curves": "^1.8.2",
"@noble/hashes": "^1.7.2",
"@types/ini": "^4.1.1",
"deep-equality-data-structures": "^2.0.0",
"fast-xml-parser": "^5.5.6",
"ini": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
"mime": "^4.0.7",
"yaml": "^2.7.1",
"zod": "^4.3.6",
"zod-deep-partial": "^1.2.0"
}
},
"node_modules/@types/ini": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz",
"integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz",
"integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@vercel/ncc": {
"version": "0.38.4",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
"dev": true,
"license": "MIT",
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/deep-equality-data-structures": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz",
"integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==",
"license": "MIT",
"dependencies": {
"object-hash": "^3.0.0"
}
},
"node_modules/fast-xml-builder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.5.0",
"xml-naming": "^0.1.0"
}
},
"node_modules/fast-xml-parser": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz",
"integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"@nodable/entities": "^2.1.0",
"fast-xml-builder": "^1.2.0",
"path-expression-matcher": "^1.5.0",
"strnum": "^2.3.0",
"xml-naming": "^0.1.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/ini": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
"integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"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==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
"node_modules/mime": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
"funding": [
"https://github.com/sponsors/broofa"
],
"license": "MIT",
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">=16"
}
},
"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==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/path-expression-matcher": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/strnum": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
"integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"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==",
"license": "BSD-2-Clause"
},
"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==",
"license": "MIT"
},
"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==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/xml-naming": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-deep-partial": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz",
"integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==",
"license": "MIT",
"peerDependencies": {
"zod": "^4.1.13"
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "ten31portal-startos",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
"check": "tsc --noEmit"
},
"dependencies": {
"@start9labs/start-sdk": "^0.4.0-beta.58"
},
"devDependencies": {
"@types/node": "^22.19.0",
"@vercel/ncc": "^0.38.4",
"typescript": "^5.9.3"
}
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
set -e
# Generate session secret if not set or default
if [ "$TEN31_SESSION_SECRET" = "change-me" ]; then
if [ -f /data/.session-secret ]; then
export TEN31_SESSION_SECRET=$(cat /data/.session-secret)
else
export TEN31_SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
echo "$TEN31_SESSION_SECRET" > /data/.session-secret
chmod 600 /data/.session-secret
fi
fi
# Create first approver on first boot if no users exist
if [ ! -f /data/.initialized ]; then
echo "First boot: running migrations and creating default approver..."
# Set default credentials (user can change via CLI later)
ADMIN_NAME="${TEN31_ADMIN_NAME:-Admin}"
ADMIN_EMAIL="${TEN31_ADMIN_EMAIL:-admin@ten31.com}"
ADMIN_PASSWORD="${TEN31_ADMIN_PASSWORD:-changeme}"
python3 -m ten31portal.cli create-user \
--name "$ADMIN_NAME" \
--email "$ADMIN_EMAIL" \
--role approver \
--password "$ADMIN_PASSWORD" || true
touch /data/.initialized
echo "Default approver created: $ADMIN_EMAIL"
fi
# Serve frontend static files from the backend
exec uvicorn ten31portal.main:app \
--host 0.0.0.0 \
--port 8000 \
--log-level info
+4
View File
@@ -0,0 +1,4 @@
import { sdk } from '../sdk'
// No custom actions for v1. Config is handled at install time.
export const actions = sdk.Actions.of()
+5
View File
@@ -0,0 +1,5 @@
import { sdk } from './sdk'
export const { createBackup, restoreInit } = sdk.setupBackups(
async ({ effects }) => sdk.Backups.ofVolumes('main'),
)
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from './sdk'
export const setDependencies = sdk.setupDependencies(async () => ({}))
+8
View File
@@ -0,0 +1,8 @@
export { createBackup } from './backups'
export { main } from './main'
export { init, uninit } from './init'
export { actions } from './actions'
import { buildManifest } from '@start9labs/start-sdk'
import { manifest as sdkManifest } from './manifest'
import { versionGraph } from './install/versionGraph'
export const manifest = buildManifest(versionGraph, sdkManifest)
+16
View File
@@ -0,0 +1,16 @@
import { sdk } from '../sdk'
import { setDependencies } from '../dependencies'
import { setInterfaces } from '../interfaces'
import { versionGraph } from '../install/versionGraph'
import { actions } from '../actions'
import { restoreInit } from '../backups'
export const init = sdk.setupInit(
restoreInit,
versionGraph,
setInterfaces,
setDependencies,
actions,
)
export const uninit = sdk.setupUninit(versionGraph)
+7
View File
@@ -0,0 +1,7 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { current, other } from './versions'
export const versionGraph = VersionGraph.of({
current,
other,
})
+2
View File
@@ -0,0 +1,2 @@
export { v_0_1_0 as current } from './v_0_1_0'
export const other = []
@@ -0,0 +1,16 @@
import { VersionInfo } from '@start9labs/start-sdk'
import * as fs from 'fs'
export const v_0_1_0 = VersionInfo.of({
version: '0.1.0:0',
releaseNotes: {
en_US: 'Initial release. Internal fund administration and quarterly valuation sign-off.',
},
migrations: {
up: async ({ effects }) => {
// DB initialization handled by Alembic on app startup.
// First approver account created by start.sh if no users exist.
},
down: async ({ effects }) => {},
},
})
+24
View File
@@ -0,0 +1,24 @@
import { sdk } from './sdk'
const appPort = 8000
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const uiMulti = sdk.MultiHost.of(effects, 'ui-multi')
const uiMultiOrigin = await uiMulti.bindPort(appPort, {
protocol: 'http',
})
const ui = sdk.createInterface(effects, {
name: 'Web UI',
id: 'ui',
description: 'Ten31Portal fund administration interface',
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
const uiReceipt = await uiMultiOrigin.export([ui])
return [uiReceipt]
})
+39
View File
@@ -0,0 +1,39 @@
import { sdk } from './sdk'
const appPort = 8000
export const main = sdk.setupMain(async ({ effects }) => {
console.info('Starting Ten31Portal')
const subcontainer = await sdk.SubContainer.of(
effects,
{ imageId: 'main' },
sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: '/data',
readonly: false,
}),
'ten31portal-sub',
)
return sdk.Daemons.of(effects).addDaemon('primary', {
subcontainer,
exec: {
command: ['sh', '-c', '/app/start.sh'],
env: {
TEN31_DB_PATH: '/data/portal.db',
TEN31_SESSION_SECRET: process.env.TEN31_SESSION_SECRET || 'change-me',
},
},
ready: {
display: 'Web Interface',
fn: () =>
sdk.healthCheck.checkPortListening(effects, appPort, {
successMessage: 'Ten31Portal is ready',
errorMessage: 'Ten31Portal is starting...',
}),
},
requires: [],
})
})
+32
View File
@@ -0,0 +1,32 @@
import { setupManifest } from '@start9labs/start-sdk'
export const manifest = setupManifest({
id: 'ten31portal',
title: 'Ten31Portal',
license: 'MIT',
packageRepo: 'https://github.com/smallblocks/Ten31Portal',
upstreamRepo: 'https://github.com/smallblocks/Ten31Portal',
marketingUrl: 'https://ten31.com',
donationUrl: 'https://ten31.com',
docsUrls: ['https://github.com/smallblocks/Ten31Portal/blob/main/README.md'],
description: {
short: 'Internal fund administration and valuation sign-off',
long: 'System of record for Ten31 entities, holdings, positions, and quarterly valuation sign-off. Replaces Carta for internal use.',
},
volumes: ['main'],
images: {
main: {
source: { dockerBuild: { dockerfile: './Dockerfile', workdir: '.' } },
arch: ['x86_64', 'aarch64'],
},
},
alerts: {
install: 'After installation, access the web UI to log in. A default approver account is created on first boot using the configured credentials.',
update: null,
uninstall: 'Uninstalling will remove all fund data. Create a backup first.',
restore: null,
start: null,
stop: null,
},
dependencies: {},
})
+4
View File
@@ -0,0 +1,4 @@
import { StartSdk } from '@start9labs/start-sdk'
import { manifest } from './manifest'
export const sdk = StartSdk.of().withManifest(manifest).build(true)
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"outDir": "./javascript",
"rootDir": "./startos"
},
"include": ["startos/**/*.ts"]
}