mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e8ce6df7 | ||
|
|
3517b2f0ed | ||
|
|
220f5c8676 | ||
|
|
4dfad3efb3 | ||
|
|
e152bacc7c | ||
|
|
8d9c83b0f6 | ||
|
|
2fe7530cd4 | ||
|
|
4389bf2013 | ||
|
|
50f80b975d | ||
|
|
1ccb30f0a7 | ||
|
|
d44a504c04 | ||
|
|
62ac931468 | ||
|
|
6873bc92bb | ||
|
|
02f0c0c259 | ||
|
|
db317283ac | ||
|
|
3ebe8d0fac | ||
|
|
a074c10205 | ||
|
|
55b11598f5 | ||
|
|
ebbbbc2696 | ||
|
|
0149260926 | ||
|
|
a075ff4808 | ||
|
|
2acd858ca4 | ||
|
|
064fb7ea3c | ||
|
|
2b309e9582 | ||
|
|
40df00bd95 | ||
|
|
6c342f3957 |
No files matched your search
@@ -1,21 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["@commitlint/config-conventional"],
|
"extends": ["@commitlint/config-conventional"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"type-enum": [
|
|
||||||
2,
|
|
||||||
"always",
|
|
||||||
[
|
|
||||||
"feat",
|
|
||||||
"fix",
|
|
||||||
"docs",
|
|
||||||
"style",
|
|
||||||
"refactor",
|
|
||||||
"perf",
|
|
||||||
"test",
|
|
||||||
"chore",
|
|
||||||
"revert"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"subject-case": [0]
|
"subject-case": [0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-44
@@ -5,13 +5,22 @@ yarn-error.log*
|
|||||||
|
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
.next
|
coverage
|
||||||
.nuxt
|
release
|
||||||
|
|
||||||
.env.local
|
# Local data, secrets and editor state
|
||||||
.env.development.local
|
db
|
||||||
.env.test.local
|
old_db
|
||||||
.env.production.local
|
ssl
|
||||||
|
uploads
|
||||||
|
plugins
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.mcp.json
|
||||||
|
.claude
|
||||||
|
CLAUDE.md
|
||||||
|
termix-plugin-signing.key
|
||||||
|
*.tmxplug.sig
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
@@ -29,63 +38,31 @@ Thumbs.db
|
|||||||
|
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
.husky
|
||||||
|
|
||||||
README.md
|
README.md
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
|
CHANGELOG.md
|
||||||
LICENSE
|
LICENSE
|
||||||
|
|
||||||
|
|
||||||
uploads/
|
|
||||||
|
|
||||||
electron/
|
electron/
|
||||||
electron-builder.json
|
electron-builder.json
|
||||||
|
Casks
|
||||||
|
packaging
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
logs
|
logs
|
||||||
*.log
|
|
||||||
|
|
||||||
pids
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
|
||||||
coverage
|
|
||||||
|
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
.npm
|
.npm
|
||||||
|
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
.cache
|
.cache
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
|
*.tgz
|
||||||
.next
|
.node_repl_history
|
||||||
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
.serverless
|
|
||||||
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
.tern-port
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.{js,cjs,mjs,jsx,ts,tsx,json,css,scss,md,yml,yaml}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
+2
-19
@@ -1,22 +1,5 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
*.js text eol=lf
|
|
||||||
*.cjs text eol=lf
|
|
||||||
*.mjs text eol=lf
|
|
||||||
*.jsx text eol=lf
|
|
||||||
*.ts text eol=lf
|
|
||||||
*.tsx text eol=lf
|
|
||||||
*.json text eol=lf
|
|
||||||
*.css text eol=lf
|
|
||||||
*.scss text eol=lf
|
|
||||||
*.html text eol=lf
|
|
||||||
*.md text eol=lf
|
|
||||||
*.yaml text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
|
|
||||||
*.sh text eol=lf
|
|
||||||
*.bash text eol=lf
|
|
||||||
|
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
*.cmd text eol=crlf
|
*.cmd text eol=crlf
|
||||||
*.ps1 text eol=crlf
|
*.ps1 text eol=crlf
|
||||||
@@ -25,10 +8,10 @@
|
|||||||
*.jpg binary
|
*.jpg binary
|
||||||
*.jpeg binary
|
*.jpeg binary
|
||||||
*.gif binary
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
*.ico binary
|
*.ico binary
|
||||||
*.svg binary
|
*.icns binary
|
||||||
*.woff binary
|
*.woff binary
|
||||||
*.woff2 binary
|
*.woff2 binary
|
||||||
*.ttf binary
|
*.ttf binary
|
||||||
*.eot binary
|
*.eot binary
|
||||||
*.icns binary
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
custom: https://donate.termix.site/
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Overview
|
|
||||||
|
|
||||||
_Short summary of what this PR does_
|
|
||||||
|
|
||||||
- [ ] Added: ...
|
|
||||||
- [ ] Updated: ...
|
|
||||||
- [ ] Removed: ...
|
|
||||||
- [ ] Fixed: ...
|
|
||||||
|
|
||||||
# Changes Made
|
|
||||||
|
|
||||||
_Detailed explanation of changes (if needed)_
|
|
||||||
|
|
||||||
- ...
|
|
||||||
|
|
||||||
# Related Issues
|
|
||||||
|
|
||||||
_Link any issues this PR addresses_
|
|
||||||
|
|
||||||
- Closes #ISSUE_NUMBER
|
|
||||||
- Related to #ISSUE_NUMBER
|
|
||||||
|
|
||||||
# Screenshots / Demos
|
|
||||||
|
|
||||||
_(Optional: add before/after screenshots, GIFs, or console output)_
|
|
||||||
|
|
||||||
# Checklist
|
|
||||||
|
|
||||||
- [ ] Code follows project style guidelines
|
|
||||||
- [ ] Supports mobile and desktop UI/app (if applicable)
|
|
||||||
- [ ] I have read [Contributing.md](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)
|
|
||||||
- [ ] This is not a translation request. See [docs](https://docs.termix.site/translations)
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: "Version to build (e.g., 1.8.0)"
|
description: "Version to build (e.g., 26.10.0)"
|
||||||
required: true
|
required: true
|
||||||
build_type:
|
build_type:
|
||||||
description: "Build type"
|
description: "Build type"
|
||||||
@@ -22,11 +22,11 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: "Version to build (e.g., 1.8.0)"
|
description: "Version to build (e.g., 26.10.0)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
build_type:
|
build_type:
|
||||||
description: "Build type (Development or Production)"
|
description: "Build type (Development, Beta or Production)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
dry_run:
|
dry_run:
|
||||||
@@ -52,7 +52,14 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Resolve source revision
|
- name: Resolve source revision
|
||||||
run: echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
run: |
|
||||||
|
echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||||
|
echo "BUILD_CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# The image reads its version from package.json, so a beta has to say so.
|
||||||
|
- name: Stamp beta version
|
||||||
|
if: ${{ inputs.build_type == 'Beta' }}
|
||||||
|
run: node scripts/sync-version.cjs --version "${{ inputs.version }}"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v4
|
||||||
@@ -74,7 +81,7 @@ jobs:
|
|||||||
ALL_TAGS=()
|
ALL_TAGS=()
|
||||||
|
|
||||||
if [ "$BUILD_TYPE" = "Production" ]; then
|
if [ "$BUILD_TYPE" = "Production" ]; then
|
||||||
TAGS+=("release-$VERSION" "$VERSION" "latest")
|
TAGS+=("$VERSION" "latest")
|
||||||
elif [ "$BUILD_TYPE" = "Beta" ]; then
|
elif [ "$BUILD_TYPE" = "Beta" ]; then
|
||||||
TAGS+=("beta" "$VERSION")
|
TAGS+=("beta" "$VERSION")
|
||||||
else
|
else
|
||||||
@@ -120,7 +127,7 @@ jobs:
|
|||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||||
org.opencontainers.image.revision=${{ env.SOURCE_SHA }}
|
org.opencontainers.image.revision=${{ env.SOURCE_SHA }}
|
||||||
org.opencontainers.image.created=${{ github.run_id }}
|
org.opencontainers.image.created=${{ env.BUILD_CREATED }}
|
||||||
outputs: ${{ inputs.dry_run && 'type=cacheonly' || 'type=registry,compression=zstd' }}
|
outputs: ${{ inputs.dry_run && 'type=cacheonly' || 'type=registry,compression=zstd' }}
|
||||||
|
|
||||||
- name: Login to Docker Hub as termixssh
|
- name: Login to Docker Hub as termixssh
|
||||||
|
|||||||
+190
-161
@@ -117,6 +117,20 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
||||||
|
# build only fetches them when the pins change.
|
||||||
|
- name: Cache bundled plugins
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/termix-bundled-plugins
|
||||||
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
||||||
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
|
# A beta build has to know it is a beta, or it reports the stable version.
|
||||||
|
- name: Stamp beta version
|
||||||
|
if: inputs.version_override != ''
|
||||||
|
run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}"
|
||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: package-version
|
id: package-version
|
||||||
run: |
|
run: |
|
||||||
@@ -129,7 +143,12 @@ jobs:
|
|||||||
- name: Build Windows (All Architectures)
|
- name: Build Windows (All Architectures)
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npm run build && npx electron-builder --win --x64 --ia32
|
run: npm run build && npx electron-builder --win --x64 --ia32 --arm64 --publish never
|
||||||
|
|
||||||
|
# The updater only installs the x64 NSIS build; 32-bit and ARM installs get a link.
|
||||||
|
# The combined multi-arch installer is never uploaded.
|
||||||
|
- name: Fix Windows update files
|
||||||
|
run: node scripts/fix-update-info.cjs --drop ia32 --drop arm64 --drop termix_windows_nsis.exe release/latest.yml release/beta.yml
|
||||||
|
|
||||||
- name: Upload Windows x64 NSIS Installer
|
- name: Upload Windows x64 NSIS Installer
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -147,6 +166,14 @@ jobs:
|
|||||||
path: release/termix_windows_ia32_nsis.exe
|
path: release/termix_windows_ia32_nsis.exe
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Windows arm64 NSIS Installer
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: hashFiles('release/termix_windows_arm64_nsis.exe') != '' && inputs.artifact_destination != 'none'
|
||||||
|
with:
|
||||||
|
name: termix_windows_arm64_nsis
|
||||||
|
path: release/termix_windows_arm64_nsis.exe
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
- name: Upload Windows x64 MSI Installer
|
- name: Upload Windows x64 MSI Installer
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
if: hashFiles('release/termix_windows_x64_msi.msi') != '' && inputs.artifact_destination != 'none'
|
if: hashFiles('release/termix_windows_x64_msi.msi') != '' && inputs.artifact_destination != 'none'
|
||||||
@@ -163,6 +190,14 @@ jobs:
|
|||||||
path: release/termix_windows_ia32_msi.msi
|
path: release/termix_windows_ia32_msi.msi
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Windows arm64 MSI Installer
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: hashFiles('release/termix_windows_arm64_msi.msi') != '' && inputs.artifact_destination != 'none'
|
||||||
|
with:
|
||||||
|
name: termix_windows_arm64_msi
|
||||||
|
path: release/termix_windows_arm64_msi.msi
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
- name: Create Windows x64 Portable zip
|
- name: Create Windows x64 Portable zip
|
||||||
if: hashFiles('release/win-unpacked/*') != ''
|
if: hashFiles('release/win-unpacked/*') != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -173,6 +208,22 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_portable.zip"
|
Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_portable.zip"
|
||||||
|
|
||||||
|
- name: Create Windows arm64 Portable zip
|
||||||
|
if: hashFiles('release/win-arm64-unpacked/*') != ''
|
||||||
|
run: |
|
||||||
|
Compress-Archive -Path "release\win-arm64-unpacked\*" -DestinationPath "termix_windows_arm64_portable.zip"
|
||||||
|
|
||||||
|
- name: Upload Windows update files
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: hashFiles('release/*.yml') != '' && inputs.artifact_destination != 'none'
|
||||||
|
with:
|
||||||
|
name: termix_windows_update_meta
|
||||||
|
path: |
|
||||||
|
release/*.yml
|
||||||
|
release/termix_windows_x64_nsis.exe.blockmap
|
||||||
|
!release/builder-debug.yml
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
- name: Upload Windows x64 Portable
|
- name: Upload Windows x64 Portable
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
if: hashFiles('termix_windows_x64_portable.zip') != '' && inputs.artifact_destination != 'none'
|
if: hashFiles('termix_windows_x64_portable.zip') != '' && inputs.artifact_destination != 'none'
|
||||||
@@ -189,6 +240,14 @@ jobs:
|
|||||||
path: termix_windows_ia32_portable.zip
|
path: termix_windows_ia32_portable.zip
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Windows arm64 Portable
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: hashFiles('termix_windows_arm64_portable.zip') != '' && inputs.artifact_destination != 'none'
|
||||||
|
with:
|
||||||
|
name: termix_windows_arm64_portable
|
||||||
|
path: termix_windows_arm64_portable.zip
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||||
if: (inputs.build_type == 'all' || inputs.build_type == 'linux' || inputs.build_type == '') && inputs.artifact_destination != 'submit'
|
if: (inputs.build_type == 'all' || inputs.build_type == 'linux' || inputs.build_type == '') && inputs.artifact_destination != 'submit'
|
||||||
@@ -220,16 +279,30 @@ jobs:
|
|||||||
npm install --force @rollup/rollup-linux-arm64-gnu
|
npm install --force @rollup/rollup-linux-arm64-gnu
|
||||||
npm install --force @rollup/rollup-linux-arm-gnueabihf
|
npm install --force @rollup/rollup-linux-arm-gnueabihf
|
||||||
|
|
||||||
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
||||||
|
# build only fetches them when the pins change.
|
||||||
|
- name: Cache bundled plugins
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/termix-bundled-plugins
|
||||||
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
||||||
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
|
# A beta build has to know it is a beta, or it reports the stable version.
|
||||||
|
- name: Stamp beta version
|
||||||
|
if: inputs.version_override != ''
|
||||||
|
run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}"
|
||||||
|
|
||||||
- name: Build Linux x64
|
- name: Build Linux x64
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
DEBUG: electron-builder
|
DEBUG: electron-builder
|
||||||
run: npm run build && npx electron-builder --linux --x64
|
run: npm run build && npx electron-builder --linux --x64 --publish never
|
||||||
|
|
||||||
- name: Build Linux arm64 and armv7l
|
- name: Build Linux arm64 and armv7l
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npx electron-builder --linux --arm64 --armv7l
|
run: npx electron-builder --linux --arm64 --armv7l --publish never
|
||||||
|
|
||||||
- name: Rename Linux artifacts for consistency
|
- name: Rename Linux artifacts for consistency
|
||||||
run: |
|
run: |
|
||||||
@@ -245,6 +318,19 @@ jobs:
|
|||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
node scripts/fix-update-info.cjs \
|
||||||
|
--rename termix_linux_amd64_deb.deb=termix_linux_x64_deb.deb \
|
||||||
|
--rename termix_linux_x86_64_appimage.AppImage=termix_linux_x64_appimage.AppImage \
|
||||||
|
release/*-linux*.yml
|
||||||
|
|
||||||
|
- name: Upload Linux update files
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: hashFiles('release/*-linux*.yml') != '' && inputs.artifact_destination != 'none'
|
||||||
|
with:
|
||||||
|
name: termix_linux_update_meta
|
||||||
|
path: release/*-linux*.yml
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
- name: Upload Linux x64 AppImage
|
- name: Upload Linux x64 AppImage
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && inputs.artifact_destination != 'none'
|
if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && inputs.artifact_destination != 'none'
|
||||||
@@ -429,6 +515,20 @@ jobs:
|
|||||||
npm install dmg-license
|
npm install dmg-license
|
||||||
node scripts/install-macos-sharp.cjs
|
node scripts/install-macos-sharp.cjs
|
||||||
|
|
||||||
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
||||||
|
# build only fetches them when the pins change.
|
||||||
|
- name: Cache bundled plugins
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/termix-bundled-plugins
|
||||||
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
||||||
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
|
# A beta build has to know it is a beta, or it reports the stable version.
|
||||||
|
- name: Stamp beta version
|
||||||
|
if: inputs.version_override != ''
|
||||||
|
run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}"
|
||||||
|
|
||||||
- name: Check for Code Signing Certificates
|
- name: Check for Code Signing Certificates
|
||||||
id: check_certs
|
id: check_certs
|
||||||
run: |
|
run: |
|
||||||
@@ -468,17 +568,18 @@ jobs:
|
|||||||
|
|
||||||
security find-identity -v -p codesigning $KEYCHAIN_PATH
|
security find-identity -v -p codesigning $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# Betas never go to the App Store, so they skip the MAS package.
|
||||||
- name: Build macOS App Store Package
|
- name: Build macOS App Store Package
|
||||||
if: steps.check_certs.outputs.has_certs == 'true'
|
id: mas_build
|
||||||
|
if: steps.check_certs.outputs.has_certs == 'true' && inputs.version_override == ''
|
||||||
env:
|
env:
|
||||||
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
|
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
||||||
BUILD_VERSION="${{ github.run_number }}"
|
BUILD_VERSION="${{ github.run_number }}"
|
||||||
|
|
||||||
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"
|
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" --publish never
|
||||||
|
|
||||||
- name: Clean up MAS keychain before DMG build
|
- name: Clean up MAS keychain before DMG build
|
||||||
if: steps.check_certs.outputs.has_certs == 'true'
|
if: steps.check_certs.outputs.has_certs == 'true'
|
||||||
@@ -534,10 +635,27 @@ jobs:
|
|||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then
|
if [ "${{ steps.mas_build.outcome }}" != "success" ]; then
|
||||||
npm run build
|
npm run build
|
||||||
fi
|
fi
|
||||||
npx electron-builder --mac dmg --universal --x64 --arm64 --publish never
|
npx electron-builder --mac dmg zip --universal --x64 --arm64 --publish never
|
||||||
|
|
||||||
|
# The updater picks the x64 or arm64 zip, so the universal one is left out.
|
||||||
|
- name: Fix macOS update files
|
||||||
|
run: node scripts/fix-update-info.cjs --drop universal release/*-mac.yml
|
||||||
|
|
||||||
|
- name: Upload macOS update files
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: hashFiles('release/*-mac.yml') != '' && inputs.artifact_destination != 'none'
|
||||||
|
with:
|
||||||
|
name: termix_macos_update_meta
|
||||||
|
path: |
|
||||||
|
release/*-mac.yml
|
||||||
|
release/termix_macos_x64_update.zip
|
||||||
|
release/termix_macos_x64_update.zip.blockmap
|
||||||
|
release/termix_macos_arm64_update.zip
|
||||||
|
release/termix_macos_arm64_update.zip.blockmap
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
- name: Verify macOS sharp packaging
|
- name: Verify macOS sharp packaging
|
||||||
run: |
|
run: |
|
||||||
@@ -555,7 +673,7 @@ jobs:
|
|||||||
echo "path=$MAS_PKG" >> "$GITHUB_OUTPUT"
|
echo "path=$MAS_PKG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload macOS MAS PKG
|
- name: Upload macOS MAS PKG
|
||||||
if: steps.check_certs.outputs.has_certs == 'true' && steps.locate_mas_pkg.outputs.path != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit')
|
if: steps.mas_build.outcome == 'success' && steps.locate_mas_pkg.outputs.path != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit')
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: termix_macos_universal_mas
|
name: termix_macos_universal_mas
|
||||||
@@ -722,7 +840,7 @@ jobs:
|
|||||||
choco pack termix-ssh.nuspec
|
choco pack termix-ssh.nuspec
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "Chocolatey push failed with exit code $LASTEXITCODE"
|
throw "Chocolatey pack failed with exit code $LASTEXITCODE"
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Check for Chocolatey API Key
|
- name: Check for Chocolatey API Key
|
||||||
@@ -755,144 +873,6 @@ jobs:
|
|||||||
path: choco-build/*.nupkg
|
path: choco-build/*.nupkg
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
submit-to-flatpak:
|
|
||||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
||||||
if: inputs.artifact_destination == 'submit' && (inputs.build_type == 'all' || inputs.build_type == 'linux' || inputs.build_type == '')
|
|
||||||
needs: []
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.source_ref || github.ref }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Get version from package.json
|
|
||||||
id: package-version
|
|
||||||
run: |
|
|
||||||
VERSION=$(node -p "require('./package.json').version")
|
|
||||||
RELEASE_DATE=$(date +%Y-%m-%d)
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download and prepare AppImage info from public release
|
|
||||||
id: appimage-info
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.package-version.outputs.version }}"
|
|
||||||
mkdir -p release_assets
|
|
||||||
|
|
||||||
APPIMAGE_X64_NAME="termix_linux_x64_appimage.AppImage"
|
|
||||||
URL_X64="https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$APPIMAGE_X64_NAME"
|
|
||||||
PATH_X64="release_assets/$APPIMAGE_X64_NAME"
|
|
||||||
echo "Downloading x64 AppImage from $URL_X64"
|
|
||||||
curl -L -o "$PATH_X64" "$URL_X64"
|
|
||||||
chmod +x "$PATH_X64"
|
|
||||||
CHECKSUM_X64=$(sha256sum "$PATH_X64" | awk '{print $1}')
|
|
||||||
|
|
||||||
APPIMAGE_ARM64_NAME="termix_linux_arm64_appimage.AppImage"
|
|
||||||
URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$APPIMAGE_ARM64_NAME"
|
|
||||||
PATH_ARM64="release_assets/$APPIMAGE_ARM64_NAME"
|
|
||||||
echo "Downloading arm64 AppImage from $URL_ARM64"
|
|
||||||
curl -L -o "$PATH_ARM64" "$URL_ARM64"
|
|
||||||
chmod +x "$PATH_ARM64"
|
|
||||||
CHECKSUM_ARM64=$(sha256sum "$PATH_ARM64" | awk '{print $1}')
|
|
||||||
|
|
||||||
echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT
|
|
||||||
echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT
|
|
||||||
echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT
|
|
||||||
echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y imagemagick
|
|
||||||
|
|
||||||
- name: Prepare Flatpak submission files
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.package-version.outputs.version }}"
|
|
||||||
CHECKSUM_X64="${{ steps.appimage-info.outputs.checksum_x64 }}"
|
|
||||||
CHECKSUM_ARM64="${{ steps.appimage-info.outputs.checksum_arm64 }}"
|
|
||||||
RELEASE_DATE="${{ steps.package-version.outputs.release_date }}"
|
|
||||||
|
|
||||||
mkdir -p flatpak-submission
|
|
||||||
|
|
||||||
cp packaging/flatpak/com.karmaa.termix.yml flatpak-submission/
|
|
||||||
cp packaging/flatpak/com.karmaa.termix.desktop flatpak-submission/
|
|
||||||
cp packaging/flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/
|
|
||||||
cp packaging/flatpak/flathub.json flatpak-submission/
|
|
||||||
|
|
||||||
cp public/icon.svg flatpak-submission/com.karmaa.termix.svg
|
|
||||||
convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png
|
|
||||||
convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml
|
|
||||||
sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml
|
|
||||||
sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml
|
|
||||||
sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml
|
|
||||||
|
|
||||||
- name: Upload Flatpak submission as artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: flatpak-submission
|
|
||||||
path: flatpak-submission/*
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Check for Flathub token
|
|
||||||
id: check_flathub_token
|
|
||||||
run: |
|
|
||||||
if [ -n "${{ secrets.FLATHUB_TOKEN }}" ]; then
|
|
||||||
echo "has_token=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Open PR on Flathub repo
|
|
||||||
if: steps.check_flathub_token.outputs.has_token == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.FLATHUB_TOKEN }}
|
|
||||||
VERSION: ${{ steps.package-version.outputs.version }}
|
|
||||||
run: |
|
|
||||||
FLATHUB_REPO="flathub/com.karmaa.termix"
|
|
||||||
BRANCH="release-$VERSION"
|
|
||||||
|
|
||||||
git config --global user.name "LukeGus"
|
|
||||||
git config --global user.email "bugattiguy527@gmail.com"
|
|
||||||
|
|
||||||
git clone "https://x-access-token:${GH_TOKEN}@github.com/${FLATHUB_REPO}.git" flathub-repo
|
|
||||||
cd flathub-repo
|
|
||||||
|
|
||||||
git checkout -b "$BRANCH"
|
|
||||||
|
|
||||||
cp ../flatpak-submission/com.karmaa.termix.yml ./
|
|
||||||
cp ../flatpak-submission/com.karmaa.termix.desktop ./
|
|
||||||
cp ../flatpak-submission/com.karmaa.termix.metainfo.xml ./
|
|
||||||
cp ../flatpak-submission/flathub.json ./
|
|
||||||
cp ../flatpak-submission/com.karmaa.termix.svg ./
|
|
||||||
cp ../flatpak-submission/icon-256.png ./
|
|
||||||
cp ../flatpak-submission/icon-128.png ./
|
|
||||||
|
|
||||||
if git diff --quiet && git diff --cached --quiet; then
|
|
||||||
echo "No changes to submit for $VERSION; Flathub repo already up to date."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
git add -A
|
|
||||||
git commit -m "Update to $VERSION"
|
|
||||||
git push origin "$BRANCH"
|
|
||||||
|
|
||||||
EXISTING_PR=$(gh pr list --repo "$FLATHUB_REPO" --head "$BRANCH" --state open --json number -q '.[0].number' || true)
|
|
||||||
if [ -z "$EXISTING_PR" ]; then
|
|
||||||
gh pr create --repo "$FLATHUB_REPO" \
|
|
||||||
--base master \
|
|
||||||
--head "$BRANCH" \
|
|
||||||
--title "Update to $VERSION" \
|
|
||||||
--body "Automated release update to version $VERSION."
|
|
||||||
else
|
|
||||||
echo "PR #$EXISTING_PR already open for $BRANCH."
|
|
||||||
fi
|
|
||||||
|
|
||||||
submit-to-homebrew:
|
submit-to-homebrew:
|
||||||
runs-on: blacksmith-6vcpu-macos-latest
|
runs-on: blacksmith-6vcpu-macos-latest
|
||||||
if: inputs.artifact_destination == 'submit' && (inputs.build_type == 'all' || inputs.build_type == 'macos')
|
if: inputs.artifact_destination == 'submit' && (inputs.build_type == 'all' || inputs.build_type == 'macos')
|
||||||
@@ -952,8 +932,8 @@ jobs:
|
|||||||
|
|
||||||
cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb
|
cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb
|
||||||
|
|
||||||
sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb
|
sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-submission/Casks/t/termix.rb
|
||||||
sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb
|
sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-submission/Casks/t/termix.rb
|
||||||
|
|
||||||
- name: Verify Cask syntax
|
- name: Verify Cask syntax
|
||||||
run: |
|
run: |
|
||||||
@@ -991,18 +971,15 @@ jobs:
|
|||||||
echo "RELEASE_TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName -q '.[0].tagName')" >> $GITHUB_ENV
|
echo "RELEASE_TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName -q '.[0].tagName')" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Update files go up last: the desktop updater treats a release as ready
|
||||||
|
# once its update file is there, so every installer must already be up.
|
||||||
- name: Upload artifacts to latest release
|
- name: Upload artifacts to latest release
|
||||||
run: |
|
run: |
|
||||||
cd artifacts
|
find artifacts -type f ! -name '*.yml' -print0 | while IFS= read -r -d '' file; do
|
||||||
for dir in */; do
|
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
|
||||||
cd "$dir"
|
done
|
||||||
for file in *;
|
find artifacts -type f -name '*.yml' -print0 | while IFS= read -r -d '' file; do
|
||||||
do
|
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
|
||||||
if [ -f "$file" ]; then
|
|
||||||
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
cd ..
|
|
||||||
done
|
done
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
@@ -1034,6 +1011,15 @@ jobs:
|
|||||||
npm install dmg-license
|
npm install dmg-license
|
||||||
node scripts/install-macos-sharp.cjs
|
node scripts/install-macos-sharp.cjs
|
||||||
|
|
||||||
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
||||||
|
# build only fetches them when the pins change.
|
||||||
|
- name: Cache bundled plugins
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/termix-bundled-plugins
|
||||||
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
||||||
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
- name: Check for Code Signing Certificates
|
- name: Check for Code Signing Certificates
|
||||||
id: check_certs
|
id: check_certs
|
||||||
run: |
|
run: |
|
||||||
@@ -1172,7 +1158,7 @@ jobs:
|
|||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
run: |
|
run: |
|
||||||
BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}"
|
BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}"
|
||||||
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"
|
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" --publish never
|
||||||
|
|
||||||
- name: Verify macOS sharp packaging
|
- name: Verify macOS sharp packaging
|
||||||
if: steps.check_certs.outputs.has_certs == 'true'
|
if: steps.check_certs.outputs.has_certs == 'true'
|
||||||
@@ -1196,6 +1182,49 @@ jobs:
|
|||||||
--locales "en-US"
|
--locales "en-US"
|
||||||
echo "metadata_path=$META_DIR" >> "$GITHUB_OUTPUT"
|
echo "metadata_path=$META_DIR" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Apple won't create a new version while the last one is still in
|
||||||
|
# review. Pull it out so deliver can rename it to this version.
|
||||||
|
- name: Cancel pending App Store review
|
||||||
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
||||||
|
run: |
|
||||||
|
LANE_DIR="$RUNNER_TEMP/asc_cancel/fastlane"
|
||||||
|
mkdir -p "$LANE_DIR"
|
||||||
|
|
||||||
|
cat > "$LANE_DIR/Fastfile" <<'EOF'
|
||||||
|
default_platform(:mac)
|
||||||
|
|
||||||
|
lane :cancel_pending_review do
|
||||||
|
Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.from_json_file("/tmp/asc_keys/api_key.json")
|
||||||
|
app = Spaceship::ConnectAPI::App.find("com.karmaa.termix")
|
||||||
|
UI.user_error!("App not found") unless app
|
||||||
|
|
||||||
|
app.get_app_store_versions(filter: { platform: "MAC_OS" }, limit: 5).each do |v|
|
||||||
|
UI.message("Version #{v.version_string}: #{v.app_version_state || v.app_store_state}")
|
||||||
|
end
|
||||||
|
|
||||||
|
active = %w[WAITING_FOR_REVIEW IN_REVIEW UNRESOLVED_ISSUES]
|
||||||
|
canceled = false
|
||||||
|
app.get_review_submissions(filter: { platform: "MAC_OS" }).each do |sub|
|
||||||
|
UI.message("Review submission #{sub.id}: #{sub.state}")
|
||||||
|
next unless active.include?(sub.state)
|
||||||
|
sub.cancel_submission
|
||||||
|
canceled = true
|
||||||
|
UI.important("Canceled review submission #{sub.id}")
|
||||||
|
end
|
||||||
|
next unless canceled
|
||||||
|
|
||||||
|
30.times do
|
||||||
|
busy = app.get_review_submissions(filter: { platform: "MAC_OS" }).any? do |sub|
|
||||||
|
(active + %w[CANCELING]).include?(sub.state)
|
||||||
|
end
|
||||||
|
break unless busy
|
||||||
|
sleep(10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cd "$RUNNER_TEMP/asc_cancel" && fastlane cancel_pending_review
|
||||||
|
|
||||||
- name: Upload and submit to Mac App Store
|
- name: Upload and submit to Mac App Store
|
||||||
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
||||||
|
# build only fetches them when the pins change.
|
||||||
|
- name: Cache bundled plugins
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/termix-bundled-plugins
|
||||||
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
||||||
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
- name: Build plugin SDK
|
- name: Build plugin SDK
|
||||||
run: npm run build:sdk
|
run: npm run build:sdk
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,6 @@ jobs:
|
|||||||
" "$VERSION"
|
" "$VERSION"
|
||||||
gh release create "sdk-v$VERSION" \
|
gh release create "sdk-v$VERSION" \
|
||||||
--target "${{ github.sha }}" \
|
--target "${{ github.sha }}" \
|
||||||
--title "Plugin SDK $VERSION" \
|
--title "Plugin SDK v$VERSION" \
|
||||||
--notes-file sdk-notes.md \
|
--notes-file sdk-notes.md \
|
||||||
--latest=false
|
--latest=false
|
||||||
+144
-80
@@ -161,15 +161,62 @@ jobs:
|
|||||||
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||||
echo "prev_ref=$PREV" >> "$GITHUB_OUTPUT"
|
echo "prev_ref=$PREV" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
verify-beta:
|
# Translates new or changed strings with the Termix LLM server and commits
|
||||||
|
# them to the dev branch, so the beta or stable build includes them. If the
|
||||||
|
# server is down it warns and the release goes on with what is there.
|
||||||
|
translate:
|
||||||
needs: [prep]
|
needs: [prep]
|
||||||
|
if: ${{ needs.prep.outputs.skip != 'true' && needs.prep.outputs.mode != 'Overwrite release' }}
|
||||||
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
|
outputs:
|
||||||
|
sha: ${{ steps.commit.outputs.sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout dev branch
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.prep.outputs.sha || needs.prep.outputs.dev_branch }}
|
||||||
|
fetch-depth: 1
|
||||||
|
token: ${{ secrets.TERMIX_PAT }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
|
- name: Translate
|
||||||
|
uses: Termix-SSH/Termix-Registry/translate@main
|
||||||
|
with:
|
||||||
|
source: src/ui/locales/en.json
|
||||||
|
out: src/ui/locales/translated
|
||||||
|
languages: src/ui/locales/languages.json
|
||||||
|
api-key: ${{ secrets.TERMIX_LLM_API_KEY }}
|
||||||
|
|
||||||
|
- name: Commit translations
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
git add src/ui/locales
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No translation changes."
|
||||||
|
elif [ "${{ needs.prep.outputs.mode }}" = "Dry run" ]; then
|
||||||
|
echo "DRY RUN: would commit these translation changes:"
|
||||||
|
git --no-pager diff --cached --stat
|
||||||
|
else
|
||||||
|
git config user.name "LukeGus"
|
||||||
|
git config user.email "bugattiguy527@gmail.com"
|
||||||
|
git commit -m "chore: update translations"
|
||||||
|
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
||||||
|
fi
|
||||||
|
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
verify-beta:
|
||||||
|
needs: [prep, translate]
|
||||||
if: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.skip != 'true' }}
|
if: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.skip != 'true' }}
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout beta commit
|
- name: Checkout beta commit
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.prep.outputs.sha }}
|
ref: ${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -181,6 +228,15 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
||||||
|
# build only fetches them when the pins change.
|
||||||
|
- name: Cache bundled plugins
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/termix-bundled-plugins
|
||||||
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
||||||
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
- name: Build plugin SDK
|
- name: Build plugin SDK
|
||||||
run: npm run build:sdk
|
run: npm run build:sdk
|
||||||
|
|
||||||
@@ -197,7 +253,7 @@ jobs:
|
|||||||
run: npm run test
|
run: npm run test
|
||||||
|
|
||||||
normalize:
|
normalize:
|
||||||
needs: [prep]
|
needs: [prep, translate]
|
||||||
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Overwrite release' }}
|
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Overwrite release' }}
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
@@ -253,7 +309,7 @@ jobs:
|
|||||||
git commit -m "chore: lint, format, and bump version to ${{ needs.prep.outputs.version }}"
|
git commit -m "chore: lint, format, and bump version to ${{ needs.prep.outputs.version }}"
|
||||||
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
||||||
|
|
||||||
crowdin:
|
sync-main:
|
||||||
needs: [prep, normalize]
|
needs: [prep, normalize]
|
||||||
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Overwrite release' }}
|
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Overwrite release' }}
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
@@ -265,79 +321,27 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.TERMIX_PAT }}
|
token: ${{ secrets.TERMIX_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
|
|
||||||
- name: Merge main into dev branch
|
- name: Merge main into dev branch
|
||||||
run: |
|
run: |
|
||||||
git config user.name "LukeGus"
|
git config user.name "LukeGus"
|
||||||
git config user.email "bugattiguy527@gmail.com"
|
git config user.email "bugattiguy527@gmail.com"
|
||||||
git fetch origin main
|
git fetch origin main
|
||||||
git merge origin/main -X ours --no-edit || true
|
# -X ours leaves modify/delete conflicts, so settle those to the dev side too
|
||||||
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
if ! git merge origin/main -X ours --no-edit; then
|
||||||
|
git diff --name-only --diff-filter=U | while read -r f; do
|
||||||
- name: Clean up stale Crowdin git integration branch
|
if git cat-file -e "HEAD:$f" 2>/dev/null; then
|
||||||
continue-on-error: true
|
git checkout --ours -- "$f" && git add -- "$f"
|
||||||
env:
|
else
|
||||||
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
git rm -q -- "$f"
|
||||||
run: |
|
fi
|
||||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head i18n_translate --state open --json number -q '.[0].number' || true)
|
done
|
||||||
if [ -n "$PR_NUMBER" ]; then
|
git commit --no-edit
|
||||||
gh pr close "$PR_NUMBER" --repo ${{ github.repository }} --delete-branch || true
|
|
||||||
fi
|
fi
|
||||||
git push origin --delete i18n_translate || true
|
|
||||||
|
|
||||||
- name: Upload sources to Crowdin
|
|
||||||
uses: crowdin/github-action@v3
|
|
||||||
with:
|
|
||||||
upload_sources: true
|
|
||||||
upload_translations: false
|
|
||||||
download_translations: false
|
|
||||||
create_pull_request: false
|
|
||||||
push_translations: false
|
|
||||||
token: ${{ secrets.CROWDIN_API_KEY }}
|
|
||||||
project_id: "858252"
|
|
||||||
env:
|
|
||||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }}
|
|
||||||
|
|
||||||
# Off until the machine translation credits are topped up.
|
|
||||||
- name: Pre-translate untranslated strings
|
|
||||||
if: false
|
|
||||||
env:
|
|
||||||
CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }}
|
|
||||||
run: node scripts/crowdin-pretranslate.cjs
|
|
||||||
|
|
||||||
- name: Download translations from Crowdin
|
|
||||||
uses: crowdin/github-action@v3
|
|
||||||
with:
|
|
||||||
upload_sources: false
|
|
||||||
upload_translations: false
|
|
||||||
download_translations: true
|
|
||||||
create_pull_request: false
|
|
||||||
push_translations: false
|
|
||||||
token: ${{ secrets.CROWDIN_API_KEY }}
|
|
||||||
project_id: "858252"
|
|
||||||
env:
|
|
||||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }}
|
|
||||||
|
|
||||||
- name: Commit translations to dev branch
|
|
||||||
run: |
|
|
||||||
git config user.name "LukeGus"
|
|
||||||
git config user.email "bugattiguy527@gmail.com"
|
|
||||||
|
|
||||||
git add src/ui/locales/translated
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No translation changes to commit."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
git commit -m "chore: sync Crowdin translations for ${{ needs.prep.outputs.version }}"
|
|
||||||
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
||||||
|
|
||||||
merge-to-main:
|
merge-to-main:
|
||||||
needs: [prep, normalize, crowdin]
|
needs: [prep, translate, normalize, sync-main]
|
||||||
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.channel == 'Stable' && (needs.normalize.result == 'success' || needs.normalize.result == 'skipped') && (needs.crowdin.result == 'success' || needs.crowdin.result == 'skipped') }}
|
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.channel == 'Stable' && (needs.translate.result == 'success' || needs.translate.result == 'skipped') && (needs.normalize.result == 'success' || needs.normalize.result == 'skipped') && (needs.sync-main.result == 'success' || needs.sync-main.result == 'skipped') }}
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
outputs:
|
outputs:
|
||||||
main_sha: ${{ steps.merge.outputs.main_sha }}
|
main_sha: ${{ steps.merge.outputs.main_sha }}
|
||||||
@@ -405,14 +409,14 @@ jobs:
|
|||||||
echo "build_ref=$MAIN_SHA" >> "$GITHUB_OUTPUT"
|
echo "build_ref=$MAIN_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
needs: [prep, verify-beta, merge-to-main]
|
needs: [prep, translate, verify-beta, merge-to-main]
|
||||||
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.skip != 'true' && (needs.merge-to-main.result == 'success' || needs.verify-beta.result == 'success') }}
|
if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.skip != 'true' && (needs.merge-to-main.result == 'success' || needs.verify-beta.result == 'success') }}
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.prep.outputs.version }}
|
version: ${{ needs.prep.outputs.version }}
|
||||||
build_type: ${{ needs.prep.outputs.channel == 'Beta' && 'Beta' || 'Production' }}
|
build_type: ${{ needs.prep.outputs.channel == 'Beta' && 'Beta' || 'Production' }}
|
||||||
dry_run: ${{ needs.prep.outputs.mode == 'Dry run' }}
|
dry_run: ${{ needs.prep.outputs.mode == 'Dry run' }}
|
||||||
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.prep.outputs.sha }}
|
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.translate.outputs.sha || needs.prep.outputs.sha }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
@@ -462,7 +466,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ needs.prep.outputs.release_tag }}"
|
TAG="${{ needs.prep.outputs.release_tag }}"
|
||||||
TITLE="v${{ needs.prep.outputs.version }}"
|
TITLE="Termix v${{ needs.prep.outputs.version }}"
|
||||||
if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then
|
if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then
|
||||||
gh release edit "$TAG" --repo ${{ github.repository }} --title "$TITLE" --notes-file RELEASE_BODY.md
|
gh release edit "$TAG" --repo ${{ github.repository }} --title "$TITLE" --notes-file RELEASE_BODY.md
|
||||||
else
|
else
|
||||||
@@ -470,7 +474,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
create-beta-release:
|
create-beta-release:
|
||||||
needs: [prep, docker]
|
needs: [prep, translate, docker]
|
||||||
if: ${{ always() && needs.prep.outputs.channel == 'Beta' && needs.docker.result == 'success' && needs.prep.outputs.mode != 'Dry run' }}
|
if: ${{ always() && needs.prep.outputs.channel == 'Beta' && needs.docker.result == 'success' && needs.prep.outputs.mode != 'Dry run' }}
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
@@ -479,7 +483,7 @@ jobs:
|
|||||||
- name: Checkout beta commit
|
- name: Checkout beta commit
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.prep.outputs.sha }}
|
ref: ${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -492,7 +496,7 @@ jobs:
|
|||||||
node scripts/generate-beta-body.cjs \
|
node scripts/generate-beta-body.cjs \
|
||||||
--version "${{ needs.prep.outputs.version }}" \
|
--version "${{ needs.prep.outputs.version }}" \
|
||||||
--branch "${{ needs.prep.outputs.dev_branch }}" \
|
--branch "${{ needs.prep.outputs.dev_branch }}" \
|
||||||
--sha "${{ needs.prep.outputs.sha }}" \
|
--sha "${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}" \
|
||||||
--prev "${{ needs.prep.outputs.prev_ref }}" > BETA_BODY.md
|
--prev "${{ needs.prep.outputs.prev_ref }}" > BETA_BODY.md
|
||||||
|
|
||||||
- name: Create beta release
|
- name: Create beta release
|
||||||
@@ -500,7 +504,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.prep.outputs.version }}"
|
VERSION="${{ needs.prep.outputs.version }}"
|
||||||
TITLE="Termix ${VERSION%-beta.*} Beta ${VERSION##*-beta.}"
|
TITLE="Termix v${VERSION%-beta.*} Beta ${VERSION##*-beta.}"
|
||||||
TAG="${{ needs.prep.outputs.release_tag }}"
|
TAG="${{ needs.prep.outputs.release_tag }}"
|
||||||
if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then
|
if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then
|
||||||
gh release edit "$TAG" --repo ${{ github.repository }} \
|
gh release edit "$TAG" --repo ${{ github.repository }} \
|
||||||
@@ -508,11 +512,19 @@ jobs:
|
|||||||
else
|
else
|
||||||
gh release create "$TAG" --repo ${{ github.repository }} \
|
gh release create "$TAG" --repo ${{ github.repository }} \
|
||||||
--title "$TITLE" --notes-file BETA_BODY.md \
|
--title "$TITLE" --notes-file BETA_BODY.md \
|
||||||
--prerelease --target "${{ needs.prep.outputs.sha }}"
|
--prerelease --target "${{ needs.translate.outputs.sha || needs.prep.outputs.sha }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
electron-release:
|
electron-release:
|
||||||
needs: [prep, merge-to-main, docker, create-release, create-beta-release]
|
needs:
|
||||||
|
[
|
||||||
|
prep,
|
||||||
|
translate,
|
||||||
|
merge-to-main,
|
||||||
|
docker,
|
||||||
|
create-release,
|
||||||
|
create-beta-release,
|
||||||
|
]
|
||||||
if: ${{ always() && needs.prep.outputs.skip != 'true' && needs.docker.result == 'success' && needs.create-release.result != 'failure' && needs.create-beta-release.result != 'failure' }}
|
if: ${{ always() && needs.prep.outputs.skip != 'true' && needs.docker.result == 'success' && needs.create-release.result != 'failure' && needs.create-beta-release.result != 'failure' }}
|
||||||
uses: ./.github/workflows/electron.yml
|
uses: ./.github/workflows/electron.yml
|
||||||
with:
|
with:
|
||||||
@@ -520,12 +532,12 @@ jobs:
|
|||||||
artifact_destination: ${{ needs.prep.outputs.mode == 'Dry run' && 'file' || 'release' }}
|
artifact_destination: ${{ needs.prep.outputs.mode == 'Dry run' && 'file' || 'release' }}
|
||||||
release_tag: ${{ needs.prep.outputs.release_tag }}
|
release_tag: ${{ needs.prep.outputs.release_tag }}
|
||||||
version_override: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.version || '' }}
|
version_override: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.version || '' }}
|
||||||
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.prep.outputs.sha }}
|
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.translate.outputs.sha || needs.prep.outputs.sha }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
electron-submit:
|
electron-submit:
|
||||||
needs: [prep, merge-to-main, electron-release]
|
needs: [prep, merge-to-main, electron-release]
|
||||||
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Skip submit' }}
|
if: ${{ always() && needs.electron-release.result == 'success' && needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Skip submit' }}
|
||||||
uses: ./.github/workflows/electron.yml
|
uses: ./.github/workflows/electron.yml
|
||||||
with:
|
with:
|
||||||
build_type: all
|
build_type: all
|
||||||
@@ -535,7 +547,7 @@ jobs:
|
|||||||
|
|
||||||
cask-commit-back:
|
cask-commit-back:
|
||||||
needs: [prep, electron-release]
|
needs: [prep, electron-release]
|
||||||
if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' }}
|
if: ${{ always() && needs.electron-release.result == 'success' && needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' }}
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -591,6 +603,58 @@ jobs:
|
|||||||
gh release delete "$TAG" --repo ${{ github.repository }} --yes || true
|
gh release delete "$TAG" --repo ${{ github.repository }} --yes || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# The docs site reads the env catalog, API spec and download list from the
|
||||||
|
# newest stable release.
|
||||||
|
# Docs written for this release live on dev-X.Y.Z in the Docs repo. Squash
|
||||||
|
# merge it into main once the release is out. No branch means nothing to do.
|
||||||
|
docs-merge:
|
||||||
|
needs: [prep, electron-release]
|
||||||
|
if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.electron-release.result == 'success' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Overwrite release' }}
|
||||||
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
|
steps:
|
||||||
|
- name: Merge the docs dev branch into main
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
||||||
|
REPO: Termix-SSH/Docs
|
||||||
|
VERSION: ${{ needs.prep.outputs.version }}
|
||||||
|
run: |
|
||||||
|
BRANCH="dev-$VERSION"
|
||||||
|
if ! gh api "repos/$REPO/branches/$BRANCH" > /dev/null 2>&1; then
|
||||||
|
echo "$REPO has no $BRANCH branch, nothing to merge."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
AHEAD=$(gh api "repos/$REPO/compare/main...$BRANCH" -q .ahead_by)
|
||||||
|
if [ "$AHEAD" = "0" ]; then
|
||||||
|
echo "$BRANCH has nothing main doesn't, deleting it."
|
||||||
|
gh api -X DELETE "repos/$REPO/git/refs/heads/$BRANCH"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PR_NUMBER=$(gh pr list --repo "$REPO" --head "$BRANCH" --base main --state open --json number -q '.[0].number')
|
||||||
|
if [ -z "$PR_NUMBER" ]; then
|
||||||
|
PR_NUMBER=$(gh pr create --repo "$REPO" --base main --head "$BRANCH" \
|
||||||
|
--title "v$VERSION" --body "Docs for Termix v$VERSION." | grep -oE '[0-9]+$')
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh pr merge "$PR_NUMBER" --repo "$REPO" --squash --admin --delete-branch
|
||||||
|
|
||||||
|
STATE=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state -q .state)
|
||||||
|
if [ "$STATE" != "MERGED" ]; then
|
||||||
|
echo "Docs PR #$PR_NUMBER did not merge (state: $STATE)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docs-refresh:
|
||||||
|
needs: [prep, electron-release, docs-merge]
|
||||||
|
if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.electron-release.result == 'success' && needs.prep.outputs.mode != 'Dry run' }}
|
||||||
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
|
steps:
|
||||||
|
- name: Tell the docs site
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.TERMIX_PAT }}
|
||||||
|
run: gh api repos/Termix-SSH/Docs/dispatches -f event_type=core-released
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
needs: [prep, merge-to-main, electron-release, cask-commit-back]
|
needs: [prep, merge-to-main, electron-release, cask-commit-back]
|
||||||
if: ${{ always() && needs.prep.outputs.channel == 'Stable' && (needs.prep.outputs.mode == 'Everything' || needs.prep.outputs.mode == 'Skip submit') && needs.merge-to-main.result == 'success' }}
|
if: ${{ always() && needs.prep.outputs.channel == 'Stable' && (needs.prep.outputs.mode == 'Everything' || needs.prep.outputs.mode == 'Skip submit') && needs.merge-to-main.result == 'success' }}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Translate
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
retranslate:
|
||||||
|
description: "Translate every string again, not only new or changed ones"
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: translate-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
translate:
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/heads/dev-') }}
|
||||||
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
token: ${{ secrets.TERMIX_PAT }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
|
- name: Translate
|
||||||
|
uses: Termix-SSH/Termix-Registry/translate@main
|
||||||
|
with:
|
||||||
|
source: src/ui/locales/en.json
|
||||||
|
out: src/ui/locales/translated
|
||||||
|
languages: src/ui/locales/languages.json
|
||||||
|
retranslate: ${{ inputs.retranslate }}
|
||||||
|
api-key: ${{ secrets.TERMIX_LLM_API_KEY }}
|
||||||
|
|
||||||
|
- name: Commit translations
|
||||||
|
run: |
|
||||||
|
git add src/ui/locales
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No translation changes."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git config user.name "LukeGus"
|
||||||
|
git config user.email "bugattiguy527@gmail.com"
|
||||||
|
git commit -m "chore: update translations"
|
||||||
|
git push origin HEAD:"${GITHUB_REF_NAME}"
|
||||||
+3
-3
@@ -20,10 +20,10 @@ coverage
|
|||||||
termix-plugin-signing.key
|
termix-plugin-signing.key
|
||||||
/src/**/*.js
|
/src/**/*.js
|
||||||
|
|
||||||
.vscode/*
|
.vscode/
|
||||||
!.vscode/settings.json
|
.idea/
|
||||||
.idea
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
|
|||||||
+3
-13
@@ -1,24 +1,14 @@
|
|||||||
build
|
build
|
||||||
coverage
|
coverage
|
||||||
dist
|
dist
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
dist-ssr
|
dist-ssr
|
||||||
release
|
release
|
||||||
|
|
||||||
node_modules
|
|
||||||
src/mcp-server/node_modules
|
|
||||||
package-lock.json
|
|
||||||
pnpm-lock.yaml
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
db
|
db
|
||||||
|
|
||||||
.env
|
|
||||||
|
|
||||||
*.min.js
|
*.min.js
|
||||||
*.min.css
|
*.min.css
|
||||||
openapi.json
|
openapi.json
|
||||||
|
|
||||||
# Generated by drizzle-kit; formatting is the tool's own
|
|
||||||
drizzle/
|
drizzle/
|
||||||
|
|
||||||
src/ui/settings/settings-search-index.ts
|
src/ui/settings/settings-search-index.ts
|
||||||
Vendored
-22
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"prettier.prettierPath": "./node_modules/prettier",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit"
|
|
||||||
},
|
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact"
|
|
||||||
],
|
|
||||||
"files.eol": "\n",
|
|
||||||
"files.insertFinalNewline": true,
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"[markdown]": {
|
|
||||||
"files.trimTrailingWhitespace": false
|
|
||||||
},
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
|
||||||
}
|
|
||||||
+100
-2
@@ -1,15 +1,113 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 26.10.2
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Hide sidebar after opening, in Customize Sidebar, closes the host list once you open a host
|
||||||
|
- Sort the dashboard's Host Status card by name, IP address or status
|
||||||
|
- Windows on ARM builds of the desktop app (installer, MSI and portable)
|
||||||
|
- Plugins get an event when a host is added
|
||||||
|
- Plugins can start a short-lived session as a user and read the user's language, for tasks that run without a browser
|
||||||
|
- Plugins can pin a panel to the right edge of the window that pushes the page over
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The Docker Compose files no longer start a guacd container. npm run dev -- --guacd also points remote desktop at the guacd it starts
|
||||||
|
- The side menu now opens over the page when it expands on hover, so open sessions like remote desktop are not resized each time. Turn on Push content when it expands in Settings to get the old behavior back
|
||||||
|
- On macOS, moving between split panes is now Cmd+Option+Arrow, so Option+Arrow can move the cursor by word in a terminal
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Picking a section lower down in the host editor, like Warpgate, stuttered on the way and sometimes stopped before it got there
|
||||||
|
- Sorting hosts by IP address put 10.0.0.210 before 10.0.0.3
|
||||||
|
- The local terminal failed to start in the universal macOS build
|
||||||
|
- Hosts shared with you never showed up on a linked desktop
|
||||||
|
- A linked desktop could never turn on an unsigned plugin the server had enabled
|
||||||
|
- Creating, cloning or deleting hosts failed with "no such table: main.p_vault_profiles" after the Vault plugin was removed
|
||||||
|
- Opening a user in Settings, Users blocked the other admin sections until you went back to the user list
|
||||||
|
- A linked desktop could keep showing the same sync conflicts, or delete a local host, when the server could not send its copy
|
||||||
|
- Usernames are now unique
|
||||||
|
- Importing user data keeps foreign keys on
|
||||||
|
- Trusted proxy login checks the address nginx saw, not nginx itself
|
||||||
|
- The desktop login frame only trusts the server's own origin
|
||||||
|
- External login URLs and callback ports are checked on the desktop app
|
||||||
|
- A changed jump host key is no longer accepted without asking
|
||||||
|
- Synced plugins signed only by a key the server sent are no longer turned on by themselves
|
||||||
|
- Two connects to the same host at once could overwrite each other's connection
|
||||||
|
- Indonesian uses the same technical terms everywhere
|
||||||
|
- When the server was down or still starting, the login page said it could not connect to the database and stopped trying. It now says the server is not responding and continues on its own once the server is back
|
||||||
|
- nginx answered 404 instead of 502 while the backend was down
|
||||||
|
- A client tunnel that could not start always said "Endpoint SSH connection closed" on the desktop app instead of the real reason, like the target refusing the connection
|
||||||
|
- The Docker server crashed and restarted every few hours in ssh2's native crypto on Node 24
|
||||||
|
- Plugin names and descriptions showed in English in the plugin manager, setup picker and host editor no matter the UI language
|
||||||
|
- Creating a host through the API without an auth type failed with a server error. It now picks the type from the credential, key or password, or says auth type is required
|
||||||
|
- The desktop app would not start its local server when another program used port 30001. It now picks a free port on its own
|
||||||
|
- A linked desktop could never finish syncing with a server on Postgres
|
||||||
|
- Routine plugin activity filled the audit log and pushed out real security entries
|
||||||
|
- Opening a full-screen view link while signed out showed a spinner forever. It now shows the login and opens the link after
|
||||||
|
- A linked desktop behind a proxy that needs extra headers, or with an allowed self-signed certificate, could not open remote terminals or tunnels
|
||||||
|
|
||||||
|
## 26.10.1
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Slovak, Croatian, Slovenian, Lithuanian, Latvian, Estonian and Malay
|
||||||
|
- The desktop app can update itself. Pick Automatic, Ask first or Never in Settings, Updates. Store and package manager installs keep updating the way they do now
|
||||||
|
- Community plugins. Turn on the community registry in Plugins, Registries to browse and install plugins made by other people, each version reviewed before it is listed
|
||||||
|
- Custom plugin registries. Add your own registry by its index URL; its signing key is pinned when you add it
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Every language is now fully translated, in Termix and in every plugin, and stays up to date with each release
|
||||||
|
- Languages with more than two plural forms, like Russian, Polish and Arabic, now use the right one
|
||||||
|
- The Plugins tab shows how many times each plugin was downloaded, in a Downloads column
|
||||||
|
- Plugins, Browse hides plugins you already have. Turn on Show installed to see them
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Regional browser languages like `es-MX`, `pt` or `zh-HK` showed English instead of the closest translation
|
||||||
|
- Counts like "1 keys" or "Moved 1 host(s)" now use the singular
|
||||||
|
- Plugin updates on the desktop app, synced or installed from the Plugins tab, failed to start with "Cannot find package"
|
||||||
|
- The desktop app showed plugins that only run on the server as not running and out of date
|
||||||
|
- Keep the terminal WebSocket open past nginx's 60s default
|
||||||
|
- Plugin download counts now show in the Plugins tab without waiting for the background check
|
||||||
|
- Clicking into a saved password, key passphrase or sudo password field and saving without typing erased it, which stopped sudo autofill
|
||||||
|
- The desktop app now says when its data is from 2.8 or older and needs 2.9 first, instead of only saying the local server could not start
|
||||||
|
- Client tunnels on the desktop app stopped with "Endpoint SSH connection closed" when a reverse proxy dropped their idle connection
|
||||||
|
- Typing right after opening a host from the sidebar went to the host search instead of the new session
|
||||||
|
- SSH config import failed on files that put a tab or `=` between a keyword and its value
|
||||||
|
- App icons and the favicon have rounded corners again
|
||||||
|
|
||||||
## 26.10.0
|
## 26.10.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Temp
|
- Plugins. Every feature beyond hosts, credentials and sharing is now its own plugin with its own repo, and you pick which ones you want
|
||||||
|
- A Plugins tab to install, update, pin, turn off and remove plugins from the official registry, with release notes and a beta channel per plugin
|
||||||
|
- Developer mode to install a plugin from a file, and a plugin SDK on npm to build your own
|
||||||
|
- The community plugin registry is open for submissions. Termix can install community plugins in a later update
|
||||||
|
- Redesigned UI with a Manage tab for hosts and credentials and a Settings tab with search
|
||||||
|
- New full screen onboarding where you choose which plugins to start with
|
||||||
|
- Beta channel for Termix itself in Settings > Updates
|
||||||
|
- Docs links on every settings page, in the Plugins tab, in plugin settings and in the command palette
|
||||||
|
- Plugins can link their own docs and list the environment variables they read, shown on their page in the Plugins tab
|
||||||
|
- Report a bug links that open a prefilled issue in the right repo
|
||||||
|
- `TRUSTED_PROXIES` to choose which proxies nginx takes client IPs from
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Tem
|
- Termix now uses calendar versions (year, month, patch), starting with 26.10.0
|
||||||
|
- Redesigned host defaults with admin, user and folder levels
|
||||||
|
- Simpler host status: online or offline, kept live without a refresh
|
||||||
|
- Bug reports and feature requests now go to each repo instead of the Support repo
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A lot of bugs across the app, the desktop app and every plugin
|
||||||
|
- Several security issues reported through GitHub advisories
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Downgrading to 2.8 is no longer supported. A 2.8 install has to start 2.9 once before upgrading
|
- Downgrading to 2.8 is no longer supported. A 2.8 install has to start 2.9 once before upgrading
|
||||||
|
- The donation reminder popup
|
||||||
+9
-9
@@ -25,7 +25,7 @@ Termix is a small core plus plugins. This repo is the core: hosts, credentials,
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds everything, starts the backend and then the Vite server at `http://localhost:5173/`. Backend changes restart the backend, and plugin changes in `../Termix-Plugins` rebuild and reload that plugin. `npm run dev:electron` does the same with the desktop app, and `npm run dev -- --help` lists the other options.
|
This builds everything, starts the backend and then the Vite server at `http://localhost:5173/`. Backend changes restart the backend, and plugin changes in `../Plugins` rebuild and reload that plugin. `npm run dev:electron` does the same with the desktop app, and `npm run dev -- --help` lists the other options.
|
||||||
|
|
||||||
## Useful commands
|
## Useful commands
|
||||||
|
|
||||||
@@ -38,26 +38,26 @@ npm run format # format the code with Prettier
|
|||||||
|
|
||||||
## Making a change
|
## Making a change
|
||||||
|
|
||||||
1. **Fork the repository**: Click "Fork" at the top right of the [repository page](https://github.com/Termix-SSH/Termix).
|
1. Fork the [repo](https://github.com/Termix-SSH/Termix).
|
||||||
2. **Create a branch**:
|
2. Create a branch:
|
||||||
```sh
|
```sh
|
||||||
git checkout -b feature/my-new-feature
|
git checkout -b feature/my-new-feature
|
||||||
```
|
```
|
||||||
3. **Make your changes** and add or update tests for them.
|
3. Make your changes and add or update tests for them.
|
||||||
4. **Commit your changes**:
|
4. Commit your changes:
|
||||||
```sh
|
```sh
|
||||||
git commit -m "feat: add my new feature"
|
git commit -m "feat: add my new feature"
|
||||||
```
|
```
|
||||||
5. **Push to your fork**:
|
5. Push to your fork:
|
||||||
```sh
|
```sh
|
||||||
git push origin feature/my-new-feature
|
git push origin feature/my-new-feature
|
||||||
```
|
```
|
||||||
6. **Open a pull request** against the current `dev-*` branch with a clear description.
|
6. Open a pull request against the current `dev-*` branch with a clear description.
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Follow the existing code style. The frontend uses React, Tailwind CSS and shadcn components in `src/ui/`.
|
- Follow the existing code style. The frontend uses React, Tailwind CSS and shadcn components in `src/ui/`.
|
||||||
- Never hardcode text. Use `t()` and add the English string to `src/ui/locales/en.json`. Other languages are translated on [Crowdin](https://docs.termix.site/translations).
|
- Never hardcode text. Use `t()` and add the English string to `src/ui/locales/en.json`. That's the only locale file you need to update. Other languages are translated automatically, and if an automatic translation is wrong you can fix it in that language's file.
|
||||||
- Keep the UI flat and square. Avoid rounded corners on panels, cards and dialogs.
|
- Keep the UI flat and square. Avoid rounded corners on panels, cards and dialogs.
|
||||||
- Use the theme variables (like `var(--bg-base)` and `var(--text-primary)`) instead of fixed colors.
|
- Use the theme variables (like `var(--bg-base)` and `var(--text-primary)`) instead of fixed colors.
|
||||||
- There is one component tree for desktop and mobile. Use `src/ui/hooks/use-mobile.ts` for layout changes.
|
- There is one component tree for desktop and mobile. Use `src/ui/hooks/use-mobile.ts` for layout changes.
|
||||||
@@ -72,4 +72,4 @@ Start from the [plugin template](https://github.com/Termix-SSH/Termix-Plugin-Tem
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Bugs and ideas for Termix itself go in [this repo](https://github.com/Termix-SSH/Termix/issues/new/choose). Plugin problems go in that plugin's own `Plugin-<Name>` repo. Please be as detailed as possible, preferably in English. You can also ask in the [Discord](https://discord.gg/jVQGdvHDrf) server.
|
Bugs and ideas for Termix itself go in [this repo](https://github.com/Termix-SSH/Termix/issues/new/choose). Plugin problems go in that plugin's own `Plugin-<Name>` repo. Add as much detail as you can, in English if possible. You can also ask in the [Discord](https://discord.gg/jVQGdvHDrf) server.
|
||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
cask "termix" do
|
cask "termix" do
|
||||||
version "2.9.1"
|
version "26.10.1"
|
||||||
sha256 "6d8847e18508314db8ed4380c63bf0dd31437102a288b8e861879c92ba25a254"
|
sha256 "08a8fdc6ce3f29bd9031b050a4f6364f3ca446f17cb272392f1b3b488cea5d30"
|
||||||
|
|
||||||
url "https://github.com/Termix-SSH/Termix/releases/download/v#{version}/termix_macos_universal_dmg.dmg"
|
url "https://github.com/Termix-SSH/Termix/releases/download/v#{version}/termix_macos_universal_dmg.dmg"
|
||||||
name "Termix"
|
name "Termix"
|
||||||
@@ -12,6 +12,8 @@ cask "termix" do
|
|||||||
strategy :github_latest
|
strategy :github_latest
|
||||||
end
|
end
|
||||||
|
|
||||||
|
auto_updates true
|
||||||
|
|
||||||
app "Termix.app"
|
app "Termix.app"
|
||||||
|
|
||||||
zap trash: [
|
zap trash: [
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"drips": {
|
|
||||||
"ethereum": {
|
|
||||||
"ownedBy": "0x67e0C779119D9BcC2187564A66B80a58767d05d1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,203 @@
|
|||||||
Copyright 2025 Luke Gustafson
|
Copyright 2026 Luke Gustafson
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Apache License
|
||||||
you may not use this file except in compliance with the License.
|
Version 2.0, January 2004
|
||||||
You may obtain a copy of the License at
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
1. Definitions.
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
See the License for the specific language governing permissions and
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
limitations under the License.
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -18,36 +18,38 @@
|
|||||||
<a href="https://donate.termix.site/"><img alt="Donations this month" src="https://img.shields.io/badge/dynamic/json?style=for-the-badge&label=Donations%20this%20month&query=%24.fiatTotal&prefix=%24&url=https%3A%2F%2Ftermix.site%2Fdonation-snapshot.json&color=F39044&labelColor=1a1a1a" /></a>
|
<a href="https://donate.termix.site/"><img alt="Donations this month" src="https://img.shields.io/badge/dynamic/json?style=for-the-badge&label=Donations%20this%20month&query=%24.fiatTotal&prefix=%24&url=https%3A%2F%2Ftermix.site%2Fdonation-snapshot.json&color=F39044&labelColor=1a1a1a" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<a href="https://www.youtube.com/watch?v=vhtkP-UqV5U"><img src="./.github/readme/overview.jpg" width="720" alt="Watch the Termix overview on YouTube" /></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Termix is a free, open source, self-hosted platform for managing your servers, built on plugins. The core is small. It keeps your hosts, credentials, users, sharing and sessions. Everything else, like the SSH terminal, remote desktop, file manager, Docker and metrics, is a plugin you can turn on, turn off or remove.
|
Termix is a free, open source, self-hosted server management platform built on plugins. The core keeps your hosts, credentials, users, sharing and sessions. Everything else, like the SSH terminal, remote desktop, file manager, Docker and metrics, is a plugin you can turn on, off or remove.
|
||||||
|
|
||||||
It runs on web, desktop and mobile, and it is a self-hosted alternative to Termius that stays free forever.
|
It runs on web, desktop and mobile. Think of it as a free, self-hosted Termius.
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Try the Demo
|
## Try the Demo
|
||||||
|
|
||||||
Want to see it before you install it? Try the demo at [demo.termix.site](https://demo.termix.site/). Any username and password works.
|
Try it at [demo.termix.site](https://demo.termix.site/). Any username and password works.
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
What every Termix install has, with or without plugins:
|
What every install has, with or without plugins:
|
||||||
|
|
||||||
- **Hosts:** Save hosts in nested folders with tags and parent hosts, bulk edit, import and export, and use Quick Connect for one-off connections
|
- Hosts: Save hosts in nested folders with tags and parent hosts, bulk edit, import and export, and use Quick Connect for one-off connections
|
||||||
- **Credentials:** Reuse saved passwords and keys across hosts, with host defaults you can set for the server, a user or a folder
|
- Credentials: Reuse saved passwords and keys across hosts, with host defaults you can set for the server, a user or a folder
|
||||||
- **Sharing and Roles:** Share hosts, credentials and folders with users or roles at four levels: connect, view, edit and manage
|
- Sharing and Roles: Share hosts, credentials and folders with users or roles at four levels: connect, view, edit and manage
|
||||||
- **Accounts:** Local accounts, sessions you can see and revoke, trusted devices, API keys and an audit log
|
- Accounts: Local accounts, sessions you can see and revoke, trusted devices, API keys and an audit log
|
||||||
- **Security:** Passwords, keys and other secrets are encrypted per user, and the database can be encrypted on disk
|
- Security: Passwords, keys and other secrets are encrypted per user, and the database can be encrypted on disk
|
||||||
- **Desktop Sync:** The desktop app runs on its own and can link to a Termix server to keep an offline copy of your account
|
- Desktop Sync: The desktop app runs on its own and can link to a Termix server to keep an offline copy of your account
|
||||||
- **Interface:** Tabs and split screen, a command palette (double tap left shift), rebindable shortcuts, themes, branding and around 30 languages
|
- Interface: Tabs and split screen, a command palette (double tap left shift), rebindable shortcuts, themes, branding and around 30 languages
|
||||||
- **Databases:** SQLite by default, with PostgreSQL and MySQL supported too
|
- Databases: SQLite by default, with PostgreSQL and MySQL supported too
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ Plugins add everything else. The official ones ship with Termix, and admins can
|
|||||||
| [Web Endpoint](https://github.com/Termix-SSH/Plugin-Web-Endpoint) | Open a host's web UI inside Termix, directly or over an SSH tunnel. |
|
| [Web Endpoint](https://github.com/Termix-SSH/Plugin-Web-Endpoint) | Open a host's web UI inside Termix, directly or over an SSH tunnel. |
|
||||||
| [Workspaces](https://github.com/Termix-SSH/Plugin-Workspaces) | Save your open tabs and split layouts and reopen them in one click. |
|
| [Workspaces](https://github.com/Termix-SSH/Plugin-Workspaces) | Save your open tabs and split layouts and reopen them in one click. |
|
||||||
|
|
||||||
Want to build your own? Start from the [plugin template](https://github.com/Termix-SSH/Termix-Plugin-Template). Plugins you can install are listed in the [registry](https://github.com/Termix-SSH/Termix-Registry).
|
Want to build your own? Start from the [plugin template](https://github.com/Termix-SSH/Termix-Plugin-Template). Plugins you can install are listed in the [registry](https://github.com/Termix-SSH/Termix-Registry). Turn on the community registry in Plugins, Registries to install plugins made by other people, or add your own registry. See [submitting a plugin](https://docs.termix.site/develop/community-registry) and [hosting your own registry](https://docs.termix.site/develop/registries#host-your-own-registry).
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@@ -105,27 +107,27 @@ Want to build your own? Start from the [plugin template](https://github.com/Term
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>Web</b></td>
|
<td align="center"><b>Web</b></td>
|
||||||
<td>Any modern browser (Chrome, Safari, Firefox) · PWA support</td>
|
<td>Any modern browser (Chrome, Safari, Firefox) | PWA support</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>Windows</b> <sub>x64/ia32</sub></td>
|
<td align="center"><b>Windows</b> <sub>x64/ia32</sub></td>
|
||||||
<td>Portable · MSI Installer · Chocolatey</td>
|
<td>Portable | EXE and MSI Installer | Chocolatey</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>Linux</b> <sub>x64/ia32</sub></td>
|
<td align="center"><b>Linux</b> <sub>x64/arm64/armv7l</sub></td>
|
||||||
<td>Portable · AUR · AppImage · Deb · Flatpak</td>
|
<td>Portable | AUR | AppImage | Deb | Flatpak</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>macOS</b> <sub>x64/ia32, v12.0+</sub></td>
|
<td align="center"><b>macOS</b> <sub>Universal/x64/arm64, v12.0+</sub></td>
|
||||||
<td>Apple App Store · DMG · Homebrew</td>
|
<td>Apple App Store | DMG | Homebrew</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>iOS/iPadOS</b> <sub>v15.1+</sub></td>
|
<td align="center"><b>iOS/iPadOS</b> <sub>v15.1+</sub></td>
|
||||||
<td>Apple App Store · IPA</td>
|
<td>Apple App Store | IPA</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>Android</b> <sub>v7.0+</sub></td>
|
<td align="center"><b>Android</b> <sub>v7.0+</sub></td>
|
||||||
<td>Google Play Store · APK</td>
|
<td>Google Play Store | APK</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -133,9 +135,9 @@ Want to build your own? Start from the [plugin template](https://github.com/Term
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Visit the [Termix Docs](https://docs.termix.site/install) for full install steps on every platform.
|
See the [docs](https://docs.termix.site/install) for install steps on every platform.
|
||||||
|
|
||||||
Sample Docker Compose file (you can leave out `guacd` and the network if you don't plan to use the Remote Desktop plugin):
|
Sample Docker Compose file. Remote desktop (RDP, VNC and Telnet) is built in, so nothing else needs to run next to it:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -149,40 +151,23 @@ services:
|
|||||||
- termix-data:/app/data
|
- termix-data:/app/data
|
||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
depends_on:
|
|
||||||
- guacd
|
|
||||||
networks:
|
|
||||||
- termix-net
|
|
||||||
|
|
||||||
guacd:
|
|
||||||
image: guacamole/guacd:1.6.0
|
|
||||||
container_name: guacd
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "4822:4822"
|
|
||||||
networks:
|
|
||||||
- termix-net
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
termix-data:
|
termix-data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
networks:
|
|
||||||
termix-net:
|
|
||||||
driver: bridge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cloud Hosting
|
### Cloud Hosting
|
||||||
|
|
||||||
You can run Termix on a VPS instead of inside your own network. If Termix runs on the network it manages, an outage takes Termix down with it, right when you need it to fix things. Running it somewhere else keeps it reachable, gives you a static IP and lets you get in from anywhere without a VPN or port forward.
|
If Termix runs on the network it manages, an outage takes it down right when you need it. A VPS keeps it reachable, gives you a static IP and works from anywhere without a VPN or port forward.
|
||||||
|
|
||||||
[GINERNET](https://docs.termix.site/install/ginernet) sponsors Termix, and the docs have a step by step guide for their VPS platform.
|
[GINERNET](https://docs.termix.site/install/ginernet) sponsors Termix and the docs have a setup guide for their VPS.
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
Termix is free and open source with no subscriptions or paid plans. If you find it useful, consider donating to help cover server costs, domains and development time.
|
Termix has no paid plans. Donations cover servers, domains and development time.
|
||||||
|
|
||||||
[Donate](https://donate.termix.site/)
|
[Donate](https://donate.termix.site/)
|
||||||
|
|
||||||
@@ -190,7 +175,7 @@ Termix is free and open source with no subscriptions or paid plans. If you find
|
|||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
Interested in a paid placement to support development? Email [mail@termix.site](mailto:mail@termix.site).
|
For a paid sponsor spot, email [mail@termix.site](mailto:mail@termix.site).
|
||||||
|
|
||||||
<!-- SPONSORS:START -->
|
<!-- SPONSORS:START -->
|
||||||
|
|
||||||
@@ -198,14 +183,6 @@ Interested in a paid placement to support development? Email [mail@termix.site](
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<a href="https://www.digitalocean.com/">
|
|
||||||
<img src="https://termix.site/img/sponsors/digitalocean.svg" height="40" alt="DigitalOcean" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://crowdin.com/">
|
|
||||||
<img src="https://termix.site/img/sponsors/crowdin.svg" height="40" alt="Crowdin" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://www.blacksmith.sh/">
|
<a href="https://www.blacksmith.sh/">
|
||||||
<img src="https://termix.site/img/sponsors/blacksmith.svg" height="40" alt="Blacksmith" />
|
<img src="https://termix.site/img/sponsors/blacksmith.svg" height="40" alt="Blacksmith" />
|
||||||
</a>
|
</a>
|
||||||
@@ -246,7 +223,7 @@ Bugs and ideas for Termix itself (login, hosts, credentials, sharing, sync, the
|
|||||||
|
|
||||||
Every plugin has its own repo, so a problem with the terminal, file manager, Docker, remote desktop or any other plugin goes in that plugin's repo (see the table above, or use Report an issue on the plugin's page in the app, which fills in your versions for you). Not sure where it goes? Open it here and it will be moved.
|
Every plugin has its own repo, so a problem with the terminal, file manager, Docker, remote desktop or any other plugin goes in that plugin's repo (see the table above, or use Report an issue on the plugin's page in the app, which fills in your versions for you). Not sure where it goes? Open it here and it will be moved.
|
||||||
|
|
||||||
You need to be logged in to GitHub. Please be as detailed as possible, preferably in English.
|
You need a GitHub account. Add as much detail as you can, in English if possible.
|
||||||
|
|
||||||
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server. Update videos are on [YouTube](https://www.youtube.com/@TermixSSH/videos).
|
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server. Update videos are on [YouTube](https://www.youtube.com/@TermixSSH/videos).
|
||||||
|
|
||||||
@@ -254,4 +231,4 @@ For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf)
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the Apache License Version 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE) for more information.
|
Apache License 2.0. See [LICENSE](LICENSE).
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please report any vulnerabilities to [GitHub Security](https://github.com/Termix-SSH/Termix/security/advisories).
|
|
||||||
|
|
||||||
## External secret storage
|
|
||||||
|
|
||||||
By default, a single-container installation generates its keys in the Termix
|
|
||||||
data directory for ease of recovery. Production deployments that keep backups
|
|
||||||
or database files outside a trusted encrypted volume should set
|
|
||||||
`TERMIX_REQUIRE_EXTERNAL_SECRETS=true` and provide all four keys through a
|
|
||||||
secret manager:
|
|
||||||
|
|
||||||
- `JWT_SECRET` (at least 64 characters)
|
|
||||||
- `DATABASE_KEY` (64 hexadecimal characters)
|
|
||||||
- `ENCRYPTION_KEY` (64 hexadecimal characters)
|
|
||||||
- `INTERNAL_AUTH_TOKEN` (at least 32 characters)
|
|
||||||
|
|
||||||
Each value can instead be mounted as a Docker or Kubernetes secret and supplied
|
|
||||||
with its corresponding `_FILE` variable, such as `ENCRYPTION_KEY_FILE`.
|
|
||||||
Hardened mode fails closed instead of writing a replacement key beside the
|
|
||||||
encrypted database.
|
|
||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/index.css",
|
"css": "src/ui/index.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
project_id: "858252"
|
|
||||||
api_token: "env:CROWDIN_API_TOKEN"
|
|
||||||
preserve_hierarchy: true
|
|
||||||
|
|
||||||
files:
|
|
||||||
# Core has always lived at /en.json in Crowdin; moving it would orphan its translations.
|
|
||||||
- source: /src/ui/locales/en.json
|
|
||||||
dest: /en.json
|
|
||||||
translation: /src/ui/locales/translated/%locale_with_underscore%.json
|
|
||||||
+12
-7
@@ -1,7 +1,7 @@
|
|||||||
# Stage 1: Install dependencies
|
# Stage 1: Install dependencies
|
||||||
# The builders only produce JS, so they run on the build machine's own arch
|
# The builders only produce JS, so they run on the build machine's own arch
|
||||||
# once instead of under QEMU per target. Rolldown hangs under arm64 emulation.
|
# once instead of under QEMU per target. Rolldown hangs under arm64 emulation.
|
||||||
FROM --platform=$BUILDPLATFORM node:26-slim AS deps
|
FROM --platform=$BUILDPLATFORM node:24-slim AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||||
@@ -26,7 +26,7 @@ COPY . .
|
|||||||
RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete
|
RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete
|
||||||
|
|
||||||
RUN npm cache clean --force && \
|
RUN npm cache clean --force && \
|
||||||
NODE_OPTIONS="--max-old-space-size=3072" npm run build
|
TERMIX_RUNTIME_BASE_PATH=true NODE_OPTIONS="--max-old-space-size=3072" npm run build
|
||||||
|
|
||||||
# Stage 3: Build backend
|
# Stage 3: Build backend
|
||||||
FROM deps AS backend-builder
|
FROM deps AS backend-builder
|
||||||
@@ -34,10 +34,12 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build:backend
|
# Bundled plugin downloads show in the plugin store, so keep them between builds.
|
||||||
|
RUN --mount=type=cache,target=/app/node_modules/.cache/termix-bundled-plugins \
|
||||||
|
npm run build:backend
|
||||||
|
|
||||||
# Stage 4: Download OPKSSH binary for the target platform so the image works offline
|
# Stage 4: Download OPKSSH binary for the target platform so the image works offline
|
||||||
FROM node:26-slim AS opkssh-downloader
|
FROM node:24-slim AS opkssh-downloader
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG OPKSSH_VERSION=v0.16.0
|
ARG OPKSSH_VERSION=v0.16.0
|
||||||
ARG OPKSSH_SHA256_AMD64=c018c3e7baf98612b923e742dd87be38650bf61e3b755fb2bc90de177568b1bf
|
ARG OPKSSH_SHA256_AMD64=c018c3e7baf98612b923e742dd87be38650bf61e3b755fb2bc90de177568b1bf
|
||||||
@@ -58,7 +60,7 @@ RUN case "$TARGETARCH" in \
|
|||||||
echo -n "$OPKSSH_VERSION" > version.txt
|
echo -n "$OPKSSH_VERSION" > version.txt
|
||||||
|
|
||||||
# Stage 5: Production dependencies only
|
# Stage 5: Production dependencies only
|
||||||
FROM node:26-slim AS production-deps
|
FROM node:24-slim AS production-deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||||
@@ -72,16 +74,19 @@ COPY vendor ./vendor
|
|||||||
|
|
||||||
COPY scripts/patch-ssh2-agent.cjs ./scripts/
|
COPY scripts/patch-ssh2-agent.cjs ./scripts/
|
||||||
|
|
||||||
|
# ssh2's native crypto crashes Node 24 when a cipher is freed, so ssh2 uses
|
||||||
|
# OpenSSL instead (chacha20-poly1305 is then left out).
|
||||||
RUN npm ci --omit=dev --ignore-scripts && \
|
RUN npm ci --omit=dev --ignore-scripts && \
|
||||||
node scripts/patch-ssh2-agent.cjs && \
|
node scripts/patch-ssh2-agent.cjs && \
|
||||||
rm -rf node_modules/better-sqlite3/prebuilds && \
|
rm -rf node_modules/better-sqlite3/prebuilds && \
|
||||||
npm run build-release --prefix node_modules/better-sqlite3 && \
|
npm run build-release --prefix node_modules/better-sqlite3 && \
|
||||||
test -f node_modules/better-sqlite3/build/Release/better_sqlite3.node && \
|
test -f node_modules/better-sqlite3/build/Release/better_sqlite3.node && \
|
||||||
npm rebuild bcryptjs ssh2 && \
|
npm rebuild bcryptjs && \
|
||||||
|
rm -rf node_modules/ssh2/lib/protocol/crypto/build && \
|
||||||
npm cache clean --force
|
npm cache clean --force
|
||||||
|
|
||||||
# Stage 6: Final optimized image
|
# Stage 6: Final optimized image
|
||||||
FROM node:26-slim
|
FROM node:24-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# The release the opkssh-downloader stage baked in, so the plugin trusts that
|
# The release the opkssh-downloader stage baked in, so the plugin trusts that
|
||||||
|
|||||||
+94
-80
@@ -3,173 +3,183 @@
|
|||||||
{
|
{
|
||||||
"id": "acme-ssl",
|
"id": "acme-ssl",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-ACME-SSL/releases/download/v1.0.0/acme-ssl-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-ACME-SSL/releases/download/v1.0.2/acme-ssl-1.0.2.tmxplug",
|
||||||
"sha256": "1725735a058065b211436b166c0ef7d4ca6e74438102764664381160a2b305d3"
|
"sha256": "157d0f96dc42136ff64e618b6904db7862079cd31c1f1cf5f19846b1d127b00f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ai",
|
"id": "ai",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.0.0/ai-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.1.0/ai-1.1.0.tmxplug",
|
||||||
"sha256": "ea121658305bed9d28e046e096fc458976258d7cb77387e1b8ac82ccb9a001cb"
|
"sha256": "71dda6940d62c9eede7821bbac21e99443fbc08728a47759f215e2416f1d162f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alerts",
|
"id": "alerts",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Alerts/releases/download/v1.0.0/alerts-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Alerts/releases/download/v1.0.2/alerts-1.0.2.tmxplug",
|
||||||
"sha256": "93274529eb3445db9e1dddc630b7608b15250eb179bb7919a43c3e6707fa20b5",
|
"sha256": "50768e77019276343a5d162c85d081659fb89d1cc3733ac45cd0db4436918faa",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "automations",
|
"id": "automations",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.0.0/automations-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.0.2/automations-1.0.2.tmxplug",
|
||||||
"sha256": "6cb00cf9464e42dbc46786a6ef74b07d62da39ece9905197584d2934265b9b10"
|
"sha256": "674eb04276b4a0c77c794b30647f8680218a78de8d52b73e99ec900cc1b3fdb3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "docker",
|
"id": "docker",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.0.0/docker-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.0.2/docker-1.0.2.tmxplug",
|
||||||
"sha256": "d1d1dd441b51a97231d5da94e38a12b2d2b25db6da5e2b13b4a4b9486881ed2d",
|
"sha256": "0e1c722682faac6a027fd9b01e6910eb53ce5acb13332c6c8d8868554f3701ab",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "file-manager",
|
"id": "file-manager",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.0.0/file-manager-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.0.3/file-manager-1.0.3.tmxplug",
|
||||||
"sha256": "8d60f6299a38fcc69a17ca30abb0e2a168aa880c0cfb50d57f1dd37158d811a5",
|
"sha256": "250e9779a6b1caf11721f1ee2baf76655ae6e52d963e894d8db84eb9677cf0b3",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fleets",
|
"id": "fleets",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Fleets/releases/download/v1.0.0/fleets-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Fleets/releases/download/v1.0.2/fleets-1.0.2.tmxplug",
|
||||||
"sha256": "4a774d4a3685b0ee8da5b73d14c60d40f10b6a3f1c5b31a5dfefb94ba30fb7ad"
|
"sha256": "50dab38e1c6c95fdb4d0544ce50cdf84dae84789e64aa34ea9d04fdac3a38c59"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "homepage",
|
"id": "homepage",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Homepage/releases/download/v1.0.0/homepage-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Homepage/releases/download/v1.0.2/homepage-1.0.2.tmxplug",
|
||||||
"sha256": "403513bc1f96a7c21d39d55c8fd1d4e55fde7b8e6ec63228839bf3243d93e68a"
|
"sha256": "88cdea8ba8f458fcf124e7ab97e3d419896d8d22ffa9a372af34f1818872975b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "host-metrics",
|
"id": "host-metrics",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.0.0/host-metrics-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.0.3/host-metrics-1.0.3.tmxplug",
|
||||||
"sha256": "1cfc81f724418fd68306e3ac30daa529b03aa065740a866f6fbcaa67edb440ac",
|
"sha256": "8f6022568349fc4318d509781bdb9d5b40973c010a9e27f720eed6baf68b881e",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ldap",
|
"id": "ldap",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-LDAP/releases/download/v1.0.0/ldap-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-LDAP/releases/download/v1.0.2/ldap-1.0.2.tmxplug",
|
||||||
"sha256": "2d8cb269242c12522a5195fa44ec14e95ade3b00d7e7be12e9204631518a0f88"
|
"sha256": "9ef269ad281f1c68cea53e0d43ccb7a5d4f949f11b30b22eac3d01047b20eb03"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "network-topology",
|
"id": "network-topology",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Network-Topology/releases/download/v1.0.0/network-topology-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Network-Topology/releases/download/v1.0.2/network-topology-1.0.2.tmxplug",
|
||||||
"sha256": "5c0a585dcffd1cd523e94d45f2eda32800c403a4c832854fec7331b651da7b7d"
|
"sha256": "8d32340a0d0f9674b872da5d3d53e90f529b80dfe0d73ab294eed00e565fbb56"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "opkssh",
|
"id": "opkssh",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-OPKSSH/releases/download/v1.0.0/opkssh-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-OPKSSH/releases/download/v1.0.2/opkssh-1.0.2.tmxplug",
|
||||||
"sha256": "7e8a17aa9053954e937ac2274928e29b821dfd671759e356551b27c13e23c9ad"
|
"sha256": "4bb8f1ac5d39bd18ac3d0fe82ddbf2f26ab4c01bd75e6f7a073d65c2e9aef219"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "proxmox",
|
"id": "proxmox",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Proxmox/releases/download/v1.0.0/proxmox-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Proxmox/releases/download/v1.0.2/proxmox-1.0.2.tmxplug",
|
||||||
"sha256": "cb6149ab69e348e5a0a1939b31943cb64aded3e1ee930b91dc77c46a715990d0"
|
"sha256": "b92be2644b0c8121732103b922069a6cb3cf00caad4e6549f7a8cb471bda5e4e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "remote-desktop",
|
"id": "remote-desktop",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.0.0/remote-desktop-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.1.0/remote-desktop-1.1.0.tmxplug",
|
||||||
"sha256": "5201dc1ce4846ef9f509a4c609698e2b2970e31fbc152bb2fe5c39c72c5c6d39",
|
"sha256": "6f74f1d26f91a43b2d2203417d744211f366c335d5fff7bed34bfce33874dd01",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "secret-sources",
|
"id": "secret-sources",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Secret-Sources/releases/download/v1.0.0/secret-sources-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Secret-Sources/releases/download/v1.0.2/secret-sources-1.0.2.tmxplug",
|
||||||
"sha256": "b6fbe38cd6d62d0b361c102f13f050ee8a281983dc806e32e8d8feeb416ceb20"
|
"sha256": "82e2b1e95b0837dd3e5fee277be4eea2d1ce68567b0e204941c48150f415d134"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "serial",
|
"id": "serial",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Serial/releases/download/v1.0.0/serial-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Serial/releases/download/v1.0.2/serial-1.0.2.tmxplug",
|
||||||
"sha256": "c8c3ac8c916253c928195005773e558ee87555fb47b7b56f3e43c0df0fb852c4",
|
"sha256": "6ede7e8726a7312429473f34a46f93ba6657c5b7ebc12bed18f80ec62c211715",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": "desktop"
|
"recommended": "desktop",
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "session-recording",
|
"id": "session-recording",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Session-Recording/releases/download/v1.0.0/session-recording-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Session-Recording/releases/download/v1.0.2/session-recording-1.0.2.tmxplug",
|
||||||
"sha256": "ff0e6948806e0127399d41a6abd0f41fce404e65ba2c9d8044fb6dced401f4bb"
|
"sha256": "55ace6897710ce9a63f24e3d736a9e533cac557be0f3d98974956c94cf417d1c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "session-sharing",
|
"id": "session-sharing",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Session-Sharing/releases/download/v1.0.0/session-sharing-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Session-Sharing/releases/download/v1.0.2/session-sharing-1.0.2.tmxplug",
|
||||||
"sha256": "4d2e363086a8fe79815bf76b4ed359ac82d5926373ab24854028e4dbc5a57ac6"
|
"sha256": "6408e7949a9549ac5ec94b35205729099138e730df1598d50f4bb618d8d90804"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "snippets",
|
"id": "snippets",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Snippets/releases/download/v1.0.0/snippets-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Snippets/releases/download/v1.0.2/snippets-1.0.2.tmxplug",
|
||||||
"sha256": "0239e63df6d33cb38a1212935dae972402b174a7eab8b351084dab45196f1fe5",
|
"sha256": "cc1f5d74cf97f9a9c8f800fccf481c8a000e6fa516bc260ce24a7249d4f4c8e5",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ssh-terminal",
|
"id": "ssh-terminal",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.0.0/ssh-terminal-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.0.3/ssh-terminal-1.0.3.tmxplug",
|
||||||
"sha256": "66d2a4e87a2b5ac5551c74a4fad5808a6ee81d6e8f0a43c1f6456a12d6232714",
|
"sha256": "bc288bd843e37b64bcda7462d175039147b75dc6de8866769e4a938036a9d023",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sso",
|
"id": "sso",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-SSO/releases/download/v1.0.0/sso-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-SSO/releases/download/v1.0.3/sso-1.0.3.tmxplug",
|
||||||
"sha256": "88d7fcf8eb3b0916d69bcc7349a58d98e37c493bfadc8b90f81b8aa5c845abc5",
|
"sha256": "87b03c0789d981dbc806162d73dfcc2c624150375277efd2ae40ab9d7657d48e",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
|
"recommended": false,
|
||||||
|
"consent": false,
|
||||||
"enabledByEnv": ["OIDC_CLIENT_ID"]
|
"enabledByEnv": ["OIDC_CLIENT_ID"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "step-ca",
|
"id": "step-ca",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Step-CA/releases/download/v1.0.0/step-ca-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Step-CA/releases/download/v1.0.2/step-ca-1.0.2.tmxplug",
|
||||||
"sha256": "997af2068f88c017c76a057683ac5e367566b5b8bc0961fc1e29c32b702a9059"
|
"sha256": "685b644fe44c2d3fa007baee16e8ea8a7d5204f039ebc8aa65e8093ad93adf8a"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tailscale",
|
"id": "tailscale",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Tailscale/releases/download/v1.0.0/tailscale-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Tailscale/releases/download/v1.0.2/tailscale-1.0.2.tmxplug",
|
||||||
"sha256": "963a84859f8411bcb27e2950d46a7a43b239ace1295ac2abc4a77ecec43806d9"
|
"sha256": "5c7b00d6b6b0186d833302e39a9d31b92ea92d4542e9e2dab550b95837a59d97"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "telemetry",
|
"id": "telemetry",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Telemetry/releases/download/v1.0.0/telemetry-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Telemetry/releases/download/v1.0.2/telemetry-1.0.2.tmxplug",
|
||||||
"sha256": "9f973902320a01eed5fead9b31288face503a790bf68a2826176e48fb663c30c",
|
"sha256": "5ffdc58b05cd8b37fd0a207f99d2d3a256164130c950032501c499f869edb9ec",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": true
|
"consent": true
|
||||||
@@ -178,73 +188,77 @@
|
|||||||
{
|
{
|
||||||
"id": "termix-identity",
|
"id": "termix-identity",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Termix-Identity/releases/download/v1.0.0/termix-identity-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Termix-Identity/releases/download/v1.0.2/termix-identity-1.0.2.tmxplug",
|
||||||
"sha256": "00dfe3da65f78d7d085817c93a4e01bfef6931fb70ff5576c76e2f641e337555"
|
"sha256": "ac6c90a48b4f1e9cff6773a5fd267b2bf7319d2fc7e2aa2cd4004cec82f0f927"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tmux-monitor",
|
"id": "tmux-monitor",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.0.0/tmux-monitor-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.0.3/tmux-monitor-1.0.3.tmxplug",
|
||||||
"sha256": "bbc508410296ca3434b5ba8c40fa5ef87646d104ec6f75da3bf0e22313bbbf18"
|
"sha256": "f152d64f433d3f1de7c3fdf172e752ea7e333909f0fdda77336a6b8dddb9f242"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "totp",
|
"id": "totp",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-TOTP/releases/download/v1.0.0/totp-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-TOTP/releases/download/v1.0.2/totp-1.0.2.tmxplug",
|
||||||
"sha256": "c0b6b43d778dfd47af7bdbca099667bf46539de62936c3291947ac2425c02de9",
|
"sha256": "94a0772660fa8e4dfd9afc15b68d5bc9fe02fe28a1acfd35e65a57c304d460c0",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tunnels",
|
"id": "tunnels",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.0.0/tunnels-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.0.3/tunnels-1.0.3.tmxplug",
|
||||||
"sha256": "82df362cfc820437db8c56ebd1e3140d7fcef677d98a5439d7e868486cc21ad4",
|
"sha256": "d0de9e59ccc9e1a0f2817dd1ec9951021f2d7e89304894bb1e8cefd65aebff0b",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "vault",
|
"id": "vault",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Vault/releases/download/v1.0.0/vault-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Vault/releases/download/v1.0.2/vault-1.0.2.tmxplug",
|
||||||
"sha256": "d84b0ef1a309bb3a89288f488a242dd19df9e46b237df778c2f2a77e77867960"
|
"sha256": "48ef402d030912a836489edf0fdfcaf7929cbd67c21f16a2079cb4925134cc28"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "wake-on-lan",
|
"id": "wake-on-lan",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Wake-On-LAN/releases/download/v1.0.0/wake-on-lan-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Wake-On-LAN/releases/download/v1.0.2/wake-on-lan-1.0.2.tmxplug",
|
||||||
"sha256": "e680c22279438a566d1033939e4a3063a0fbbeb7f57b6db3bd6712bed09a4388"
|
"sha256": "3e9cf9a133b4dd96ec5f2b9fbf2d6a3ff7e08f8dfbf1e86b8d45de0c2f024f42"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "warpgate",
|
"id": "warpgate",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Warpgate/releases/download/v1.0.0/warpgate-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Warpgate/releases/download/v1.0.2/warpgate-1.0.2.tmxplug",
|
||||||
"sha256": "c974be686488905277a52440b248d14370f3f543a7bd53f3485d5b6db4fab40b"
|
"sha256": "5c2c1c9aef40dbbf98424d02303f2ee77e6e869334247881e8aa0fcd8c415b47"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "web-endpoint",
|
"id": "web-endpoint",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.0.0/web-endpoint-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.0.2/web-endpoint-1.0.2.tmxplug",
|
||||||
"sha256": "e93d8535617783bc538d415f25a5acc04e3805417002d68d4ebc4044c8fbed8c"
|
"sha256": "0326edb1052d1debd083004c2dd1711d21a902cfacb55ce74d1a8856a5122477"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "webauthn",
|
"id": "webauthn",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-WebAuthn/releases/download/v1.0.0/webauthn-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-WebAuthn/releases/download/v1.0.2/webauthn-1.0.2.tmxplug",
|
||||||
"sha256": "fba89362c503813a12a7b73c9aed6994a10a1d010db8e1237a1f901131eb8713",
|
"sha256": "8269f6e407897332648c8526b65383c4db071ea782bf1e58aa7c7c00627f3f4b",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "workspaces",
|
"id": "workspaces",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Workspaces/releases/download/v1.0.0/workspaces-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Workspaces/releases/download/v1.0.2/workspaces-1.0.2.tmxplug",
|
||||||
"sha256": "31d4d2ba02b59f2c2189c0400eb22d07aa8ba72728cd73a4512995581cf3afcd",
|
"sha256": "97519a01fbb2eb2eecf21d7b9ab9028c740457bcff9fc6b075fb6facbc5ccb6b",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,24 +12,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
GUACD_HOST: "guacd-dev"
|
|
||||||
GUACD_TUNNEL_HOST: "termix-dev"
|
|
||||||
GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole"
|
|
||||||
GUACD_DRIVE_PATH: "/termix-data/rdp-drive"
|
|
||||||
# REDIS_URL: "redis://redis:6379"
|
# REDIS_URL: "redis://redis:6379"
|
||||||
# TERMIX_REDIS_PREFIX: "termix:collab"
|
# TERMIX_REDIS_PREFIX: "termix:collab"
|
||||||
# TERMIX_STEP_CA_REDIS_PREFIX: "termix:step-ca"
|
# TERMIX_STEP_CA_REDIS_PREFIX: "termix:step-ca"
|
||||||
depends_on:
|
|
||||||
- guacd-dev
|
|
||||||
networks:
|
|
||||||
- termix-dev-net
|
|
||||||
|
|
||||||
guacd-dev:
|
|
||||||
image: guacamole/guacd:1.6.0
|
|
||||||
container_name: guacd-dev
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- termix-dev-data:/termix-data
|
|
||||||
networks:
|
networks:
|
||||||
- termix-dev-net
|
- termix-dev-net
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
termix:
|
termix:
|
||||||
image: ghcr.io/lukegus/termix:latest
|
image: ghcr.io/termix-ssh/termix:latest
|
||||||
container_name: termix
|
container_name: termix
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -9,12 +9,6 @@ services:
|
|||||||
- termix-data:/app/data
|
- termix-data:/app/data
|
||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
GUACD_HOST: "guacd"
|
|
||||||
GUACD_TUNNEL_HOST: "termix"
|
|
||||||
GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole"
|
|
||||||
# Where guacd stores files for RDP drive redirection (one folder per
|
|
||||||
# user is created underneath). Must be writable by guacd's user.
|
|
||||||
GUACD_DRIVE_PATH: "/termix-data/rdp-drive"
|
|
||||||
# Multi-instance deployments should point every backend at the same
|
# Multi-instance deployments should point every backend at the same
|
||||||
# Redis. Without it, collaboration rooms keep their single-instance
|
# Redis. Without it, collaboration rooms keep their single-instance
|
||||||
# in-memory fallback.
|
# in-memory fallback.
|
||||||
@@ -28,21 +22,14 @@ services:
|
|||||||
# Trusted reverse-proxy authentication is disabled by default. When
|
# Trusted reverse-proxy authentication is disabled by default. When
|
||||||
# enabled, do not expose this container directly to untrusted clients.
|
# enabled, do not expose this container directly to untrusted clients.
|
||||||
# TRUSTED_PROXY_AUTH_ENABLED: "true"
|
# TRUSTED_PROXY_AUTH_ENABLED: "true"
|
||||||
# TRUSTED_PROXY_AUTH_TRUSTED_PROXIES: "172.16.0.0/12"
|
# List the address your authenticating proxy connects from, as this
|
||||||
|
# container sees it (for a proxy container on termix-net, its IP there).
|
||||||
|
# Keep it narrow: published ports can reach the container from the
|
||||||
|
# Docker gateway, so do not trust whole private ranges.
|
||||||
|
# TRUSTED_PROXY_AUTH_TRUSTED_PROXIES: "172.20.0.10"
|
||||||
# TRUSTED_PROXY_AUTH_ROLE_MAP: '{"operators":["user"]}'
|
# TRUSTED_PROXY_AUTH_ROLE_MAP: '{"operators":["user"]}'
|
||||||
# TRUSTED_PROXY_AUTH_USERNAME_HEADER: "x-forwarded-username"
|
# TRUSTED_PROXY_AUTH_USERNAME_HEADER: "x-forwarded-username"
|
||||||
# TRUSTED_PROXY_AUTH_ROLE_HEADER: "x-forwarded-role"
|
# TRUSTED_PROXY_AUTH_ROLE_HEADER: "x-forwarded-role"
|
||||||
depends_on:
|
|
||||||
- guacd
|
|
||||||
networks:
|
|
||||||
- termix-net
|
|
||||||
|
|
||||||
guacd:
|
|
||||||
image: guacamole/guacd:1.6.0
|
|
||||||
container_name: guacd
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- termix-data:/termix-data
|
|
||||||
networks:
|
networks:
|
||||||
- termix-net
|
- termix-net
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ fi
|
|||||||
mkdir -p /tmp/nginx
|
mkdir -p /tmp/nginx
|
||||||
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /tmp/nginx/nginx.conf
|
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /tmp/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Which proxies may set the client IP through X-Forwarded-For. Unset keeps
|
||||||
|
# the private ranges in the template.
|
||||||
|
if [ -n "$TRUSTED_PROXIES" ]; then
|
||||||
|
REAL_IP_LINES=""
|
||||||
|
for proxy in $(echo "$TRUSTED_PROXIES" | tr ',' ' '); do
|
||||||
|
case "$proxy" in
|
||||||
|
*[!0-9a-fA-F:./]*) echo "Ignoring invalid TRUSTED_PROXIES entry: $proxy" ;;
|
||||||
|
*) REAL_IP_LINES="${REAL_IP_LINES} set_real_ip_from ${proxy};\n" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
echo "Trusting X-Forwarded-For from: $TRUSTED_PROXIES"
|
||||||
|
awk -v lines="$REAL_IP_LINES" '
|
||||||
|
/# BEGIN TRUSTED_PROXIES/ { printf "%s", lines; skip = 1; next }
|
||||||
|
/# END TRUSTED_PROXIES/ { skip = 0; next }
|
||||||
|
!skip
|
||||||
|
' /tmp/nginx/nginx.conf > /tmp/nginx/nginx.conf.tmp
|
||||||
|
mv /tmp/nginx/nginx.conf.tmp /tmp/nginx/nginx.conf
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$ENABLE_SSL" = "true" ] && [ "$PORT" = "$SSL_PORT" ]; then
|
if [ "$ENABLE_SSL" = "true" ] && [ "$PORT" = "$SSL_PORT" ]; then
|
||||||
echo "HTTP and HTTPS use port $SSL_PORT; disabling the HTTP redirect listener"
|
echo "HTTP and HTTPS use port $SSL_PORT; disabling the HTTP redirect listener"
|
||||||
sed -i '/# BEGIN HTTP_REDIRECT_SERVER/,/# END HTTP_REDIRECT_SERVER/d' /tmp/nginx/nginx.conf
|
sed -i '/# BEGIN HTTP_REDIRECT_SERVER/,/# END HTTP_REDIRECT_SERVER/d' /tmp/nginx/nginx.conf
|
||||||
|
|||||||
+19
-1
@@ -41,10 +41,12 @@ http {
|
|||||||
image/svg+xml;
|
image/svg+xml;
|
||||||
client_header_timeout 300s;
|
client_header_timeout 300s;
|
||||||
|
|
||||||
|
# BEGIN TRUSTED_PROXIES
|
||||||
set_real_ip_from 127.0.0.1;
|
set_real_ip_from 127.0.0.1;
|
||||||
set_real_ip_from 10.0.0.0/8;
|
set_real_ip_from 10.0.0.0/8;
|
||||||
set_real_ip_from 172.16.0.0/12;
|
set_real_ip_from 172.16.0.0/12;
|
||||||
set_real_ip_from 192.168.0.0/16;
|
set_real_ip_from 192.168.0.0/16;
|
||||||
|
# END TRUSTED_PROXIES
|
||||||
real_ip_header X-Forwarded-For;
|
real_ip_header X-Forwarded-For;
|
||||||
real_ip_recursive on;
|
real_ip_recursive on;
|
||||||
|
|
||||||
@@ -198,6 +200,9 @@ http {
|
|||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# The TCP peer before set_real_ip_from rewrites $remote_addr from
|
||||||
|
# X-Forwarded-For; trusted proxy login checks this address.
|
||||||
|
proxy_set_header X-Termix-Proxy-Peer $realip_remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||||
@@ -423,6 +428,16 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||||
|
# A terminal is quiet most of the time - waiting on a prompt, reading
|
||||||
|
# output, running something long. Every console plugin dials this route
|
||||||
|
# (terminal, serial, remote desktop), and nginx's default
|
||||||
|
# proxy_read_timeout of 60s closed the socket about a minute into any
|
||||||
|
# pause, which reaches the user as the SSH server disconnecting. The
|
||||||
|
# plugin WebSocket routes below already ask for 86400s; this is the same
|
||||||
|
# class of socket, so it asks for the same.
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /host/ {
|
location /host/ {
|
||||||
@@ -461,9 +476,12 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
# Only nginx's own errors land here (the backend is down or too slow).
|
||||||
|
# There is no 50x page, so answer directly instead of a 404.
|
||||||
location = /50x.html {
|
location = /50x.html {
|
||||||
root /app/html;
|
|
||||||
internal;
|
internal;
|
||||||
|
default_type application/json;
|
||||||
|
return 502 '{"error":"The Termix server is not responding"}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-1
@@ -41,10 +41,12 @@ http {
|
|||||||
image/svg+xml;
|
image/svg+xml;
|
||||||
client_header_timeout 300s;
|
client_header_timeout 300s;
|
||||||
|
|
||||||
|
# BEGIN TRUSTED_PROXIES
|
||||||
set_real_ip_from 127.0.0.1;
|
set_real_ip_from 127.0.0.1;
|
||||||
set_real_ip_from 10.0.0.0/8;
|
set_real_ip_from 10.0.0.0/8;
|
||||||
set_real_ip_from 172.16.0.0/12;
|
set_real_ip_from 172.16.0.0/12;
|
||||||
set_real_ip_from 192.168.0.0/16;
|
set_real_ip_from 192.168.0.0/16;
|
||||||
|
# END TRUSTED_PROXIES
|
||||||
real_ip_header X-Forwarded-For;
|
real_ip_header X-Forwarded-For;
|
||||||
real_ip_recursive on;
|
real_ip_recursive on;
|
||||||
|
|
||||||
@@ -167,6 +169,9 @@ http {
|
|||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# The TCP peer before set_real_ip_from rewrites $remote_addr from
|
||||||
|
# X-Forwarded-For; trusted proxy login checks this address.
|
||||||
|
proxy_set_header X-Termix-Proxy-Peer $realip_remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||||
@@ -392,6 +397,16 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||||
|
# A terminal is quiet most of the time - waiting on a prompt, reading
|
||||||
|
# output, running something long. Every console plugin dials this route
|
||||||
|
# (terminal, serial, remote desktop), and nginx's default
|
||||||
|
# proxy_read_timeout of 60s closed the socket about a minute into any
|
||||||
|
# pause, which reaches the user as the SSH server disconnecting. The
|
||||||
|
# plugin WebSocket routes below already ask for 86400s; this is the same
|
||||||
|
# class of socket, so it asks for the same.
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /host/ {
|
location /host/ {
|
||||||
@@ -430,9 +445,12 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
# Only nginx's own errors land here (the backend is down or too slow).
|
||||||
|
# There is no 50x page, so answer directly instead of a 404.
|
||||||
location = /50x.html {
|
location = /50x.html {
|
||||||
root /app/html;
|
|
||||||
internal;
|
internal;
|
||||||
|
default_type application/json;
|
||||||
|
return 502 '{"error":"The Termix server is not responding"}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
If you are an AI agent, do not place documentation files here. This is for maintainers only.
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `plugin_install_counts` DROP COLUMN `source`;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Choose an unused prefix from N + 1 disjoint prefixes for N existing users.
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT id, username,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY registered_at, id) AS sequence,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY username ORDER BY registered_at, id) AS occurrence
|
||||||
|
FROM users
|
||||||
|
), candidates AS (
|
||||||
|
SELECT 0 AS candidate UNION ALL SELECT sequence FROM ranked
|
||||||
|
), available AS (
|
||||||
|
SELECT MIN(candidate) AS candidate FROM candidates
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM ranked WHERE username LIKE CONCAT(CONCAT('termix-recovered-', candidate, '-'), '%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
UPDATE users JOIN ranked ON ranked.id = users.id CROSS JOIN available
|
||||||
|
SET users.username = CONCAT('termix-recovered-', available.candidate, '-', ranked.sequence)
|
||||||
|
WHERE ranked.occurrence > 1;--> statement-breakpoint
|
||||||
|
ALTER TABLE `users` MODIFY COLUMN `username` varchar(255) NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `users` ADD CONSTRAINT `idx_users_username` UNIQUE(`username`);
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,27 @@
|
|||||||
"when": 1791388888997,
|
"when": 1791388888997,
|
||||||
"tag": "0069_plugin_channel",
|
"tag": "0069_plugin_channel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1791550497926,
|
||||||
|
"tag": "0070_drop_donation_modal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1791755801066,
|
||||||
|
"tag": "0071_drop_install_count_source",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 72,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1791935283211,
|
||||||
|
"tag": "0072_unique_username",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "users" DROP COLUMN "donation_modal_dismissed";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "plugin_install_counts" DROP COLUMN "source";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Choose an unused prefix from N + 1 disjoint prefixes for N existing users.
|
||||||
|
WITH ranked AS MATERIALIZED (
|
||||||
|
SELECT id, username,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY registered_at, id) AS sequence,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY username ORDER BY registered_at, id) AS occurrence
|
||||||
|
FROM users
|
||||||
|
), candidates AS (
|
||||||
|
SELECT 0 AS candidate UNION ALL SELECT sequence FROM ranked
|
||||||
|
), available AS (
|
||||||
|
SELECT MIN(candidate) AS candidate FROM candidates
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM ranked WHERE username LIKE 'termix-recovered-' || candidate || '-' || '%'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
UPDATE users SET username = 'termix-recovered-' || (SELECT candidate FROM available) || '-' || ranked.sequence
|
||||||
|
FROM ranked WHERE ranked.id = users.id AND ranked.occurrence > 1;--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "username" SET DATA TYPE varchar(255);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "idx_users_username" ON "users" USING btree ("username");
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,27 @@
|
|||||||
"when": 1791388888397,
|
"when": 1791388888397,
|
||||||
"tag": "0069_plugin_channel",
|
"tag": "0069_plugin_channel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791550496661,
|
||||||
|
"tag": "0070_drop_donation_modal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791755800419,
|
||||||
|
"tag": "0071_drop_install_count_source",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 72,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791935282340,
|
||||||
|
"tag": "0072_unique_username",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `plugin_install_counts` DROP COLUMN `source`;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Choose an unused prefix from N + 1 disjoint prefixes for N existing users.
|
||||||
|
WITH ranked AS MATERIALIZED (
|
||||||
|
SELECT id, username,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY registered_at, id) AS sequence,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY username ORDER BY registered_at, id) AS occurrence
|
||||||
|
FROM users
|
||||||
|
), candidates AS (
|
||||||
|
SELECT 0 AS candidate UNION ALL SELECT sequence FROM ranked
|
||||||
|
), available AS (
|
||||||
|
SELECT MIN(candidate) AS candidate FROM candidates
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM ranked WHERE username LIKE 'termix-recovered-' || candidate || '-' || '%'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
UPDATE users SET username = 'termix-recovered-' || (SELECT candidate FROM available) || '-' || ranked.sequence
|
||||||
|
FROM ranked WHERE ranked.id = users.id AND ranked.occurrence > 1;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `idx_users_username` ON `users` (`username`);
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -456,6 +456,27 @@
|
|||||||
"when": 1791388887785,
|
"when": 1791388887785,
|
||||||
"tag": "0064_plugin_channel",
|
"tag": "0064_plugin_channel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 65,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1791550495357,
|
||||||
|
"tag": "0065_drop_donation_modal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 66,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1791755799726,
|
||||||
|
"tag": "0066_drop_install_count_source",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 67,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1791935281343,
|
||||||
|
"tag": "0067_unique_username",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+13
-5
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"appId": "com.karmaa.termix",
|
"appId": "com.karmaa.termix",
|
||||||
"productName": "Termix",
|
"productName": "Termix",
|
||||||
"publish": null,
|
"publish": {
|
||||||
|
"provider": "generic",
|
||||||
|
"url": "https://github.com/Termix-SSH/Termix/releases/latest/download"
|
||||||
|
},
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
@@ -43,11 +46,11 @@
|
|||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"arch": ["x64", "ia32"]
|
"arch": ["x64", "ia32", "arm64"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": "msi",
|
"target": "msi",
|
||||||
"arch": ["x64", "ia32"]
|
"arch": ["x64", "ia32", "arm64"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "public/icon.ico",
|
"icon": "public/icon.ico",
|
||||||
@@ -114,8 +117,13 @@
|
|||||||
{
|
{
|
||||||
"target": "dmg",
|
"target": "dmg",
|
||||||
"arch": ["universal", "x64", "arm64"]
|
"arch": ["universal", "x64", "arm64"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "zip",
|
||||||
|
"arch": ["x64", "arm64"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"artifactName": "termix_macos_${arch}_update.${ext}",
|
||||||
"icon": "public/icon.icns",
|
"icon": "public/icon.icns",
|
||||||
"category": "public.app-category.developer-tools",
|
"category": "public.app-category.developer-tools",
|
||||||
"hardenedRuntime": true,
|
"hardenedRuntime": true,
|
||||||
@@ -123,7 +131,7 @@
|
|||||||
"entitlements": "packaging/build/entitlements.mac.plist",
|
"entitlements": "packaging/build/entitlements.mac.plist",
|
||||||
"entitlementsInherit": "packaging/build/entitlements.mac.inherit.plist",
|
"entitlementsInherit": "packaging/build/entitlements.mac.inherit.plist",
|
||||||
"type": "distribution",
|
"type": "distribution",
|
||||||
"minimumSystemVersion": "10.15",
|
"minimumSystemVersion": "12.0",
|
||||||
"mergeASARs": false,
|
"mergeASARs": false,
|
||||||
"singleArchFiles": "**/*.{node,bare,dylib}",
|
"singleArchFiles": "**/*.{node,bare,dylib}",
|
||||||
"x64ArchFiles": "**/{*.{node,bare,dylib},node-pty/prebuilds/*/spawn-helper}"
|
"x64ArchFiles": "**/{*.{node,bare,dylib},node-pty/prebuilds/*/spawn-helper}"
|
||||||
@@ -148,6 +156,6 @@
|
|||||||
"NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
|
"NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generateUpdatesFilesForAllChannels": true,
|
"generateUpdatesFilesForAllChannels": false,
|
||||||
"msiProjectCreated": "packaging/build/msi-shortcuts.cjs"
|
"msiProjectCreated": "packaging/build/msi-shortcuts.cjs"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
// Desktop self updates. The release is picked from the GitHub releases list
|
||||||
|
// instead of electron-updater's GitHub provider, because the repo also holds
|
||||||
|
// SDK and 2.9.x releases, and a new release exists a while before its files
|
||||||
|
// finish uploading. The channel file is uploaded last, so a release counts
|
||||||
|
// only once it has one for this platform.
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const REPO = "Termix-SSH/Termix";
|
||||||
|
const RELEASES_API = `https://api.github.com/repos/${REPO}/releases?per_page=30`;
|
||||||
|
const DOWNLOAD_BASE = `https://github.com/${REPO}/releases/download/`;
|
||||||
|
const MODES = ["automatic", "prompt", "never"];
|
||||||
|
const DEFAULT_MODE = "prompt";
|
||||||
|
const FIRST_CHECK_DELAY = 30 * 1000;
|
||||||
|
const CHECK_INTERVAL = 6 * 60 * 60 * 1000;
|
||||||
|
const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/;
|
||||||
|
|
||||||
|
function parseVersion(value) {
|
||||||
|
const match = VERSION.exec(String(value || "").trim());
|
||||||
|
if (!match) return null;
|
||||||
|
return {
|
||||||
|
parts: match.slice(1, 4).map(Number),
|
||||||
|
beta: match[4] == null ? null : Number(match[4]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A beta sorts before the release it leads up to.
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (a.parts[i] !== b.parts[i]) return a.parts[i] - b.parts[i];
|
||||||
|
}
|
||||||
|
if (a.beta === b.beta) return 0;
|
||||||
|
if (a.beta == null) return 1;
|
||||||
|
if (b.beta == null) return -1;
|
||||||
|
return a.beta - b.beta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches the names electron-builder writes and electron-updater reads.
|
||||||
|
function channelFile(channel, platform, arch) {
|
||||||
|
if (platform === "win32") return `${channel}.yml`;
|
||||||
|
if (platform === "darwin") return `${channel}-mac.yml`;
|
||||||
|
return `${channel}-linux${arch === "x64" ? "" : `-${arch}`}.yml`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The newest release this install should move to, or null. Stable installs
|
||||||
|
* only see stable releases; a beta install also sees newer betas.
|
||||||
|
*/
|
||||||
|
function resolveRelease(releases, { currentVersion, platform, arch }) {
|
||||||
|
const current = parseVersion(currentVersion);
|
||||||
|
if (!current || !Array.isArray(releases)) return null;
|
||||||
|
const allowBeta = current.beta != null;
|
||||||
|
|
||||||
|
const newest = releases
|
||||||
|
.filter((release) => release && !release.draft)
|
||||||
|
.map((release) => ({ release, version: parseVersion(release.tag_name) }))
|
||||||
|
.filter(
|
||||||
|
({ release, version }) =>
|
||||||
|
version &&
|
||||||
|
release.tag_name.startsWith("v") &&
|
||||||
|
(allowBeta || version.beta == null) &&
|
||||||
|
compareVersions(version, current) > 0,
|
||||||
|
)
|
||||||
|
.sort((a, b) => compareVersions(b.version, a.version))[0];
|
||||||
|
if (!newest) return null;
|
||||||
|
|
||||||
|
const { release, version } = newest;
|
||||||
|
const channel = version.beta == null ? "latest" : "beta";
|
||||||
|
const file = channelFile(channel, platform, arch);
|
||||||
|
const assets = Array.isArray(release.assets) ? release.assets : [];
|
||||||
|
if (!assets.some((asset) => asset && asset.name === file)) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tag: release.tag_name,
|
||||||
|
version: release.tag_name.slice(1),
|
||||||
|
channel,
|
||||||
|
feedUrl: `${DOWNLOAD_BASE}${release.tag_name}`,
|
||||||
|
releaseUrl:
|
||||||
|
release.html_url ||
|
||||||
|
`https://github.com/${REPO}/releases/tag/${release.tag_name}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How this copy of the app was installed, which decides whether it can
|
||||||
|
* replace itself:
|
||||||
|
* - auto: NSIS, DMG, AppImage or deb, updated in place
|
||||||
|
* - manual: MSI, Chocolatey, Flatpak, portable builds and 32-bit Windows, told where to download
|
||||||
|
* - store: Mac App Store
|
||||||
|
* - disabled: dev builds
|
||||||
|
*/
|
||||||
|
function getInstallKind({
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
isPackaged,
|
||||||
|
mas,
|
||||||
|
env,
|
||||||
|
execPath,
|
||||||
|
resourcesPath,
|
||||||
|
exists,
|
||||||
|
readFile,
|
||||||
|
}) {
|
||||||
|
if (!isPackaged) return "disabled";
|
||||||
|
if (mas) return "store";
|
||||||
|
|
||||||
|
if (platform === "linux") {
|
||||||
|
if (env.FLATPAK_ID || exists("/.flatpak-info")) return "manual";
|
||||||
|
if (env.APPIMAGE) return "auto";
|
||||||
|
try {
|
||||||
|
const type = readFile(path.join(resourcesPath, "package-type")).trim();
|
||||||
|
// The AUR package reuses the deb's files, so ask dpkg if it owns us.
|
||||||
|
if (type === "deb" && exists("/var/lib/dpkg/info/termix.list")) {
|
||||||
|
return "auto";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a package install.
|
||||||
|
}
|
||||||
|
return "manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === "win32") {
|
||||||
|
if (arch !== "x64") return "manual";
|
||||||
|
const uninstaller = path.join(
|
||||||
|
path.dirname(execPath),
|
||||||
|
"Uninstall Termix.exe",
|
||||||
|
);
|
||||||
|
return exists(uninstaller) ? "auto" : "manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === "darwin") {
|
||||||
|
// Still running from the mounted DMG.
|
||||||
|
return execPath.startsWith("/Volumes/") ? "manual" : "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMode(mode) {
|
||||||
|
return MODES.includes(mode) ? mode : DEFAULT_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUpdater({
|
||||||
|
currentVersion,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
kind,
|
||||||
|
getMode,
|
||||||
|
saveMode,
|
||||||
|
fetchReleases,
|
||||||
|
loadAutoUpdater,
|
||||||
|
send,
|
||||||
|
log,
|
||||||
|
beforeInstall,
|
||||||
|
}) {
|
||||||
|
const state = {
|
||||||
|
kind,
|
||||||
|
mode: normalizeMode(getMode()),
|
||||||
|
currentVersion,
|
||||||
|
status: "idle",
|
||||||
|
version: null,
|
||||||
|
releaseUrl: null,
|
||||||
|
progress: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
const enabled = kind === "auto" || kind === "manual";
|
||||||
|
let updater = null;
|
||||||
|
let timer = null;
|
||||||
|
let checkedOnce = false;
|
||||||
|
|
||||||
|
function update(patch) {
|
||||||
|
Object.assign(state, patch);
|
||||||
|
send({ ...state });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdater() {
|
||||||
|
if (updater) return updater;
|
||||||
|
updater = loadAutoUpdater();
|
||||||
|
updater.logger = {
|
||||||
|
info: (...args) => log("[updater]", ...args),
|
||||||
|
warn: (...args) => log("[updater]", ...args),
|
||||||
|
error: (...args) => log("[updater]", ...args),
|
||||||
|
debug: () => {},
|
||||||
|
};
|
||||||
|
updater.autoInstallOnAppQuit = true;
|
||||||
|
updater.on("update-available", (info) =>
|
||||||
|
update({
|
||||||
|
status: updater.autoDownload ? "downloading" : "available",
|
||||||
|
version: info.version,
|
||||||
|
progress: null,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
updater.on("update-not-available", () =>
|
||||||
|
update({ status: "up-to-date", error: null }),
|
||||||
|
);
|
||||||
|
updater.on("download-progress", (progress) =>
|
||||||
|
update({
|
||||||
|
status: "downloading",
|
||||||
|
progress: Math.round(progress.percent || 0),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
updater.on("update-downloaded", (info) =>
|
||||||
|
update({
|
||||||
|
status: "downloaded",
|
||||||
|
version: info.version,
|
||||||
|
progress: 100,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
updater.on("error", (error) => {
|
||||||
|
log("[updater] error:", error?.message || String(error));
|
||||||
|
update({ status: "error", error: error?.message || String(error) });
|
||||||
|
});
|
||||||
|
return updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function check() {
|
||||||
|
if (!enabled) return { ...state };
|
||||||
|
if (["checking", "downloading", "downloaded"].includes(state.status)) {
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
checkedOnce = true;
|
||||||
|
update({ status: "checking", error: null });
|
||||||
|
|
||||||
|
let release;
|
||||||
|
try {
|
||||||
|
release = resolveRelease(await fetchReleases(), {
|
||||||
|
currentVersion,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log("[updater] release lookup failed:", error?.message || String(error));
|
||||||
|
update({ status: "error", error: error?.message || String(error) });
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!release) {
|
||||||
|
update({ status: "up-to-date" });
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
update({ version: release.version, releaseUrl: release.releaseUrl });
|
||||||
|
if (kind === "manual") {
|
||||||
|
update({ status: "available" });
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auto = getUpdater();
|
||||||
|
auto.setFeedURL({ provider: "generic", url: release.feedUrl });
|
||||||
|
auto.channel = release.channel;
|
||||||
|
auto.allowDowngrade = false;
|
||||||
|
auto.autoDownload = state.mode === "automatic";
|
||||||
|
await auto.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
log("[updater] check failed:", error?.message || String(error));
|
||||||
|
update({ status: "error", error: error?.message || String(error) });
|
||||||
|
}
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
if (kind !== "auto" || state.status !== "available") return { ...state };
|
||||||
|
update({ status: "downloading", progress: 0 });
|
||||||
|
try {
|
||||||
|
await getUpdater().downloadUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
log("[updater] download failed:", error?.message || String(error));
|
||||||
|
update({ status: "error", error: error?.message || String(error) });
|
||||||
|
}
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
function install() {
|
||||||
|
if (kind !== "auto" || state.status !== "downloaded") return false;
|
||||||
|
// The tray keeps windows alive on close, so mark the quit first.
|
||||||
|
beforeInstall?.();
|
||||||
|
setImmediate(() => getUpdater().quitAndInstall());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(mode) {
|
||||||
|
if (!MODES.includes(mode)) return { ...state };
|
||||||
|
saveMode(mode);
|
||||||
|
update({ mode });
|
||||||
|
if (updater) updater.autoDownload = mode === "automatic";
|
||||||
|
if (mode !== "never" && !checkedOnce) void check();
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (!enabled || timer) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (state.mode !== "never") void check();
|
||||||
|
}, FIRST_CHECK_DELAY).unref?.();
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (state.mode !== "never") void check();
|
||||||
|
}, CHECK_INTERVAL);
|
||||||
|
timer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getState: () => ({ ...state }),
|
||||||
|
check,
|
||||||
|
download,
|
||||||
|
install,
|
||||||
|
setMode,
|
||||||
|
start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DEFAULT_MODE,
|
||||||
|
RELEASES_API,
|
||||||
|
channelFile,
|
||||||
|
compareVersions,
|
||||||
|
createUpdater,
|
||||||
|
getInstallKind,
|
||||||
|
normalizeMode,
|
||||||
|
parseVersion,
|
||||||
|
resolveRelease,
|
||||||
|
};
|
||||||
@@ -35,7 +35,13 @@ function parseConflictingPort(stderr) {
|
|||||||
* stderr. Always returns a failure -- see the note above on why every exit
|
* stderr. Always returns a failure -- see the note above on why every exit
|
||||||
* that reaches this point counts as one.
|
* that reaches this point counts as one.
|
||||||
*/
|
*/
|
||||||
|
// Part of PRE_29_DATABASE_ERROR in src/backend/database/db/upgrade-guard.ts.
|
||||||
|
const OLD_DATABASE_MARKER = "This database is from Termix 2.8 or older";
|
||||||
|
|
||||||
function classifyBackendFailure(stderr = "") {
|
function classifyBackendFailure(stderr = "") {
|
||||||
|
if (stderr.includes(OLD_DATABASE_MARKER)) {
|
||||||
|
return { reason: "old-database", port: null };
|
||||||
|
}
|
||||||
if (stderr.includes("EADDRINUSE")) {
|
if (stderr.includes("EADDRINUSE")) {
|
||||||
return { reason: "port-in-use", port: parseConflictingPort(stderr) };
|
return { reason: "port-in-use", port: parseConflictingPort(stderr) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function writeJson(filePath, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULTS = { defaultConnectionOrigin: "local" };
|
const DEFAULTS = { defaultConnectionOrigin: "local", updateMode: "prompt" };
|
||||||
|
|
||||||
function getDesktopSettings() {
|
function getDesktopSettings() {
|
||||||
return { ...DEFAULTS, ...readJson(dataPath("desktop-settings.json"), {}) };
|
return { ...DEFAULTS, ...readJson(dataPath("desktop-settings.json"), {}) };
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
// Saved by the file manager after the user picks an editor in a file dialog.
|
||||||
|
const EXTERNAL_EDITOR_SETTING = "fileManager.externalEditorPath";
|
||||||
|
|
||||||
|
// Extensions the OS would run rather than open in an editor. With no editor
|
||||||
|
// configured the temp copy goes to shell.openPath, and its name comes from
|
||||||
|
// the remote server.
|
||||||
|
const LAUNCHABLE_EXTENSIONS = new Set([
|
||||||
|
// Windows
|
||||||
|
"application",
|
||||||
|
"appref-ms",
|
||||||
|
"bat",
|
||||||
|
"chm",
|
||||||
|
"cmd",
|
||||||
|
"com",
|
||||||
|
"cpl",
|
||||||
|
"exe",
|
||||||
|
"gadget",
|
||||||
|
"hta",
|
||||||
|
"inf",
|
||||||
|
"jar",
|
||||||
|
"js",
|
||||||
|
"jse",
|
||||||
|
"library-ms",
|
||||||
|
"lnk",
|
||||||
|
"msc",
|
||||||
|
"msi",
|
||||||
|
"msp",
|
||||||
|
"mst",
|
||||||
|
"pif",
|
||||||
|
"ps1",
|
||||||
|
"psd1",
|
||||||
|
"psm1",
|
||||||
|
"py",
|
||||||
|
"pyw",
|
||||||
|
"reg",
|
||||||
|
"scf",
|
||||||
|
"scr",
|
||||||
|
"search-ms",
|
||||||
|
"settingcontent-ms",
|
||||||
|
"url",
|
||||||
|
"vbe",
|
||||||
|
"vbs",
|
||||||
|
"ws",
|
||||||
|
"wsf",
|
||||||
|
"wsh",
|
||||||
|
"xll",
|
||||||
|
// macOS
|
||||||
|
"app",
|
||||||
|
"command",
|
||||||
|
"pkg",
|
||||||
|
"terminal",
|
||||||
|
"tool",
|
||||||
|
"workflow",
|
||||||
|
// Linux
|
||||||
|
"appimage",
|
||||||
|
"desktop",
|
||||||
|
"run",
|
||||||
|
"sh",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isLaunchableFileName(fileName) {
|
||||||
|
// Windows drops trailing dots and spaces, so "a.exe. " is saved as a.exe.
|
||||||
|
const name = path.basename(String(fileName || "")).replace(/[. ]+$/, "");
|
||||||
|
const extension = path.extname(name).slice(1).toLowerCase();
|
||||||
|
return LAUNCHABLE_EXTENSIONS.has(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { EXTERNAL_EDITOR_SETTING, isLaunchableFileName };
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* The login URL a server hands the desktop app goes to shell.openExternal,
|
||||||
|
* so only web pages pass: any other scheme (file:, smb:, ms-msdt:, ...)
|
||||||
|
* would launch whatever OS handler is registered for it.
|
||||||
|
*/
|
||||||
|
function isExternalLoginUrl(value) {
|
||||||
|
try {
|
||||||
|
const { protocol } = new URL(value);
|
||||||
|
return protocol === "https:" || protocol === "http:";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A non-privileged port for the loopback callback listener. */
|
||||||
|
function isCallbackPort(value) {
|
||||||
|
return Number.isInteger(value) && value >= 1024 && value <= 65535;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { isExternalLoginUrl, isCallbackPort };
|
||||||
@@ -6,9 +6,16 @@
|
|||||||
|
|
||||||
let linked = null;
|
let linked = null;
|
||||||
|
|
||||||
|
const WEBSOCKET_TO_HTTP = { "ws:": "http:", "wss:": "https:" };
|
||||||
|
|
||||||
|
// A WebSocket to the linked server (wss://host/...) counts as the link's
|
||||||
|
// https://host origin, so its upgrade carries the proxy headers too.
|
||||||
function getOrigin(url) {
|
function getOrigin(url) {
|
||||||
try {
|
try {
|
||||||
return new URL(url).origin;
|
const parsed = new URL(url);
|
||||||
|
const httpProtocol = WEBSOCKET_TO_HTTP[parsed.protocol];
|
||||||
|
if (httpProtocol) parsed.protocol = httpProtocol;
|
||||||
|
return parsed.origin;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,7 +330,11 @@ const AUTH_TOKEN_PATTERN = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|||||||
// path, so nothing here names it. Only a /plugin-api/<id> path is accepted,
|
// path, so nothing here names it. Only a /plugin-api/<id> path is accepted,
|
||||||
// which keeps every transfer on the backend's plugin routes.
|
// which keeps every transfer on the backend's plugin routes.
|
||||||
const API_PATH_PATTERN = /^\/plugin-api\/[a-z0-9][a-z0-9-]*$/;
|
const API_PATH_PATTERN = /^\/plugin-api\/[a-z0-9][a-z0-9-]*$/;
|
||||||
const LOCAL_BACKEND = "http://localhost:30001";
|
let localBackendPort = 30001;
|
||||||
|
|
||||||
|
function setLocalBackendPort(port) {
|
||||||
|
localBackendPort = port;
|
||||||
|
}
|
||||||
let registeredApiPath = null;
|
let registeredApiPath = null;
|
||||||
|
|
||||||
function setTransferApiPath(apiPath) {
|
function setTransferApiPath(apiPath) {
|
||||||
@@ -403,7 +407,8 @@ function createTargetResolver({ localBaseUrl, getLinkedServer, apiPath }) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
url: `${normalizeHttpBase(
|
url: `${normalizeHttpBase(
|
||||||
localBaseUrl ?? `${LOCAL_BACKEND}${resolveApiPath()}`,
|
localBaseUrl ??
|
||||||
|
`http://localhost:${localBackendPort}${resolveApiPath()}`,
|
||||||
"Local backend URL",
|
"Local backend URL",
|
||||||
)}${routePath}`,
|
)}${routePath}`,
|
||||||
headers,
|
headers,
|
||||||
@@ -1050,6 +1055,7 @@ module.exports = {
|
|||||||
createLocalFileHandlers,
|
createLocalFileHandlers,
|
||||||
createTargetResolver,
|
createTargetResolver,
|
||||||
setTransferApiPath,
|
setTransferApiPath,
|
||||||
|
setLocalBackendPort,
|
||||||
publishDownload,
|
publishDownload,
|
||||||
defaultPublishFs,
|
defaultPublishFs,
|
||||||
assertWithinRoot,
|
assertWithinRoot,
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ function resolveLocalShell(
|
|||||||
* how many helpers were fixed.
|
* how many helpers were fixed.
|
||||||
*/
|
*/
|
||||||
function repairSpawnHelpers(ptyDir, fsImpl = fs) {
|
function repairSpawnHelpers(ptyDir, fsImpl = fs) {
|
||||||
const dir = ptyDir.replace(/app\.asar(?!\.unpacked)/, "app.asar.unpacked");
|
// A universal macOS build ships app-arm64.asar and app-x64.asar.
|
||||||
|
const dir = ptyDir.replace(
|
||||||
|
/\b(app(?:-[a-z0-9]+)?)\.asar(?!\.unpacked)/,
|
||||||
|
"$1.asar.unpacked",
|
||||||
|
);
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(dir, "build", "Release", "spawn-helper"),
|
path.join(dir, "build", "Release", "spawn-helper"),
|
||||||
path.join(
|
path.join(
|
||||||
|
|||||||
+169
-117
@@ -29,12 +29,21 @@ const pty = require("node-pty");
|
|||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
const linkedServer = require("./linked-server.cjs");
|
const linkedServer = require("./linked-server.cjs");
|
||||||
const desktopSettings = require("./desktop-settings.cjs");
|
const desktopSettings = require("./desktop-settings.cjs");
|
||||||
|
const autoUpdates = require("./auto-updater.cjs");
|
||||||
const { launchNativeRdp } = require("./native-rdp.cjs");
|
const { launchNativeRdp } = require("./native-rdp.cjs");
|
||||||
const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs");
|
const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs");
|
||||||
const { quitApp } = require("./app-quit.cjs");
|
const { quitApp } = require("./app-quit.cjs");
|
||||||
const { selectLinuxPasswordStore } = require("./linux-password-store.cjs");
|
const { selectLinuxPasswordStore } = require("./linux-password-store.cjs");
|
||||||
const { resolveLocalShell, repairSpawnHelpers } = require("./local-shell.cjs");
|
const { resolveLocalShell, repairSpawnHelpers } = require("./local-shell.cjs");
|
||||||
const { registerLocalFileHandlers } = require("./local-files.cjs");
|
const {
|
||||||
|
registerLocalFileHandlers,
|
||||||
|
setLocalBackendPort,
|
||||||
|
} = require("./local-files.cjs");
|
||||||
|
const {
|
||||||
|
EXTERNAL_EDITOR_SETTING,
|
||||||
|
isLaunchableFileName,
|
||||||
|
} = require("./external-editor.cjs");
|
||||||
|
const { isExternalLoginUrl, isCallbackPort } = require("./external-login.cjs");
|
||||||
|
|
||||||
const localTerminalSessions = new Map();
|
const localTerminalSessions = new Map();
|
||||||
|
|
||||||
@@ -433,14 +442,6 @@ function isInsecureModeEnabled() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrigin(url) {
|
|
||||||
try {
|
|
||||||
return new URL(url).origin;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPrivateNetworkHost(hostname) {
|
function isPrivateNetworkHost(hostname) {
|
||||||
if (
|
if (
|
||||||
hostname === "localhost" ||
|
hostname === "localhost" ||
|
||||||
@@ -484,7 +485,7 @@ function isInvalidCertificateAllowedForUrl(url) {
|
|||||||
// allows a self-signed certificate.
|
// allows a self-signed certificate.
|
||||||
const linked = linkedServer.getLinkedServer();
|
const linked = linkedServer.getLinkedServer();
|
||||||
if (!linked?.allowInvalidCertificate) return false;
|
if (!linked?.allowInvalidCertificate) return false;
|
||||||
return getOrigin(url) === linked.origin;
|
return linkedServer.isLinkedOrigin(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTlsVerificationOptions(url) {
|
function getTlsVerificationOptions(url) {
|
||||||
@@ -699,6 +700,9 @@ let tray = null;
|
|||||||
let isQuitting = false;
|
let isQuitting = false;
|
||||||
const tempFiles = new Map();
|
const tempFiles = new Map();
|
||||||
const externalEditorSessions = new Map();
|
const externalEditorSessions = new Map();
|
||||||
|
// Paths the user picked in an open dialog this session. Only these may be
|
||||||
|
// saved as the external editor, so the renderer cannot name an executable.
|
||||||
|
const dialogChosenPaths = new Set();
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
|
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
|
||||||
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
|
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
|
||||||
@@ -1041,7 +1045,7 @@ function getBackendPaths() {
|
|||||||
backendCwd: backendDir,
|
backendCwd: backendDir,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// fork() does not go through Electron's asar redirector — use the unpacked path.
|
// fork() does not go through Electron's asar redirector, use the unpacked path.
|
||||||
// On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR
|
// On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR
|
||||||
// app-arm64.asar / app-x64.asar instead of app.asar, so match all variants.
|
// app-arm64.asar / app-x64.asar instead of app.asar, so match all variants.
|
||||||
const unpackedRoot = getUnpackedAppRoot(appRoot);
|
const unpackedRoot = getUnpackedAppRoot(appRoot);
|
||||||
@@ -1115,6 +1119,35 @@ function reapOrphanedBackendProcess() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BACKEND_PORT = 30001;
|
||||||
|
let backendPort = DEFAULT_BACKEND_PORT;
|
||||||
|
|
||||||
|
function canListenOn(port) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const probe = net.createServer();
|
||||||
|
probe.once("error", () => resolve(null));
|
||||||
|
probe.listen(port, "127.0.0.1", () => {
|
||||||
|
const { port: bound } = probe.address();
|
||||||
|
probe.close(() => resolve(bound));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 30001 when it is free, otherwise any free port. Only the desktop app talks
|
||||||
|
// to this backend, so the number itself does not matter.
|
||||||
|
async function findBackendPort() {
|
||||||
|
const preferred = await canListenOn(DEFAULT_BACKEND_PORT);
|
||||||
|
if (preferred) return preferred;
|
||||||
|
const fallback = await canListenOn(0);
|
||||||
|
if (fallback) {
|
||||||
|
logToFile(
|
||||||
|
`Port ${DEFAULT_BACKEND_PORT} is in use, starting the backend on ${fallback}`,
|
||||||
|
);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return DEFAULT_BACKEND_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
function startBackendServer() {
|
function startBackendServer() {
|
||||||
reapOrphanedBackendProcess();
|
reapOrphanedBackendProcess();
|
||||||
backendFailure = null;
|
backendFailure = null;
|
||||||
@@ -1155,7 +1188,8 @@ function startBackendServer() {
|
|||||||
DATA_DIR: dataDir,
|
DATA_DIR: dataDir,
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
ELECTRON_EMBEDDED: "true",
|
ELECTRON_EMBEDDED: "true",
|
||||||
PORT: "30001",
|
PORT: String(backendPort),
|
||||||
|
EMBEDDED_BACKEND_PORT: String(backendPort),
|
||||||
VERSION: app.getVersion(),
|
VERSION: app.getVersion(),
|
||||||
},
|
},
|
||||||
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||||
@@ -1303,7 +1337,7 @@ function createTray() {
|
|||||||
try {
|
try {
|
||||||
const { nativeImage } = require("electron");
|
const { nativeImage } = require("electron");
|
||||||
|
|
||||||
// Native APIs (Tray, nativeImage) can't load files from inside app.asar —
|
// Native APIs (Tray, nativeImage) can't load files from inside app.asar,
|
||||||
// use the unpacked path so the OS sees a real file.
|
// use the unpacked path so the OS sees a real file.
|
||||||
const publicRoot = isDev
|
const publicRoot = isDev
|
||||||
? path.join(appRoot, "public")
|
? path.join(appRoot, "public")
|
||||||
@@ -1420,6 +1454,7 @@ function createWindow() {
|
|||||||
sandbox: true,
|
sandbox: true,
|
||||||
webSecurity: true,
|
webSecurity: true,
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
additionalArguments: [`--termix-backend-port=${backendPort}`],
|
||||||
partition: termixSessionPartition,
|
partition: termixSessionPartition,
|
||||||
allowRunningInsecureContent: false,
|
allowRunningInsecureContent: false,
|
||||||
webviewTag: false,
|
webviewTag: false,
|
||||||
@@ -1662,6 +1697,12 @@ ipcMain.handle("get-embedded-server-status", () => {
|
|||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"external-browser-login",
|
"external-browser-login",
|
||||||
async (_event, authUrl, callbackPort) => {
|
async (_event, authUrl, callbackPort) => {
|
||||||
|
if (!isExternalLoginUrl(authUrl)) {
|
||||||
|
return { success: false, error: "Invalid authentication URL" };
|
||||||
|
}
|
||||||
|
if (!isCallbackPort(callbackPort)) {
|
||||||
|
return { success: false, error: "Invalid callback port" };
|
||||||
|
}
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -1752,6 +1793,65 @@ ipcMain.handle("save-desktop-settings", (_event, settings) => {
|
|||||||
return desktopSettings.saveDesktopSettings(settings);
|
return desktopSettings.saveDesktopSettings(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let appUpdater = null;
|
||||||
|
|
||||||
|
function startAppUpdater() {
|
||||||
|
const kind = autoUpdates.getInstallKind({
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
isPackaged: app.isPackaged,
|
||||||
|
mas: !!process.mas,
|
||||||
|
env: process.env,
|
||||||
|
execPath: process.execPath,
|
||||||
|
resourcesPath: process.resourcesPath,
|
||||||
|
exists: (target) => fs.existsSync(target),
|
||||||
|
readFile: (target) => fs.readFileSync(target, "utf8"),
|
||||||
|
});
|
||||||
|
logToFile("Updater install kind:", kind);
|
||||||
|
appUpdater = autoUpdates.createUpdater({
|
||||||
|
currentVersion: app.getVersion(),
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
kind,
|
||||||
|
getMode: () => desktopSettings.getDesktopSettings().updateMode,
|
||||||
|
saveMode: (mode) =>
|
||||||
|
desktopSettings.saveDesktopSettings({ updateMode: mode }),
|
||||||
|
fetchReleases: async () => {
|
||||||
|
const { net } = require("electron");
|
||||||
|
const response = await net.fetch(autoUpdates.RELEASES_API, {
|
||||||
|
headers: { Accept: "application/vnd.github+json" },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub releases returned ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
loadAutoUpdater: () => require("electron-updater").autoUpdater,
|
||||||
|
send: (state) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("desktop-update-state", state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
log: logToFile,
|
||||||
|
beforeInstall: () => {
|
||||||
|
isQuitting = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
appUpdater.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"desktop-update-get-state",
|
||||||
|
() => appUpdater?.getState() ?? null,
|
||||||
|
);
|
||||||
|
ipcMain.handle("desktop-update-check", () => appUpdater?.check() ?? null);
|
||||||
|
ipcMain.handle("desktop-update-download", () => appUpdater?.download() ?? null);
|
||||||
|
ipcMain.handle("desktop-update-install", () => appUpdater?.install() ?? false);
|
||||||
|
ipcMain.handle(
|
||||||
|
"desktop-update-set-mode",
|
||||||
|
(_event, mode) => appUpdater?.setMode(mode) ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
// The server an older version of the app was pointed at, offered again when
|
// The server an older version of the app was pointed at, offered again when
|
||||||
// linking so upgraded installs do not have to look it up.
|
// linking so upgraded installs do not have to look it up.
|
||||||
ipcMain.handle("get-previous-server-url", () => {
|
ipcMain.handle("get-previous-server-url", () => {
|
||||||
@@ -1924,6 +2024,16 @@ const C2S_WS_HIGH_WATERMARK = 1024 * 1024;
|
|||||||
const C2S_WS_LOW_WATERMARK = 256 * 1024;
|
const C2S_WS_LOW_WATERMARK = 256 * 1024;
|
||||||
const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024;
|
const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
// A proxy in front of the server may drop an idle WebSocket, which stopped
|
||||||
|
// the tunnel. Pings count as traffic.
|
||||||
|
function keepC2SSocketAlive(ws) {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
||||||
|
}, 25_000);
|
||||||
|
timer.unref?.();
|
||||||
|
ws.once("close", () => clearInterval(timer));
|
||||||
|
}
|
||||||
|
|
||||||
// C2S (client-to-server) tunnels relay through the server this desktop is
|
// C2S (client-to-server) tunnels relay through the server this desktop is
|
||||||
// linked to, not the embedded backend.
|
// linked to, not the embedded backend.
|
||||||
function getC2SRemoteBaseUrl() {
|
function getC2SRemoteBaseUrl() {
|
||||||
@@ -2192,6 +2302,7 @@ async function openC2SRelay(
|
|||||||
getWebSocketOptions(relayUrl, { headers }),
|
getWebSocketOptions(relayUrl, { headers }),
|
||||||
);
|
);
|
||||||
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
|
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
|
||||||
|
keepC2SSocketAlive(ws);
|
||||||
const pendingChunks = [];
|
const pendingChunks = [];
|
||||||
let ready = false;
|
let ready = false;
|
||||||
let closed = false;
|
let closed = false;
|
||||||
@@ -2299,10 +2410,13 @@ async function testC2SRelay(
|
|||||||
getWebSocketOptions(relayUrl, { headers }),
|
getWebSocketOptions(relayUrl, { headers }),
|
||||||
);
|
);
|
||||||
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
|
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
|
||||||
|
keepC2SSocketAlive(ws);
|
||||||
|
|
||||||
if (runtime) runtime.authSocket = ws;
|
if (runtime) runtime.authSocket = ws;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
// The relay sends its reason and then closes; keep the reason.
|
||||||
|
let relayError = null;
|
||||||
const settle = (result) => {
|
const settle = (result) => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
@@ -2339,6 +2453,10 @@ async function testC2SRelay(
|
|||||||
settle({ success: true });
|
settle({ success: true });
|
||||||
} else if (message.type === "error") {
|
} else if (message.type === "error") {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
relayError = normalizeC2SErrorMessage(
|
||||||
|
message.error,
|
||||||
|
"Tunnel test failed",
|
||||||
|
);
|
||||||
settle(createC2SFailure(message.error, "Tunnel test failed"));
|
settle(createC2SFailure(message.error, "Tunnel test failed"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2352,7 +2470,10 @@ async function testC2SRelay(
|
|||||||
});
|
});
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
if (runtime && c2sTunnelRuntimes.get(tunnel.name) === runtime) {
|
if (runtime && c2sTunnelRuntimes.get(tunnel.name) === runtime) {
|
||||||
setC2STunnelError(tunnel.name, "Endpoint SSH connection closed");
|
setC2STunnelError(
|
||||||
|
tunnel.name,
|
||||||
|
relayError || "Endpoint SSH connection closed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
settle({ success: false, error: "Tunnel test connection closed" });
|
settle({ success: false, error: "Tunnel test connection closed" });
|
||||||
@@ -2609,6 +2730,7 @@ async function startC2SRemoteTunnel(tunnel, index = 0, authToken) {
|
|||||||
getWebSocketOptions(relayUrl, { headers }),
|
getWebSocketOptions(relayUrl, { headers }),
|
||||||
);
|
);
|
||||||
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
|
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
|
||||||
|
keepC2SSocketAlive(ws);
|
||||||
const sockets = new Map();
|
const sockets = new Map();
|
||||||
let closed = false;
|
let closed = false;
|
||||||
|
|
||||||
@@ -3083,18 +3205,16 @@ ipcMain.handle("get-c2s-tunnel-preset-default-name", () => {
|
|||||||
return `[${date}] ${computerName} (${platform} ${release})`;
|
return `[${date}] ${computerName} (${platform} ${release})`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function readSetting(key) {
|
||||||
|
const settingsPath = path.join(app.getPath("userData"), "settings.json");
|
||||||
|
if (!fs.existsSync(settingsPath)) return null;
|
||||||
|
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
||||||
|
return settings[key] !== undefined ? settings[key] : null;
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.handle("get-setting", (event, key) => {
|
ipcMain.handle("get-setting", (event, key) => {
|
||||||
try {
|
try {
|
||||||
const userDataPath = app.getPath("userData");
|
return readSetting(key);
|
||||||
const settingsPath = path.join(userDataPath, "settings.json");
|
|
||||||
|
|
||||||
if (!fs.existsSync(settingsPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingsData = fs.readFileSync(settingsPath, "utf8");
|
|
||||||
const settings = JSON.parse(settingsData);
|
|
||||||
return settings[key] !== undefined ? settings[key] : null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading setting:", error);
|
console.error("Error reading setting:", error);
|
||||||
return null;
|
return null;
|
||||||
@@ -3102,6 +3222,16 @@ ipcMain.handle("get-setting", (event, key) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("set-setting", (event, key, value) => {
|
ipcMain.handle("set-setting", (event, key, value) => {
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
return { success: false, error: "Invalid setting key" };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
key === EXTERNAL_EDITOR_SETTING &&
|
||||||
|
value &&
|
||||||
|
!dialogChosenPaths.has(value)
|
||||||
|
) {
|
||||||
|
return { success: false, error: "Choose the editor in the file dialog" };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
const settingsPath = path.join(userDataPath, "settings.json");
|
const settingsPath = path.join(userDataPath, "settings.json");
|
||||||
@@ -3125,94 +3255,6 @@ ipcMain.handle("set-setting", (event, key, value) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-session-cookie", async (_event, name, targetUrl) => {
|
|
||||||
try {
|
|
||||||
const ses = mainWindow?.webContents?.session;
|
|
||||||
if (!ses) return null;
|
|
||||||
const cookies = await ses.cookies.get({
|
|
||||||
name,
|
|
||||||
...(targetUrl ? { url: targetUrl } : {}),
|
|
||||||
});
|
|
||||||
const cookie = cookies.find((candidate) =>
|
|
||||||
cookieMatchesUrl(candidate, targetUrl),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
cookie?.value ||
|
|
||||||
getRememberedElectronAuthCookie(name, targetUrl)?.value ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to get session cookie:", error);
|
|
||||||
return getRememberedElectronAuthCookie(name, targetUrl)?.value || null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function cookieMatchesUrl(cookie, targetUrl) {
|
|
||||||
if (!targetUrl) return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const targetHost = new URL(targetUrl).hostname;
|
|
||||||
const cookieDomain = (cookie.domain || "").replace(/^\./, "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
cookieDomain === targetHost ||
|
|
||||||
targetHost.endsWith(`.${cookieDomain}`) ||
|
|
||||||
(!cookieDomain && targetHost === "localhost")
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"wait-session-cookie",
|
|
||||||
async (_event, name, targetUrl, previousValue, timeoutMs = 5000) => {
|
|
||||||
const ses = mainWindow?.webContents?.session;
|
|
||||||
if (!ses) return { success: false, error: "No Electron session" };
|
|
||||||
|
|
||||||
const existingCookies = await ses.cookies.get({
|
|
||||||
name,
|
|
||||||
...(targetUrl ? { url: targetUrl } : {}),
|
|
||||||
});
|
|
||||||
const existingCookie = existingCookies.find((cookie) =>
|
|
||||||
cookieMatchesUrl(cookie, targetUrl),
|
|
||||||
);
|
|
||||||
if (existingCookie?.value && existingCookie.value !== previousValue) {
|
|
||||||
return { success: true, value: existingCookie.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
const rememberedCookie = getRememberedElectronAuthCookie(name, targetUrl);
|
|
||||||
if (rememberedCookie?.value && rememberedCookie.value !== previousValue) {
|
|
||||||
return { success: true, value: rememberedCookie.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
ses.cookies.off("changed", onCookieChanged);
|
|
||||||
resolve({ success: false, error: "Timed out waiting for cookie" });
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
function onCookieChanged(_event, cookie, _cause, removed) {
|
|
||||||
if (
|
|
||||||
removed ||
|
|
||||||
cookie.name !== name ||
|
|
||||||
!cookie.value ||
|
|
||||||
cookie.value === previousValue ||
|
|
||||||
!cookieMatchesUrl(cookie, targetUrl)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timeout);
|
|
||||||
ses.cookies.off("changed", onCookieChanged);
|
|
||||||
resolve({ success: true, value: cookie.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
ses.cookies.on("changed", onCookieChanged);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle("clear-session-cookies", async () => {
|
ipcMain.handle("clear-session-cookies", async () => {
|
||||||
try {
|
try {
|
||||||
clearPersistedElectronAuthCookies();
|
clearPersistedElectronAuthCookies();
|
||||||
@@ -3340,7 +3382,9 @@ ipcMain.handle("show-save-dialog", async (_event, options) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("show-open-dialog", async (_event, options) => {
|
ipcMain.handle("show-open-dialog", async (_event, options) => {
|
||||||
return dialog.showOpenDialog(mainWindow, options || {});
|
const result = await dialog.showOpenDialog(mainWindow, options || {});
|
||||||
|
for (const filePath of result.filePaths) dialogChosenPaths.add(filePath);
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
function toLocalFileType(stat) {
|
function toLocalFileType(stat) {
|
||||||
@@ -3663,6 +3707,15 @@ ipcMain.handle("cleanup-temp-file", (_event, tempId) => {
|
|||||||
|
|
||||||
ipcMain.handle("open-external-editor", async (_event, fileData) => {
|
ipcMain.handle("open-external-editor", async (_event, fileData) => {
|
||||||
try {
|
try {
|
||||||
|
// The editor comes from the saved setting, never from the caller.
|
||||||
|
const editorPath = readSetting(EXTERNAL_EDITOR_SETTING) || null;
|
||||||
|
if (!editorPath && isLaunchableFileName(fileData?.fileName)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Choose an external editor to open this file type",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const result = createManagedTempFile(
|
const result = createManagedTempFile(
|
||||||
fileData?.fileName,
|
fileData?.fileName,
|
||||||
fileData?.content,
|
fileData?.content,
|
||||||
@@ -3684,10 +3737,6 @@ ipcMain.handle("open-external-editor", async (_event, fileData) => {
|
|||||||
lastMtimeMs: stat.mtimeMs,
|
lastMtimeMs: stat.mtimeMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorPath =
|
|
||||||
typeof fileData?.editorPath === "string" && fileData.editorPath.trim()
|
|
||||||
? fileData.editorPath.trim()
|
|
||||||
: null;
|
|
||||||
const openError = await openPathWithEditor(result.path, editorPath);
|
const openError = await openPathWithEditor(result.path, editorPath);
|
||||||
if (openError) {
|
if (openError) {
|
||||||
closeExternalEditorSession(editId);
|
closeExternalEditorSession(editId);
|
||||||
@@ -3791,6 +3840,8 @@ app.whenReady().then(async () => {
|
|||||||
await clearElectronJwtCookiesAtStartup();
|
await clearElectronJwtCookiesAtStartup();
|
||||||
|
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
|
backendPort = await findBackendPort();
|
||||||
|
setLocalBackendPort(backendPort);
|
||||||
const result = await startBackendServer();
|
const result = await startBackendServer();
|
||||||
logToFile("startBackendServer result:", result);
|
logToFile("startBackendServer result:", result);
|
||||||
} else {
|
} else {
|
||||||
@@ -3804,6 +3855,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
createTray();
|
createTray();
|
||||||
createWindow();
|
createWindow();
|
||||||
|
startAppUpdater();
|
||||||
desktopSettings.removeOldSyncFiles();
|
desktopSettings.removeOldSyncFiles();
|
||||||
logToFile("=== Startup complete ===");
|
logToFile("=== Startup complete ===");
|
||||||
});
|
});
|
||||||
|
|||||||
+26
-10
@@ -14,7 +14,16 @@ function invokeAllowed(channel, ...args) {
|
|||||||
return ipcRenderer.invoke(channel, ...args);
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The embedded backend's port, picked by the main process at startup.
|
||||||
|
const backendPortArg = process.argv.find((arg) =>
|
||||||
|
arg.startsWith("--termix-backend-port="),
|
||||||
|
);
|
||||||
|
const backendPort = backendPortArg
|
||||||
|
? Number(backendPortArg.split("=")[1])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
backendPort,
|
||||||
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
||||||
// Kept so the interface size can reset a zoom an earlier build applied.
|
// Kept so the interface size can reset a zoom an earlier build applied.
|
||||||
setZoomFactor: (factor) => {
|
setZoomFactor: (factor) => {
|
||||||
@@ -26,6 +35,23 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
getEmbeddedServerStatus: () =>
|
getEmbeddedServerStatus: () =>
|
||||||
ipcRenderer.invoke("get-embedded-server-status"),
|
ipcRenderer.invoke("get-embedded-server-status"),
|
||||||
|
|
||||||
|
// App Store builds are updated by the store.
|
||||||
|
updates: process.mas
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
getState: () => ipcRenderer.invoke("desktop-update-get-state"),
|
||||||
|
check: () => ipcRenderer.invoke("desktop-update-check"),
|
||||||
|
download: () => ipcRenderer.invoke("desktop-update-download"),
|
||||||
|
install: () => ipcRenderer.invoke("desktop-update-install"),
|
||||||
|
setMode: (mode) => ipcRenderer.invoke("desktop-update-set-mode", mode),
|
||||||
|
onState: (callback) => {
|
||||||
|
const listener = (_event, state) => callback(state);
|
||||||
|
ipcRenderer.on("desktop-update-state", listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener("desktop-update-state", listener);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
isDev: process.env.NODE_ENV === "development",
|
isDev: process.env.NODE_ENV === "development",
|
||||||
@@ -70,16 +96,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
|
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
|
||||||
getSessionCookie: (name, targetUrl) =>
|
|
||||||
ipcRenderer.invoke("get-session-cookie", name, targetUrl),
|
|
||||||
waitForSessionCookie: (name, targetUrl, previousValue, timeoutMs) =>
|
|
||||||
ipcRenderer.invoke(
|
|
||||||
"wait-session-cookie",
|
|
||||||
name,
|
|
||||||
targetUrl,
|
|
||||||
previousValue,
|
|
||||||
timeoutMs,
|
|
||||||
),
|
|
||||||
|
|
||||||
externalBrowserLogin: (authUrl, callbackPort) =>
|
externalBrowserLogin: (authUrl, callbackPort) =>
|
||||||
ipcRenderer.invoke("external-browser-login", authUrl, callbackPort),
|
ipcRenderer.invoke("external-browser-login", authUrl, callbackPort),
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"server": "Server",
|
||||||
|
"https": "HTTPS",
|
||||||
|
"database": "Database",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"auth": "Sign in",
|
||||||
|
"proxy": "Proxies and origins",
|
||||||
|
"logging": "Logs and audit",
|
||||||
|
"plugins": "Plugins",
|
||||||
|
"desktop": "Desktop app",
|
||||||
|
"internal": "Internal"
|
||||||
|
},
|
||||||
|
"vars": [
|
||||||
|
{
|
||||||
|
"name": "PORT",
|
||||||
|
"group": "server",
|
||||||
|
"default": "8080",
|
||||||
|
"description": "Port the web UI listens on in the Docker image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATA_DIR",
|
||||||
|
"group": "server",
|
||||||
|
"default": "/app/data",
|
||||||
|
"description": "Where Termix keeps its database, keys, plugins and uploads. Mount a volume here. Outside Docker the default is ./db/data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ALLOW_EMPTY_DATA_DIR",
|
||||||
|
"group": "server",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Start even when DATA_DIR is empty but an older data folder exists elsewhere. Only set this if you really want a fresh install."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUID",
|
||||||
|
"group": "server",
|
||||||
|
"default": "1000",
|
||||||
|
"docker": true,
|
||||||
|
"description": "User id the container runs as. Match it to the owner of your data volume."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PGID",
|
||||||
|
"group": "server",
|
||||||
|
"default": "1000",
|
||||||
|
"docker": true,
|
||||||
|
"description": "Group id the container runs as."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BASE_PATH",
|
||||||
|
"group": "server",
|
||||||
|
"description": "Serve Termix under a sub path, like /termix. Set the same path in your reverse proxy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NODE_ENV",
|
||||||
|
"group": "server",
|
||||||
|
"default": "production",
|
||||||
|
"description": "Set by the image. Leave it alone."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VERSION",
|
||||||
|
"group": "server",
|
||||||
|
"description": "Set by the image to the Termix version. Leave it alone."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SETTINGS_CACHE_REFRESH_SECONDS",
|
||||||
|
"group": "server",
|
||||||
|
"default": "30",
|
||||||
|
"description": "How often each server instance reloads admin settings from the database. 0 turns the refresh off."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SSH_AUTH_SOCK",
|
||||||
|
"group": "server",
|
||||||
|
"description": "SSH agent socket used for hosts set to agent auth when they do not name their own socket."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ENABLE_SSL",
|
||||||
|
"group": "https",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Serve HTTPS. Termix makes a self-signed certificate on first boot unless you give it one."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SSL_PORT",
|
||||||
|
"group": "https",
|
||||||
|
"default": "8443",
|
||||||
|
"description": "Port for HTTPS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SSL_CERT_PATH",
|
||||||
|
"group": "https",
|
||||||
|
"default": "/app/data/ssl/termix.crt",
|
||||||
|
"description": "Certificate file for HTTPS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SSL_KEY_PATH",
|
||||||
|
"group": "https",
|
||||||
|
"default": "/app/data/ssl/termix.key",
|
||||||
|
"description": "Private key file for HTTPS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SSL_DOMAIN",
|
||||||
|
"group": "https",
|
||||||
|
"default": "localhost",
|
||||||
|
"description": "Domain put in the self-signed certificate."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_SSL_TERMINATED_BY_NGINX",
|
||||||
|
"group": "https",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by the image when nginx serves HTTPS in front of the backend."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_DIALECT",
|
||||||
|
"group": "database",
|
||||||
|
"default": "sqlite",
|
||||||
|
"description": "sqlite, postgres or mysql."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_URL",
|
||||||
|
"group": "database",
|
||||||
|
"description": "Connection URL for postgres or mysql, like postgres://user:pass@db:5432/termix."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_POOL_MAX",
|
||||||
|
"group": "database",
|
||||||
|
"default": "10",
|
||||||
|
"description": "Most connections Termix opens to postgres or mysql."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_SSL",
|
||||||
|
"group": "database",
|
||||||
|
"description": "TLS for postgres or mysql: require, no-verify or disable."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DB_FILE_ENCRYPTION",
|
||||||
|
"group": "database",
|
||||||
|
"default": "true",
|
||||||
|
"description": "Encrypt the SQLite file at rest. Set false only if something else already encrypts the disk."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DB_FILE_KEY",
|
||||||
|
"group": "database",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Only used to open SQLite files encrypted by very old versions. You almost never need it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP",
|
||||||
|
"group": "database",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Skip the backup Termix takes of the data folder before an upgrade changes the database."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_LAYER_PREUPGRADE_BACKUP_KEEP",
|
||||||
|
"group": "database",
|
||||||
|
"default": "3",
|
||||||
|
"description": "How many of those upgrade backups to keep."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DRIZZLE_MIGRATIONS_DIR",
|
||||||
|
"group": "database",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Where database migrations are read from. Set by the image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JWT_SECRET",
|
||||||
|
"group": "secrets",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Signs sign-in sessions. At least 64 characters. Made for you and saved in DATA_DIR/.env if not set. Also read from JWT_SECRET_FILE."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DATABASE_KEY",
|
||||||
|
"group": "secrets",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Key for the encrypted SQLite file, 64 hex characters. Made for you if not set. Also read from DATABASE_KEY_FILE."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ENCRYPTION_KEY",
|
||||||
|
"group": "secrets",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Wraps the data keys that encrypt passwords and SSH keys, 64 hex characters. Made for you if not set. Also read from ENCRYPTION_KEY_FILE."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "INTERNAL_AUTH_TOKEN",
|
||||||
|
"group": "secrets",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Token the server uses to call itself. Made for you if not set. Also read from INTERNAL_AUTH_TOKEN_FILE."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_REQUIRE_EXTERNAL_SECRETS",
|
||||||
|
"group": "secrets",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Refuse to start unless the four secrets above come from env vars or _FILE files, so none are written to disk."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OIDC_SYSTEM_SECRET",
|
||||||
|
"group": "secrets",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Only read to move data from installs older than 2.5. Never change it on an install that has it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WEBAUTHN_SYSTEM_SECRET",
|
||||||
|
"group": "secrets",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Only read to move data from installs older than 2.5. Never change it on an install that has it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ALLOW_PASSWORD_LOGIN",
|
||||||
|
"group": "auth",
|
||||||
|
"description": "true or false. Overrides the admin setting for signing in with a username and password."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ALLOW_REGISTRATION",
|
||||||
|
"group": "auth",
|
||||||
|
"description": "true or false. Overrides the admin setting for creating new accounts."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ALLOW_PASSWORD_RESET",
|
||||||
|
"group": "auth",
|
||||||
|
"description": "true or false. Overrides the admin setting for resetting a password from the sign in page."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EXTERNAL_ALLOW_REGISTRATION",
|
||||||
|
"group": "auth",
|
||||||
|
"description": "true or false. Overrides the admin setting for creating accounts the first time someone signs in with SSO or LDAP."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EXTERNAL_FORCE_HTTPS",
|
||||||
|
"group": "auth",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Build sign in callback URLs with https even when the request came in over http. Use it behind a proxy that does not send X-Forwarded-Proto."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OIDC_ALLOW_REGISTRATION",
|
||||||
|
"group": "auth",
|
||||||
|
"deprecated": "EXTERNAL_ALLOW_REGISTRATION",
|
||||||
|
"description": "Old name for EXTERNAL_ALLOW_REGISTRATION. Still read in 26.10."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OIDC_FORCE_HTTPS",
|
||||||
|
"group": "auth",
|
||||||
|
"deprecated": "EXTERNAL_FORCE_HTTPS",
|
||||||
|
"description": "Old name for EXTERNAL_FORCE_HTTPS. Still read in 26.10."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OIDC_CLIENT_ID",
|
||||||
|
"group": "auth",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Read by core only to move a 2.8 OIDC setup into the sso plugin. See the sso plugin for the real variables."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OIDC_ENV_OVERRIDE",
|
||||||
|
"group": "auth",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Read by core only to move a 2.8 OIDC setup into the sso plugin. See the sso plugin for the real variables."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRUSTED_PROXY_AUTH_ENABLED",
|
||||||
|
"group": "auth",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Let a proxy like Authelia or Authentik sign people in by sending their username in a header."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRUSTED_PROXY_AUTH_TRUSTED_PROXIES",
|
||||||
|
"group": "auth",
|
||||||
|
"description": "Comma separated IPs or CIDRs of the proxies allowed to send the headers. Required when trusted proxy login is on."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRUSTED_PROXY_AUTH_USERNAME_HEADER",
|
||||||
|
"group": "auth",
|
||||||
|
"default": "x-forwarded-username",
|
||||||
|
"description": "Header that holds the username."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRUSTED_PROXY_AUTH_ROLE_HEADER",
|
||||||
|
"group": "auth",
|
||||||
|
"default": "x-forwarded-role",
|
||||||
|
"description": "Header that holds the user's groups or roles."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRUSTED_PROXY_AUTH_ROLE_MAP",
|
||||||
|
"group": "auth",
|
||||||
|
"description": "JSON map from proxy roles to Termix roles, like {\"admins\":[\"admin\"],\"staff\":[\"user\"]}. Required when trusted proxy login is on."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRUSTED_PROXIES",
|
||||||
|
"group": "proxy",
|
||||||
|
"docker": true,
|
||||||
|
"description": "Comma separated IPs or CIDRs nginx takes the client IP from (X-Forwarded-For). Set it to your reverse proxy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CORS_ALLOWED_ORIGINS",
|
||||||
|
"group": "proxy",
|
||||||
|
"description": "Comma separated extra origins allowed to call the API from a browser."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_ALLOWED_ORIGINS",
|
||||||
|
"group": "proxy",
|
||||||
|
"description": "Comma separated extra origins allowed to open WebSockets."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HTTP_PROXY",
|
||||||
|
"group": "proxy",
|
||||||
|
"description": "Proxy for outbound HTTP requests, like registry downloads and webhooks."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HTTPS_PROXY",
|
||||||
|
"group": "proxy",
|
||||||
|
"description": "Proxy for outbound HTTPS requests."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NO_PROXY",
|
||||||
|
"group": "proxy",
|
||||||
|
"description": "Comma separated hosts that skip the proxy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LOG_LEVEL",
|
||||||
|
"group": "logging",
|
||||||
|
"default": "info",
|
||||||
|
"description": "debug, info, warn or error."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LOG_TIMESTAMP_FORMAT",
|
||||||
|
"group": "logging",
|
||||||
|
"description": "iso or 24h. Unset uses the local time format."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AUDIT_LOG_RETENTION_DAYS",
|
||||||
|
"group": "logging",
|
||||||
|
"description": "Delete audit entries older than this many days. Unset keeps them until the entry cap."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AUDIT_LOG_MAX_ENTRIES",
|
||||||
|
"group": "logging",
|
||||||
|
"default": "10000",
|
||||||
|
"description": "Most audit entries kept. The oldest go first."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AUDIT_LOG_FORWARD_URL",
|
||||||
|
"group": "logging",
|
||||||
|
"description": "Also POST every audit entry as JSON to this URL. Overrides the admin setting."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AUDIT_LOG_FORWARD_TOKEN",
|
||||||
|
"group": "logging",
|
||||||
|
"secret": true,
|
||||||
|
"description": "Bearer token sent with forwarded audit entries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SESSION_RECORDING_RETENTION_DAYS",
|
||||||
|
"group": "logging",
|
||||||
|
"deprecated": "the session-recording plugin setting",
|
||||||
|
"description": "Only read once to carry a 2.8 value into the session-recording plugin."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_PLUGIN_REGISTRY_URL",
|
||||||
|
"group": "plugins",
|
||||||
|
"description": "Use another plugin index instead of the official registry."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_COMMUNITY_REGISTRY_URL",
|
||||||
|
"group": "plugins",
|
||||||
|
"description": "Use another plugin index instead of the community registry. Its plugins still have to be signed with the community key."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_PLUGIN_STATS_URL",
|
||||||
|
"group": "plugins",
|
||||||
|
"description": "Where plugin download counts are read from. Defaults to stats.json next to the registry index."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_REQUIRE_SIGNED_PLUGINS",
|
||||||
|
"group": "plugins",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Only load plugins signed by a trusted key. Blocks uploads of unsigned files even in developer mode."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_BUNDLED_PLUGINS_DIR",
|
||||||
|
"group": "plugins",
|
||||||
|
"description": "Where the plugins shipped with Termix are read from. Set by the image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_PLUGIN_PUBLIC_RATE_LIMIT",
|
||||||
|
"group": "plugins",
|
||||||
|
"default": "120",
|
||||||
|
"description": "Requests per minute one IP may make to a plugin's public routes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PLUGIN_MAX_KV_KEYS",
|
||||||
|
"group": "plugins",
|
||||||
|
"default": "10000",
|
||||||
|
"description": "Most key value entries one plugin may store."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_DEV_RELOAD",
|
||||||
|
"group": "plugins",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by npm run dev so plugin rebuilds reload without a restart."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_DEV_RUNNER",
|
||||||
|
"group": "plugins",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by npm run dev."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ENABLE_INSECURE_MODE",
|
||||||
|
"group": "desktop",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Desktop app only. Skip TLS certificate checks for the linked server. Only for testing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ELECTRON_DISABLE_GPU",
|
||||||
|
"group": "desktop",
|
||||||
|
"description": "Desktop app only. Set to 1 to turn off GPU acceleration if the window is blank or flickers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_LOCAL_SHELL",
|
||||||
|
"group": "desktop",
|
||||||
|
"description": "Desktop app only. Shell the local terminal opens, like /bin/zsh or pwsh.exe."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SHELL",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Your login shell, used by the local terminal when TERMIX_LOCAL_SHELL is not set."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ELECTRON_EMBEDDED",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by the desktop app when it runs its own backend."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EMBEDDED_BACKEND_PORT",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by the desktop app to the port its own backend listens on. 30001 unless another program holds it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TERMIX_DATA_DIR",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by the desktop app to its data folder."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XDG_CURRENT_DESKTOP",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Read on Linux to pick the password store and window behavior."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "APPIMAGE",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by AppImage. Tells the desktop app it can update itself."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FLATPAK_ID",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Set by Flatpak. The desktop app links new versions instead of installing them."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DESKTOP_SESSION",
|
||||||
|
"group": "desktop",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Read on Linux to pick the password store."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VITE_BASE_PATH",
|
||||||
|
"group": "internal",
|
||||||
|
"internal": true,
|
||||||
|
"description": "Base path baked in at build time."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+3
-3
@@ -53,7 +53,7 @@ export default tseslint.config([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// MySQL has no RETURNING clause, and drizzle's mysql-core does not expose
|
// MySQL has no RETURNING clause, and drizzle's mysql-core does not expose
|
||||||
// the method at all — a bare .returning() is a TypeError there, not a bad
|
// the method at all. A bare .returning() is a TypeError there, not a bad
|
||||||
// query, and it only fails on the engine no test in this repo runs against.
|
// query, and it only fails on the engine no test in this repo runs against.
|
||||||
//
|
//
|
||||||
// 175 call sites were migrated off it. This is what stops number 176.
|
// 175 call sites were migrated off it. This is what stops number 176.
|
||||||
@@ -85,14 +85,14 @@ export default tseslint.config([
|
|||||||
{
|
{
|
||||||
// Postgres and SQLite spell it ON CONFLICT; MySQL spells it ON
|
// Postgres and SQLite spell it ON CONFLICT; MySQL spells it ON
|
||||||
// DUPLICATE KEY and names no columns, so drizzle's mysql-core has no
|
// DUPLICATE KEY and names no columns, so drizzle's mysql-core has no
|
||||||
// onConflictDoUpdate at all — another TypeError, not a bad query.
|
// onConflictDoUpdate at all, another TypeError, not a bad query.
|
||||||
selector: "CallExpression[callee.property.name='onConflictDoUpdate']",
|
selector: "CallExpression[callee.property.name='onConflictDoUpdate']",
|
||||||
message:
|
message:
|
||||||
"MySQL has no ON CONFLICT. Use upsert() from ./returning.js.",
|
"MySQL has no ON CONFLICT. Use upsert() from ./returning.js.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// better-sqlite3 puts these on a write result; node-postgres and
|
// better-sqlite3 puts these on a write result; node-postgres and
|
||||||
// mysql2 do not, so reading them directly yields undefined — and
|
// mysql2 do not, so reading them directly yields undefined, and
|
||||||
// Number(undefined) is NaN, which reaches the database as the string
|
// Number(undefined) is NaN, which reaches the database as the string
|
||||||
// "NaN" and fails an integer column. Three call sites did exactly
|
// "NaN" and fails an integer column. Three call sites did exactly
|
||||||
// this and only broke on Postgres.
|
// this and only broke on Postgres.
|
||||||
|
|||||||
+1
-30
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
@@ -20,35 +20,6 @@
|
|||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<title>Termix</title>
|
<title>Termix</title>
|
||||||
<style>
|
<style>
|
||||||
.hide-scrollbar {
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skinny-scrollbar {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: #4a4a4a #1e1e21;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skinny-scrollbar::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skinny-scrollbar::-webkit-scrollbar-track {
|
|
||||||
background: #1e1e21;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skinny-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #4a4a4a;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #1e1e21;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-scrollbar {
|
.toolbar-scrollbar {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #4a4a4a transparent;
|
scrollbar-color: #4a4a4a transparent;
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://unpkg.com/knip@latest/schema.json",
|
|
||||||
"workspaces": {
|
|
||||||
".": {
|
|
||||||
"entry": [
|
|
||||||
"src/main.tsx",
|
|
||||||
"src/backend/starter.ts",
|
|
||||||
"src/backend/utils/swagger.ts",
|
|
||||||
"src/ui/plugin-host/sdk-ui.ts",
|
|
||||||
"src/ui/plugin-host/testing-host.tsx",
|
|
||||||
"src/**/tests/**/*.{ts,tsx}",
|
|
||||||
"scripts/**/*.{cjs,mjs}",
|
|
||||||
"electron/main.cjs",
|
|
||||||
"electron/preload.js",
|
|
||||||
"vitest.config.ts",
|
|
||||||
"vitest.setup.ts",
|
|
||||||
"vite.config.ts"
|
|
||||||
],
|
|
||||||
"project": [
|
|
||||||
"src/**/*.{ts,tsx}",
|
|
||||||
"electron/**/*.cjs",
|
|
||||||
"scripts/**/*.{cjs,mjs}"
|
|
||||||
],
|
|
||||||
"ignoreBinaries": ["powershell"],
|
|
||||||
"ignoreDependencies": [
|
|
||||||
"@codemirror/theme-one-dark",
|
|
||||||
"@codemirror/view",
|
|
||||||
"@electron/notarize",
|
|
||||||
"@radix-ui/react-alert-dialog",
|
|
||||||
"@radix-ui/react-checkbox",
|
|
||||||
"@radix-ui/react-label",
|
|
||||||
"@radix-ui/react-popover",
|
|
||||||
"@radix-ui/react-scroll-area",
|
|
||||||
"@radix-ui/react-select",
|
|
||||||
"@radix-ui/react-slider",
|
|
||||||
"@radix-ui/react-slot",
|
|
||||||
"@radix-ui/react-switch",
|
|
||||||
"@radix-ui/react-tabs",
|
|
||||||
"@types/better-sqlite3",
|
|
||||||
"@types/cookie-parser",
|
|
||||||
"@types/cors",
|
|
||||||
"@types/express",
|
|
||||||
"@types/jsonwebtoken",
|
|
||||||
"@types/multer",
|
|
||||||
"@types/ssh2",
|
|
||||||
"@uiw/react-codemirror",
|
|
||||||
"@xterm/addon-clipboard",
|
|
||||||
"axios",
|
|
||||||
"bcryptjs",
|
|
||||||
"better-sqlite3",
|
|
||||||
"body-parser",
|
|
||||||
"chalk",
|
|
||||||
"class-variance-authority",
|
|
||||||
"clsx",
|
|
||||||
"cmdk",
|
|
||||||
"cookie-parser",
|
|
||||||
"cors",
|
|
||||||
"drizzle-orm",
|
|
||||||
"express",
|
|
||||||
"jsonwebtoken",
|
|
||||||
"multer",
|
|
||||||
"nanoid",
|
|
||||||
"i18next-browser-languagedetector",
|
|
||||||
"lucide-react",
|
|
||||||
"motion",
|
|
||||||
"radix-ui",
|
|
||||||
"socks",
|
|
||||||
"ssh2",
|
|
||||||
"undici",
|
|
||||||
"husky",
|
|
||||||
"sharp",
|
|
||||||
"sonner",
|
|
||||||
"tailwind-merge"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"packages/plugin-sdk": {
|
|
||||||
"entry": ["src/*.ts", "src/testing/*.ts", "cli/**/*.mjs"],
|
|
||||||
"project": ["src/**/*.ts", "cli/**/*.mjs"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+66
-44
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.0",
|
"version": "26.10.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.0",
|
"version": "26.10.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^2.4.0",
|
||||||
"@tanstack/react-virtual": "^3.14.13",
|
"@tanstack/react-virtual": "^3.14.13",
|
||||||
"@types/compression": "^1.8.1",
|
"@types/compression": "^1.8.1",
|
||||||
"@types/semver": "^7.8.0",
|
"@types/semver": "^7.8.0",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"electron-updater": "^6.8.9",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"motion": "^13.1.1",
|
"motion": "^13.1.1",
|
||||||
@@ -105,6 +107,7 @@
|
|||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"i18next": "^26.4.2",
|
"i18next": "^26.4.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"js-yaml": "^4.3.2",
|
||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
"lint-staged": "^17.2.0",
|
"lint-staged": "^17.2.0",
|
||||||
"lucide-react": "^1.33.0",
|
"lucide-react": "^1.33.0",
|
||||||
@@ -142,7 +145,6 @@
|
|||||||
"version": "14.0.1",
|
"version": "14.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
|
||||||
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
|
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
@@ -159,7 +161,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -169,14 +170,12 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@apidevtools/swagger-parser": {
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
"version": "12.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
|
||||||
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
|
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/json-schema-ref-parser": "14.0.1",
|
"@apidevtools/json-schema-ref-parser": "14.0.1",
|
||||||
@@ -1093,7 +1092,6 @@
|
|||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@deadendjs/swagger-jsdoc/-/swagger-jsdoc-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@deadendjs/swagger-jsdoc/-/swagger-jsdoc-8.2.0.tgz",
|
||||||
"integrity": "sha512-KwySDId/J0C/dlMGd77H3IN3nL7/HYVTerFp6DBhRnIY7RsbGGz30DBIyAhzhLDdqStW4wQHMP+/3qX8BQ1wTg==",
|
"integrity": "sha512-KwySDId/J0C/dlMGd77H3IN3nL7/HYVTerFp6DBhRnIY7RsbGGz30DBIyAhzhLDdqStW4wQHMP+/3qX8BQ1wTg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^12.1.0",
|
"@apidevtools/swagger-parser": "^12.1.0",
|
||||||
@@ -2702,7 +2700,6 @@
|
|||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
||||||
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
|
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -2882,7 +2879,6 @@
|
|||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
|
||||||
"integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
|
"integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20.19.0"
|
"node": ">= 20.19.0"
|
||||||
@@ -6236,7 +6232,6 @@
|
|||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/jsonwebtoken": {
|
"node_modules/@types/jsonwebtoken": {
|
||||||
@@ -7001,7 +6996,6 @@
|
|||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
@@ -7018,7 +7012,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||||||
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ajv": "^8.5.0"
|
"ajv": "^8.5.0"
|
||||||
@@ -7218,7 +7211,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/argue-cli": {
|
"node_modules/argue-cli": {
|
||||||
@@ -7391,7 +7383,6 @@
|
|||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
@@ -7506,7 +7497,6 @@
|
|||||||
"version": "5.0.12",
|
"version": "5.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||||
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
@@ -7601,7 +7591,6 @@
|
|||||||
"version": "9.7.0",
|
"version": "9.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
|
||||||
"integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
|
"integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -7748,7 +7737,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
@@ -7974,7 +7962,6 @@
|
|||||||
"version": "14.0.3",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
@@ -8272,7 +8259,6 @@
|
|||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
@@ -9031,6 +9017,34 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-updater": {
|
||||||
|
"version": "6.8.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz",
|
||||||
|
"integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"builder-util-runtime": "9.7.0",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lazy-val": "^1.0.5",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"semver": "~7.7.3",
|
||||||
|
"tiny-typed-emitter": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron/node_modules/@types/node": {
|
"node_modules/electron/node_modules/@types/node": {
|
||||||
"version": "24.19.0",
|
"version": "24.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz",
|
||||||
@@ -9578,7 +9592,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-json-stable-stringify": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
@@ -9606,7 +9619,6 @@
|
|||||||
"version": "3.1.8",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||||
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -9775,7 +9787,6 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
@@ -9888,7 +9899,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -10038,7 +10048,6 @@
|
|||||||
"version": "12.0.0",
|
"version": "12.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz",
|
||||||
"integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==",
|
"integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foreground-child": "^3.3.1",
|
"foreground-child": "^3.3.1",
|
||||||
@@ -10584,7 +10593,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/istanbul-lib-coverage": {
|
"node_modules/istanbul-lib-coverage": {
|
||||||
@@ -10643,7 +10651,6 @@
|
|||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||||
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
|
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/cliui": "^9.0.0"
|
"@isaacs/cliui": "^9.0.0"
|
||||||
@@ -10700,7 +10707,6 @@
|
|||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -10799,7 +10805,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
@@ -10826,7 +10831,6 @@
|
|||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
@@ -10892,7 +10896,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
@@ -11241,6 +11244,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -11253,6 +11262,13 @@
|
|||||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isinteger": {
|
"node_modules/lodash.isinteger": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
@@ -11490,7 +11506,6 @@
|
|||||||
"version": "10.2.6",
|
"version": "10.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||||
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^5.0.8"
|
"brace-expansion": "^5.0.8"
|
||||||
@@ -12004,7 +12019,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
@@ -12075,7 +12089,6 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -12085,7 +12098,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^11.0.0",
|
"lru-cache": "^11.0.0",
|
||||||
@@ -12102,7 +12114,6 @@
|
|||||||
"version": "11.5.3",
|
"version": "11.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
||||||
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
|
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
@@ -12905,7 +12916,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -13049,7 +13059,6 @@
|
|||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
||||||
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
@@ -13236,7 +13245,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shebang-regex": "^3.0.0"
|
"shebang-regex": "^3.0.0"
|
||||||
@@ -13249,7 +13257,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -13338,7 +13345,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -13812,6 +13818,12 @@
|
|||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-typed-emitter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
@@ -14113,7 +14125,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
@@ -14805,7 +14816,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
@@ -14977,7 +14987,6 @@
|
|||||||
"version": "2.0.0-4",
|
"version": "2.0.0-4",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-4.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-4.tgz",
|
||||||
"integrity": "sha512-MoQoNhTFI400tkaeod+X0Vety1KD2L9dUa6pa1CVcyfcATjC/iDxoMLvqZ6U3D8c5KzxBrU2HnJH+PfaXOqI7w==",
|
"integrity": "sha512-MoQoNhTFI400tkaeod+X0Vety1KD2L9dUa6pa1CVcyfcATjC/iDxoMLvqZ6U3D8c5KzxBrU2HnJH+PfaXOqI7w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
@@ -15049,9 +15058,10 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin-sdk": {
|
"packages/plugin-sdk": {
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.0.2",
|
"version": "1.0.11",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@deadendjs/swagger-jsdoc": "^8.1.2",
|
||||||
"@tailwindcss/node": "^4.2.4",
|
"@tailwindcss/node": "^4.2.4",
|
||||||
"@tailwindcss/oxide": "^4.2.4",
|
"@tailwindcss/oxide": "^4.2.4",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.1",
|
||||||
@@ -15087,9 +15097,21 @@
|
|||||||
"drizzle-orm": {
|
"drizzle-orm": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sonner": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"ssh2": {
|
"ssh2": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
|||||||
+8
-8
@@ -4,7 +4,7 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "26.10.0",
|
"version": "26.10.2",
|
||||||
"description": "Self-hosted, plugin-based server management.",
|
"description": "Self-hosted, plugin-based server management.",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main.cjs",
|
"main": "electron/main.cjs",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-node-pty.cjs && node scripts/patch-ssh2-agent.cjs",
|
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-node-pty.cjs && node scripts/patch-ssh2-agent.cjs",
|
||||||
"prepare": "husky || true",
|
"prepare": "husky || true",
|
||||||
"prebuild": "node scripts/write-electron-build-info.cjs",
|
"prebuild": "node scripts/write-electron-build-info.cjs",
|
||||||
"lint": "node scripts/generate-dialect-schema.cjs --check && eslint . && node scripts/check-importmap-csp.cjs && node scripts/check-core-plugin-ids.cjs && node scripts/check-sdk-ui-exports.cjs && node scripts/generate-settings-search-index.cjs --check && node scripts/check-test-locations.cjs && node scripts/check-changelog.cjs",
|
"lint": "node scripts/generate-dialect-schema.cjs --check && eslint . && node scripts/check-importmap-csp.cjs && node scripts/check-core-plugin-ids.cjs && node scripts/check-sdk-ui-exports.cjs && node scripts/check-sdk-guide.cjs && node scripts/generate-settings-search-index.cjs --check && node scripts/check-test-locations.cjs && node scripts/check-changelog.cjs && node scripts/check-env-catalog.cjs",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"type-check": "npm run build:sdk && tsc -b --force",
|
"type-check": "npm run build:sdk && tsc -b --force",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
"dev:electron": "node scripts/dev.mjs electron",
|
"dev:electron": "node scripts/dev.mjs electron",
|
||||||
"dev:docker": "node scripts/dev.mjs docker",
|
"dev:docker": "node scripts/dev.mjs docker",
|
||||||
"dev:frontend": "vite",
|
"dev:frontend": "vite",
|
||||||
"build": "npm run build:sdk && vite build && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
"build": "npm run build:sdk && vite build && node scripts/stamp-sw-build-info.cjs && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
||||||
"build:backend": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
"build:backend": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
||||||
"generate:openapi": "tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && node ./dist/backend/backend/utils/swagger.js",
|
"generate:openapi": "npm run build:sdk && node scripts/generate-openapi.mjs",
|
||||||
"electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty",
|
"electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty",
|
||||||
"build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir",
|
"build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir",
|
||||||
"build:win-installer": "npm run build && npm run electron:rebuild && electron-builder --win --publish=never",
|
"build:win-installer": "npm run build && npm run electron:rebuild && electron-builder --win --publish=never",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"plugins:pin": "node scripts/pin-bundled-plugins.cjs"
|
"plugins:pin": "node scripts/pin-bundled-plugins.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^2.4.0",
|
||||||
"@tanstack/react-virtual": "^3.14.13",
|
"@tanstack/react-virtual": "^3.14.13",
|
||||||
"@types/compression": "^1.8.1",
|
"@types/compression": "^1.8.1",
|
||||||
"@types/semver": "^7.8.0",
|
"@types/semver": "^7.8.0",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"electron-updater": "^6.8.9",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"motion": "^13.1.1",
|
"motion": "^13.1.1",
|
||||||
@@ -140,6 +142,7 @@
|
|||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"i18next": "^26.4.2",
|
"i18next": "^26.4.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"js-yaml": "^4.3.2",
|
||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
"lint-staged": "^17.2.0",
|
"lint-staged": "^17.2.0",
|
||||||
"lucide-react": "^1.33.0",
|
"lucide-react": "^1.33.0",
|
||||||
@@ -166,10 +169,7 @@
|
|||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
],
|
],
|
||||||
"*.{js,jsx}": [
|
"*.{js,jsx,mjs,cjs,json,css,md}": [
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{json,css,md}": [
|
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,80 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.11
|
||||||
|
|
||||||
|
Plugin API 1.3.0.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ctx.auth.issueUserToken()` mints a short-lived session for a user, for calling Termix's own API as them from background work. Needs `users:impersonate` and `engine.api` 1.3
|
||||||
|
- `ctx.users.getLocale()` reads a user's UI language. Needs `users:read` and `engine.api` 1.3
|
||||||
|
|
||||||
|
## 1.0.10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `termix-plugin pack` includes the plugin's `LICENSE` and `NOTICE` files, so third-party notices ship with it
|
||||||
|
|
||||||
|
## 1.0.9
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Worker entries in `package.json` `termix.workers`, each bundled on its own into `dist/workers/<name>.js`
|
||||||
|
- A `.wasm`, `.ttf` or `.otf` import gives the file's bytes, in `termix-plugin build` and in plugin tests
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The plugin guide listed jszip, guacamole-lite and @anthropic-ai/sdk as shared packages, but they are bundled
|
||||||
|
|
||||||
|
## 1.0.8
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `publicRateLimit` on `ctx.http.router()`, for a plugin whose public routes serve a proxied page
|
||||||
|
|
||||||
|
## 1.0.7
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A migration file, or a part of one, holding only comments made core fail to start the plugin
|
||||||
|
|
||||||
|
## 1.0.6
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `PLUGIN-GUIDE.md`, a guide to building, testing and releasing a plugin, written so AI coding tools can read it too
|
||||||
|
|
||||||
|
## 1.0.5
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `execCommand` timeout errors no longer include the command, which could hold a sudo password
|
||||||
|
- `termix-plugin validate` fails when a plugin ships migrations for some database engines but not all three
|
||||||
|
- `termix-plugin migrations` loads the table definitions from a path with a `#` or other URL characters in it
|
||||||
|
- Text, json and encrypted columns are `longtext` on MySQL in new migrations, so they are no longer capped at 64KB. Existing migrations keep `text`
|
||||||
|
- `i18next`, `react-dom`, `react-i18next` and `sonner` are optional peer dependencies, so npm stops installing them into backend-only plugins
|
||||||
|
- `termix-plugin validate` accepts a beta version like `1.1.0-beta.1` when the changelog's newest release is `1.1.0` or older, so beta releases pass
|
||||||
|
|
||||||
|
## 1.0.4
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `termix-plugin build` puts a plugin's own Tailwind classes in core's utilities layer, so responsive, hover and group variants and `className` overrides on SDK components work. Plain classes core already ships stay below core's utilities as before, while variants core ships go with the plugin's own so `max-h-56 md:max-h-none` keeps its order. Rebuild a plugin to pick it up
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
Plugin API 1.2.0.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `docs` manifest field: an https link to the plugin's docs. Termix shows it as a Documentation link on the plugin's page and in its settings
|
||||||
|
- `env` manifest field: the environment variables the plugin reads, listed in its docs and on its page
|
||||||
|
- `app.docs` and `useDocsUrl()` for links into the plugin's own docs, and `DocsLink` in `@termix-ssh/plugin-sdk/ui`
|
||||||
|
- `PanelShell` takes a `docs` link and shows it as a book icon in the header
|
||||||
|
- `@termix-ssh/plugin-sdk/openapi` with `buildOpenApi()`, and `termix-plugin openapi`, which writes `dist/openapi.json` from the `@openapi` blocks in `src/backend`. `build` runs it too
|
||||||
|
- `@termix-ssh/plugin-sdk/docs` with `pluginDocsUrl()`, `pluginDocsPage()` and `pluginEnvVars()`
|
||||||
|
- `termix-plugin validate` warns when `docs/index.md` is missing or the source reads an env var the manifest does not list
|
||||||
|
|
||||||
## 1.0.2
|
## 1.0.2
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,823 @@
|
|||||||
|
# Termix plugin guide
|
||||||
|
|
||||||
|
SDK 1.0.11 | Plugin API 1.3.0
|
||||||
|
|
||||||
|
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
||||||
|
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
||||||
|
Run `npm update @termix-ssh/plugin-sdk` to get the newest one.
|
||||||
|
|
||||||
|
If this guide and the SDK's TypeScript types disagree, the types are right. They live next to this file
|
||||||
|
in `dist/*.d.ts`. The long form of everything here is at https://docs.termix.site/develop, and the raw
|
||||||
|
Markdown of each page is at
|
||||||
|
`https://raw.githubusercontent.com/Termix-SSH/Docs/main/develop/<page>.md` (for example
|
||||||
|
`develop/backend.md`), which is easier for tools to read than the site.
|
||||||
|
|
||||||
|
## What a plugin is
|
||||||
|
|
||||||
|
Termix is self-hosted, plugin-based server management. Core keeps hosts, credentials, users, sharing and
|
||||||
|
the shell. Everything else, even the SSH terminal, is a plugin built with this SDK.
|
||||||
|
|
||||||
|
A plugin is:
|
||||||
|
|
||||||
|
- `manifest.json`: what it is, what it adds and what it may do. Termix reads it without running any code.
|
||||||
|
- A backend, `src/backend/index.ts`, run inside the Termix server process. It exports `activate(ctx)`.
|
||||||
|
- A frontend, `src/frontend/index.tsx`, run in the browser in the same page as Termix. It exports
|
||||||
|
`activate(app)`.
|
||||||
|
|
||||||
|
There is no sandbox. Capabilities make a plugin honest and easy to audit, but code runs with the server's
|
||||||
|
rights. Write it as if an admin will read every line, because reviewers do.
|
||||||
|
|
||||||
|
## Hard rules
|
||||||
|
|
||||||
|
Break one of these and the plugin fails to load, fails validation or gets rejected in review.
|
||||||
|
|
||||||
|
1. Import only `@termix-ssh/plugin-sdk/*`, your own files and npm packages. Never import Termix's own code.
|
||||||
|
2. Create everything inside `activate`. No servers, timers, sockets or connections at the top of a module.
|
||||||
|
Disable then enable runs `activate` again on the same module, and it must work without a restart.
|
||||||
|
3. Clean up what you make yourself with `ctx.disposables.add(fn)` (backend) or `app.onDispose(fn)`
|
||||||
|
(frontend). Things made through `ctx` and `app` are cleaned up for you.
|
||||||
|
4. Never call `process.exit` or touch signals.
|
||||||
|
5. Never open your own SSH connection. Use `ctx.ssh` so jump hosts, proxies, host keys, every auth type and
|
||||||
|
sharing work the way the user set them up.
|
||||||
|
6. Outbound HTTP goes through `ctx.fetch` (needs `network:outbound`), not a raw `fetch` or axios.
|
||||||
|
7. Ask for the fewest capabilities you can. A call that needs one you didn't list throws
|
||||||
|
`PluginCapabilityError`.
|
||||||
|
8. Never take a user id from a request body or a service caller. Use `ctx.currentActor()`.
|
||||||
|
9. Check permissions on the backend. Frontend checks only hide UI.
|
||||||
|
10. Call `await ctx.db.persist()` after every database write. On SQLite a write without it can be lost on
|
||||||
|
restart.
|
||||||
|
11. Never use `.returning()` in a query. MySQL doesn't have it. Read a new row back by a key you chose.
|
||||||
|
12. Never edit a migration that has shipped. Termix checks a checksum and refuses to start the plugin. Add a
|
||||||
|
new one.
|
||||||
|
13. Never index a `text()` or `json()` column. Use `varchar(n)` for anything you index.
|
||||||
|
14. Never hard code UI text. Every string goes in `locales/en.json` and is read with `t()`.
|
||||||
|
15. The plugin `id` never changes once released. It names your tables, routes and permissions.
|
||||||
|
16. The version in `manifest.json`, `package.json` and the newest `CHANGELOG.md` section must match.
|
||||||
|
17. Put an `@openapi` JSDoc block above every route.
|
||||||
|
18. No obfuscated or minified source, and no code downloaded and run at runtime.
|
||||||
|
19. No tracking, telemetry or network calls the user didn't ask for. If you need one, make it off by
|
||||||
|
default and explain it in your docs.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
manifest.json what the plugin is, adds and may do
|
||||||
|
package.json npm scripts and dependencies
|
||||||
|
CHANGELOG.md release notes, one section per version
|
||||||
|
README.md the repo front page
|
||||||
|
AGENTS.md notes for AI tools working on this plugin
|
||||||
|
docs/index.md the plugin's docs page (more pages next to it)
|
||||||
|
src/backend/index.ts activate(ctx), runs in the Termix server
|
||||||
|
src/backend/tables.ts table definitions, exports `tables`
|
||||||
|
src/backend/routes.ts HTTP routes
|
||||||
|
src/frontend/index.tsx activate(app), runs in the browser
|
||||||
|
locales/en.json every string the plugin shows
|
||||||
|
locales/translated/ other languages, like de_DE.json (optional)
|
||||||
|
migrations/ sqlite/, postgres/, mysql/ and snapshot.json, made by npm run migrations
|
||||||
|
tests/backend/ backend tests (Node)
|
||||||
|
tests/frontend/ frontend tests (jsdom)
|
||||||
|
vitest.config.ts uses the SDK preset
|
||||||
|
.github/workflows/ ci.yml and release.yml, calling shared workflows in Termix-Registry
|
||||||
|
dist/ build output, never commit it
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build # termix-plugin build: dist/backend.js, dist/frontend.js, CSS, openapi.json
|
||||||
|
npm test # termix-plugin test: Vitest
|
||||||
|
npm run typecheck # tsc
|
||||||
|
npm run validate # termix-plugin validate: manifest, files, migrations, changelog, docs
|
||||||
|
npm run migrations # termix-plugin migrations: write migrations after changing tables.ts
|
||||||
|
npm run pack # termix-plugin pack: write <id>-<version>.tmxplug (with LICENSE and NOTICE)
|
||||||
|
npm run format # Prettier
|
||||||
|
npm run format:check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Done checklist
|
||||||
|
|
||||||
|
Before you call a change finished:
|
||||||
|
|
||||||
|
- [ ] `npm test` passes
|
||||||
|
- [ ] `npm run typecheck` passes
|
||||||
|
- [ ] `npm run validate` passes with no warnings you can fix
|
||||||
|
- [ ] `npm run build` passes
|
||||||
|
- [ ] `npm run format:check` passes
|
||||||
|
- [ ] `npx termix-plugin migrations --check` passes (you ran `npm run migrations` after changing tables)
|
||||||
|
- [ ] New strings are in `locales/en.json`, and `plugin.name` / `plugin.description` match the manifest
|
||||||
|
- [ ] New routes have an `@openapi` block and a test, including without the permission
|
||||||
|
- [ ] New env vars are listed in `env` in the manifest
|
||||||
|
- [ ] `docs/` and the "About this plugin" section of `AGENTS.md` still match what the plugin does
|
||||||
|
- [ ] User facing changes are in `CHANGELOG.md` under the version they will ship in
|
||||||
|
|
||||||
|
## Manifest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@termix-ssh/plugin-sdk/schema/manifest.schema.json",
|
||||||
|
"id": "hello",
|
||||||
|
"name": "Hello",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Says hello and keeps a few notes.",
|
||||||
|
"author": { "name": "You", "url": "https://example.com" },
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"repository": "https://github.com/you/termix-plugin-hello",
|
||||||
|
"docs": "https://github.com/you/termix-plugin-hello/tree/main/docs",
|
||||||
|
"category": "Productivity",
|
||||||
|
"icon": "Hand",
|
||||||
|
"features": ["A tab from the sidebar", "Notes per user"],
|
||||||
|
"engine": { "termix": ">=26.10.0", "api": "^1.2" },
|
||||||
|
"capabilities": ["db:own", "network:serve", "ui:surface"],
|
||||||
|
"env": [],
|
||||||
|
"contributes": {
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"id": "hello",
|
||||||
|
"titleKey": "tab.title",
|
||||||
|
"icon": "Hand",
|
||||||
|
"openFrom": ["rail"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"name": "use",
|
||||||
|
"titleKey": "permissions.use.title",
|
||||||
|
"descriptionKey": "permissions.use.description",
|
||||||
|
"defaultRoles": ["admin", "user"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locales": "locales",
|
||||||
|
"platforms": ["linux", "win32", "darwin"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Unknown fields are refused at every level, so a typo fails validation. The schema at
|
||||||
|
`node_modules/@termix-ssh/plugin-sdk/schema/manifest.schema.json` has every field.
|
||||||
|
|
||||||
|
| Field | Notes |
|
||||||
|
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `id` | Lowercase, starts with a letter, 2 to 40 characters, letters, numbers and dashes. Can't be an official plugin's id. |
|
||||||
|
| `version` | Semver. Betas are `X.Y.Z-beta.N`. |
|
||||||
|
| `category` | One of: Terminal, Files & Transfer, Infrastructure, Monitoring, Networking, Access & Security, Productivity. |
|
||||||
|
| `icon` | A [Lucide](https://lucide.dev/icons/) icon name, like `Hand`. |
|
||||||
|
| `features` | Up to 20 short lines shown on the plugin's page. |
|
||||||
|
| `docs` | An https link to your docs. |
|
||||||
|
| `engine.termix` | Termix versions you work with, like `>=26.10.0`. |
|
||||||
|
| `engine.api` | Plugin API range, like `^1.2`. A major bump of the API is breaking. |
|
||||||
|
| `capabilities` | What your code may do. See the reference at the end. |
|
||||||
|
| `dependencies`, `optionalDependencies` | Other plugins, `{ "snippets": "^1.0.0" }`. You must keep working without optional ones. |
|
||||||
|
| `provides`, `requires` | Services you offer and use. |
|
||||||
|
| `env` | Every environment variable your backend reads. `validate` warns about unlisted `process.env.X`. Prefer settings. |
|
||||||
|
| `desktop` | `mirror` (default), `local` or `server`: how a linked desktop app treats the plugin. |
|
||||||
|
| `nativeDependencies` | npm packages with `.node` bindings. Avoid them: community plugins can't count on them. |
|
||||||
|
|
||||||
|
`contributes` holds `tabs`, `panels`, `dashboardCards`, `actions`, `actionSlots`, `permissions`,
|
||||||
|
`settings`, `protocols`, `auth`, `syncEntities`, `keybindingActions`, `http`, `guest`, `guestViews`,
|
||||||
|
`uiPresets` and `uses`. Frontend `register*` calls need their ids declared here first.
|
||||||
|
|
||||||
|
Capabilities and permissions are different things. A capability (`hosts:read`, with a colon) is what your
|
||||||
|
code may reach. A permission (`hello.use`, with a dot) is what a user may do.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Router } from "express";
|
||||||
|
import type { PluginContext } from "@termix-ssh/plugin-sdk/backend";
|
||||||
|
import { notes } from "./tables.js";
|
||||||
|
import { registerRoutes } from "./routes.js";
|
||||||
|
|
||||||
|
export async function activate(ctx: PluginContext) {
|
||||||
|
const notesTable = await ctx.db.define(notes);
|
||||||
|
registerRoutes(ctx.http.router<Router>(), ctx, notesTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deactivate() {
|
||||||
|
// Everything registered through ctx is disposed by core.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundled as ESM for Node 22+ into `dist/backend.js`. Backend imports of your own files use `.js`.
|
||||||
|
|
||||||
|
### What ctx has
|
||||||
|
|
||||||
|
| Member | Capability | What it does |
|
||||||
|
| ---------------------- | -------------------------------- | -------------------------------------------------------------- |
|
||||||
|
| `pluginId`, `manifest` | none | Your id and manifest |
|
||||||
|
| `log` | none | Log lines tagged with your plugin |
|
||||||
|
| `db` | `db:own` | Your own tables |
|
||||||
|
| `kv` | `kv:own` | Small key value state |
|
||||||
|
| `files.dataDir()` | `files:own` | A folder of your own |
|
||||||
|
| `settings` | none | Your settings |
|
||||||
|
| `secrets` | `secrets:own` | Encrypted values per user, `seal`/`unseal` for your own tables |
|
||||||
|
| `http`, `ws` | `network:serve` | Routes and sockets |
|
||||||
|
| `rbac` | none | Check the actor's permissions |
|
||||||
|
| `hosts` | `hosts:read`, `hosts:write` | List, read, create, update and share hosts as the actor |
|
||||||
|
| `ssh` | `ssh:connect`, `credentials:use` | Connect to hosts through Termix's SSH pipeline |
|
||||||
|
| `credentials` | varies | The actor's saved keys, and protocol logins |
|
||||||
|
| `services` | per service | Call other plugins and offer your own |
|
||||||
|
| `events` | `events:core` for core topics | Emit and listen for events |
|
||||||
|
| `notify` | `notify:send` | Send alerts to the Alerts inbox |
|
||||||
|
| `fetch` | `network:outbound` | Outbound HTTP with SSRF protection |
|
||||||
|
| `process` | `process:spawn` | Run programs on the server |
|
||||||
|
| `schedule` | none | Timers that stop on disable and never overlap |
|
||||||
|
| `audit.record` | none | Write your own audit line |
|
||||||
|
| `sync` | none | Sync your tables to a linked desktop |
|
||||||
|
| `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
|
||||||
|
| `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
|
||||||
|
| `users.getLocale` | `users:read` | A user's UI language |
|
||||||
|
| `system` | `system:tls` | The server's HTTPS certificate |
|
||||||
|
| `plugins.list()` | `plugins:read` | Which plugins are installed |
|
||||||
|
| `asUser(userId, fn)` | always audited | Run background work as a user |
|
||||||
|
| `currentActor()` | none | The user the current call runs for |
|
||||||
|
| `disposables.add(fn)` | none | Extra cleanup on disable |
|
||||||
|
|
||||||
|
### The actor
|
||||||
|
|
||||||
|
In a route the actor is the signed-in user, set by Termix. `ctx.currentActor()` returns it. Background
|
||||||
|
work with no request (a timer, an event) uses `ctx.asUser(userId, fn)`, which is audited and still applies
|
||||||
|
that user's permissions.
|
||||||
|
|
||||||
|
Background work that must call Termix's own HTTP API (the Termix CLI, say) asks for a session with
|
||||||
|
`ctx.auth.issueUserToken(userId, { purpose, ttlSeconds })` and sends it as `Authorization: Bearer <token>`.
|
||||||
|
It needs `users:impersonate` and `engine.api` `^1.3`, lasts 300 seconds by default and 900 at most, and is
|
||||||
|
audited as `plugin_user_token_issued`. The user's permissions apply, and it is refused on routes that change
|
||||||
|
their password, second factors, API keys or linked logins. `ctx.users.getLocale(userId)` gives the user's
|
||||||
|
UI language, like `zh_CN`, or null, for writing to them in it.
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
Routes live at `/plugin-api/<id>/`, sockets at `/plugin-ws/<id>/`, on the main Termix port. You never need
|
||||||
|
your own port or an nginx change. Before your handler runs, Termix checks sign in, sets the actor, answers
|
||||||
|
503 if the plugin is off, parses JSON up to 2 MB and catches errors.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
router.use(ctx.rbac.require("use") as never);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /plugin-api/hello/notes:
|
||||||
|
* post:
|
||||||
|
* summary: Add a note for the signed-in user
|
||||||
|
* tags:
|
||||||
|
* - Hello
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* text:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Note added
|
||||||
|
* 400:
|
||||||
|
* description: Missing or empty text
|
||||||
|
*/
|
||||||
|
router.post("/notes", async (req, res) => {
|
||||||
|
const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
|
||||||
|
if (!text) return res.status(400).json({ error: "text is required" });
|
||||||
|
const db = await ctx.db.client<BetterSQLite3Database>();
|
||||||
|
await db
|
||||||
|
.insert(notesTable)
|
||||||
|
.values({ userId: ctx.currentActor() as string, text });
|
||||||
|
await ctx.db.persist();
|
||||||
|
res.status(201).json({ success: true });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `@openapi` path is the full path, starting with `/plugin-api/<id>/`. `build` turns these into
|
||||||
|
`dist/openapi.json` and the docs site renders them.
|
||||||
|
- `ctx.http.router({ public: ["/webhook/:token"], bodyLimit: "20mb", rawBody: false })`. Public routes
|
||||||
|
have no actor: check their token yourself. Core allows 120 public requests a minute per address;
|
||||||
|
`publicRateLimit` raises that for a proxied page that loads its own assets.
|
||||||
|
- `ctx.http.baseUrl(req)` is the public address, for callback URLs.
|
||||||
|
- `ctx.ws.route("/live", (socket, conn) => ...)`. Long lived sockets should check
|
||||||
|
`conn.isDataUnlocked()` per message.
|
||||||
|
- `ctx.rbac.require("manage")` on a single route, `await ctx.rbac.has("manage")` in code. A full id like
|
||||||
|
`hosts.edit` checks a core permission.
|
||||||
|
- Host access: `ctx.hosts.checkAccess(hostId, "connect" | "view" | "edit" | "manage")`.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
Errors from routes, sockets, listeners and timers count against an error budget. Five in a minute and
|
||||||
|
Termix turns the plugin off (state `failed`). A 4xx you send on purpose doesn't count. Validate input and
|
||||||
|
send 400s instead of throwing.
|
||||||
|
|
||||||
|
### SSH
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { client, dispose } = await ctx.ssh.connect(hostId, {
|
||||||
|
purpose: "hello",
|
||||||
|
profile: "background",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// use the ssh2 client
|
||||||
|
} finally {
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ctx.ssh.withConnection(hostId, { pool, purpose }, fn)` borrows a pooled connection for frequent short
|
||||||
|
commands. `@termix-ssh/plugin-sdk/host-commands` has helpers to detect the OS and run package manager and
|
||||||
|
sudo commands. Hosts from `ctx.hosts` never carry secrets. Pass the host or its id to `ctx.ssh`.
|
||||||
|
|
||||||
|
### Events, timers, alerts, services
|
||||||
|
|
||||||
|
```ts
|
||||||
|
ctx.events.on("host.deleted", ({ hostId }) => cleanUp(hostId)); // core topics need events:core
|
||||||
|
ctx.events.emit("plugin.hello.greeted", { name }); // your own: plugin.<id>.<name>
|
||||||
|
ctx.schedule.every(60_000, () => refresh(), { jitterMs: 5_000 });
|
||||||
|
```
|
||||||
|
|
||||||
|
- Alerts go through `ctx.notify.send` (`notify:send`). Never build your own notification system.
|
||||||
|
- Offer a service: declare it in `provides` with a version and a permission, then
|
||||||
|
`ctx.services.provide("hello.greetings", impl)`. The name starts with your id. Methods read the caller
|
||||||
|
from `ctx.currentActor()`.
|
||||||
|
- Use a service: declare it in `requires`, then `ctx.services.get<T>("snippets.access")`. It returns
|
||||||
|
nothing while no provider runs, so always handle that.
|
||||||
|
|
||||||
|
### Shared packages
|
||||||
|
|
||||||
|
These come from Termix and are never bundled: `@termix-ssh/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`,
|
||||||
|
`cookie-parser`, `axios`, `drizzle-orm`, `sharp` and every Node builtin. List the ones you use in
|
||||||
|
`peerDependencies`. Any other npm package is bundled.
|
||||||
|
|
||||||
|
### Workers and WebAssembly
|
||||||
|
|
||||||
|
Heavy work (decoding, encoding, parsing) belongs in a worker thread so it never blocks the server. Name
|
||||||
|
each worker entry in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"termix": { "workers": { "engine": "src/backend/engine/worker.ts" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
`build` bundles it alone into `dist/workers/engine.js`; start it with
|
||||||
|
`new Worker(new URL("./workers/engine.js", import.meta.url))`. A worker bundles everything it imports and
|
||||||
|
can't use the shared packages above, so it must not import `@termix-ssh/plugin-sdk`. Talk to it with
|
||||||
|
`postMessage`.
|
||||||
|
|
||||||
|
Importing a `.wasm`, `.ttf` or `.otf` file gives its bytes as a `Uint8Array`, in `build` and in tests, so a module ships
|
||||||
|
inside the bundle with no file to find. Compile it once on the main thread with `new WebAssembly.Module`
|
||||||
|
and post that to each worker. Add `declare module "*.wasm" { const bytes: Uint8Array; export default bytes; }`
|
||||||
|
to a `.d.ts` in the plugin.
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
Needs `db:own`. Define tables once in TypeScript, generate migrations for SQLite, PostgreSQL and MySQL,
|
||||||
|
query with [Drizzle](https://orm.drizzle.team/).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/backend/tables.ts
|
||||||
|
import {
|
||||||
|
defineTable,
|
||||||
|
id,
|
||||||
|
refUser,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
} from "@termix-ssh/plugin-sdk/db";
|
||||||
|
|
||||||
|
export const notes = defineTable(
|
||||||
|
"notes",
|
||||||
|
{
|
||||||
|
id: id(),
|
||||||
|
userId: refUser(),
|
||||||
|
text: text().notNull(),
|
||||||
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
{ indexes: [{ name: "idx_p_hello_notes_user_id", columns: ["userId"] }] },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tables = [notes]; // termix-plugin migrations reads this
|
||||||
|
```
|
||||||
|
|
||||||
|
The table is stored as `p_<id with dashes as underscores>_notes`. Index names should use the same prefix.
|
||||||
|
|
||||||
|
| Builder | Type |
|
||||||
|
| ---------------------------------------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `id()` | Auto increment key |
|
||||||
|
| `text()` | Any length string. Not indexable |
|
||||||
|
| `varchar(n)` | String up to `n` (255 default). Indexable |
|
||||||
|
| `integer()`, `bigint()`, `real()`, `boolean()` | Numbers and booleans |
|
||||||
|
| `timestamp()` | A time, stored as text |
|
||||||
|
| `json()` | JSON stored as text. Not indexable |
|
||||||
|
| `encryptedText()` | Text encrypted with the owning user's key |
|
||||||
|
| `refUser()` | A user id. Rows are deleted with the user and included in their data export |
|
||||||
|
| `refHost()` | A host id. Rows are deleted with the host |
|
||||||
|
|
||||||
|
Modifiers: `.notNull()`, `.unique()`, `.primaryKey()`, `.default(value)`, `.defaultNow()`.
|
||||||
|
|
||||||
|
After any change to `tables.ts`, run `npm run migrations` and read the three new files. Renames and type
|
||||||
|
changes are refused rather than guessed: write those by hand, for all three databases. A migration may only
|
||||||
|
touch your own tables. `createTestDb` applies them in tests, so a broken one fails `npm test`.
|
||||||
|
|
||||||
|
Query through `const db = await ctx.db.client()`, then `await ctx.db.persist()` after writes
|
||||||
|
(`persist({ lazy: true })` for frequent low value writes). `ctx.db.dialect` says which database runs.
|
||||||
|
|
||||||
|
Other storage: `ctx.kv` for a few values, `ctx.settings` for settings, `ctx.secrets` for secrets,
|
||||||
|
`ctx.files.dataDir()` for files. To sync a table to a linked desktop, use `ctx.sync.registerEntity` and
|
||||||
|
`contributes.syncEntities`. The entity name is part of the protocol and never changes.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
useTranslation,
|
||||||
|
type TabProps,
|
||||||
|
type TermixApp,
|
||||||
|
} from "@termix-ssh/plugin-sdk/frontend";
|
||||||
|
import { EmptyState, PanelShell } from "@termix-ssh/plugin-sdk/ui";
|
||||||
|
import { Hand } from "lucide-react";
|
||||||
|
|
||||||
|
function HelloTab(_props: TabProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<PanelShell icon={<Hand className="size-4" />} title={t("tab.title")}>
|
||||||
|
<EmptyState title={t("tab.empty")} />
|
||||||
|
</PanelShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activate(app: TermixApp) {
|
||||||
|
app.registerRailItem({
|
||||||
|
id: "hello",
|
||||||
|
icon: Hand,
|
||||||
|
titleKey: "tab.title",
|
||||||
|
kind: "tab",
|
||||||
|
permission: "use",
|
||||||
|
});
|
||||||
|
app.registerTab("hello", HelloTab, {
|
||||||
|
icon: Hand,
|
||||||
|
titleKey: "tab.title",
|
||||||
|
singleton: true,
|
||||||
|
hostless: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every `register*` returns an undo function, and everything is undone on disable. Each component runs in
|
||||||
|
your plugin's error boundary and Suspense.
|
||||||
|
|
||||||
|
| Member | What it does |
|
||||||
|
| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||||
|
| `registerRailItem` | A sidebar rail button |
|
||||||
|
| `registerTab`, `registerPanel` | A tab type or sidebar panel (id in `contributes.tabs` / `panels`) |
|
||||||
|
| `registerDashboardCard` | A dashboard card (`contributes.dashboardCards`) |
|
||||||
|
| `registerHostAction` | A host button, palette entry or default connect |
|
||||||
|
| `registerHostEditorSection` | Your own host editor section (host settings get one for free) |
|
||||||
|
| `registerHostProtocol` | A connection protocol next to SSH |
|
||||||
|
| `registerHostBadge`, `registerHostContextMenuItem` | Badges and menu items on host rows |
|
||||||
|
| `registerPaletteEntry`, `registerPaletteGroup` | Command palette entries |
|
||||||
|
| `registerKeybindingAction`, `registerKeybindingDefault` | Bindable actions |
|
||||||
|
| `registerSettingsComponent` | A component for a `custom` settings field |
|
||||||
|
| `registerAction`, `invokeAction` | Actions across plugins |
|
||||||
|
| `registerSlotContribution`, `declareActionSlot` | Fill or own a UI slot |
|
||||||
|
| `registerComponent`, `registerExtension` | Offer a component by id, add to an extension point |
|
||||||
|
| `registerOnboardingStep` | A first run setup step |
|
||||||
|
| `registerLoginMethod`, `registerSecondFactorUI`, `registerSshAuthEditor` | Sign in UI |
|
||||||
|
| `api` | Axios client for your `/plugin-api/<id>/` routes, auth included |
|
||||||
|
| `fetch(path)`, `wsUrl(path)` | Raw fetch for streams, socket URL and auth |
|
||||||
|
| `docs`, `tabs`, `confirm`, `listHosts`, `getHost`, `t`, `hasPermission`, `desktop` | Helpers |
|
||||||
|
| `onSettingsChanged`, `onDispose` | Hooks |
|
||||||
|
|
||||||
|
Hooks from `@termix-ssh/plugin-sdk/frontend`: `useTranslation`, `usePermission`, `useSettings`, `useHost`,
|
||||||
|
`useHosts`, `usePluginApi`, `useCurrentUser`, `useTheme`, `useToast`, `useDocsUrl`, `useTabs`,
|
||||||
|
`useSlotContributions`, `useSshAuthTypes`, `useExtensions`.
|
||||||
|
|
||||||
|
Shared from Termix and never bundled: `react`, `react-dom`, `i18next`, `react-i18next`, `sonner` and the
|
||||||
|
SDK's `frontend` and `ui`. Anything else, like `lucide-react`, is bundled. A host your code gets only carries
|
||||||
|
your own host settings, under `host.pluginSettings[<id>]`.
|
||||||
|
|
||||||
|
Slots you can fill include `terminal.toolbar`, `terminal.toolbarStatus`, `terminal.sidePanel`,
|
||||||
|
`terminal.overlay`, `dashboard.counters`, `dashboard.hostMetrics`, `admin.userTabs`, `hosts.importMenu`,
|
||||||
|
`hosts.panel`, `credentials.badges`, `shell.overlay`, `shell.rightEdge` and `tab.menu`. See
|
||||||
|
https://docs.termix.site/develop/more-extension-points.
|
||||||
|
|
||||||
|
## Looking native
|
||||||
|
|
||||||
|
Build UI from `@termix-ssh/plugin-sdk/ui` so it matches Termix and follows the theme. The full list of
|
||||||
|
exports with props is `ui.api.txt` in the Termix repo
|
||||||
|
(https://github.com/Termix-SSH/Termix/blob/main/packages/plugin-sdk/ui.api.txt).
|
||||||
|
|
||||||
|
- Square: panels, cards and containers have no rounded corners. Small controls may stay rounded.
|
||||||
|
- Flat: borders, not shadows. Use theme tokens like `border-border`, `bg-background`,
|
||||||
|
`text-muted-foreground`, `text-destructive`. One accent, `text-accent-brand`. Don't add colors.
|
||||||
|
- Small type: most text 11 to 13px.
|
||||||
|
- No new modal dialogs. Ask with `useConfirm()`, open views in place with `InlineView`, and use
|
||||||
|
`PanePrompt` for small prompts.
|
||||||
|
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
||||||
|
forms end with `FormFooter`, empty lists show `EmptyState`.
|
||||||
|
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
||||||
|
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
||||||
|
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
||||||
|
desktop layout.
|
||||||
|
- Tailwind classes work. `build` compiles the ones you use against Termix's theme.
|
||||||
|
- Link to your docs with `DocsLink` or `PanelShell`'s `docs` prop, never a hard coded URL.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Declare them in `contributes.settings` and Termix draws the form, stores values, encrypts secrets and
|
||||||
|
checks saves. You don't build settings UI.
|
||||||
|
|
||||||
|
- Scopes: `admin` (for everyone), `user` (each person), `host` (a section in the host editor, with an
|
||||||
|
`enableKey` switch first).
|
||||||
|
- Types: `boolean`, `string`, `number`, `select`, `multiselect`, `secret`, `textarea`, `json`, `custom`.
|
||||||
|
- Options: `labelKey`, `descriptionKey`, `placeholderKey`, `default`, `options`, `min`, `max`, `requires`,
|
||||||
|
`group`, `permission`, `hidden`, `component`.
|
||||||
|
- Read: `ctx.settings.get(key)`, `getUser(userId, key)`, `getHost(hostId, key)`. Write: `set`, `setHost`.
|
||||||
|
React: `ctx.settings.onChange`. Check: `ctx.settings.onValidate("admin", values => ({ key: "message" }))`.
|
||||||
|
- Frontend: `useSettings("user")` gives values and `save`.
|
||||||
|
|
||||||
|
Prefer settings to environment variables. Use an env var only for something needed before Termix starts.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
Declare short names in `contributes.permissions`. Termix registers them as `<id>.<name>`. `defaultRoles`
|
||||||
|
gives them to the built in `admin` and `user` roles on first install. You can't claim a core group
|
||||||
|
(`hosts`, `credentials`, `admin`) or another plugin's. Labels are locale keys.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
Every string goes in `locales/en.json`, nested by area. Manifest keys (`titleKey`, `labelKey`) and `t()`
|
||||||
|
read it from your own namespace. Unknown keys fall back to Termix's shared strings like `common.save`.
|
||||||
|
Plurals use `_one` and `_other` with `t("hosts", { count })`. Other languages go in
|
||||||
|
`locales/translated/<code>.json` (like `de_DE.json`) with the same keys. Keep `{{placeholders}}` as they are.
|
||||||
|
Untranslated strings show in English.
|
||||||
|
|
||||||
|
Termix shows your plugin's name and description in the plugin manager, the setup picker and settings.
|
||||||
|
Put them in `locales/en.json` as `plugin.name` and `plugin.description`, with the same text as `name` and
|
||||||
|
`description` in the manifest, so they get translated too. Without them, the English manifest text shows in
|
||||||
|
every language.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin": {
|
||||||
|
"name": "Host Metrics",
|
||||||
|
"description": "Live CPU, memory, disk, network and GPU stats."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
`docs/index.md` is the first page: what the plugin does in a sentence or two, how to turn it on, how to use
|
||||||
|
it. More pages sit next to it with `title` and `order` front matter. Plain Markdown, no MDX. Images go in
|
||||||
|
`docs/img/`.
|
||||||
|
|
||||||
|
Don't write by hand what is generated: features, settings, permissions, capabilities, env vars, services,
|
||||||
|
the API reference (from `@openapi`) and the changelog. Write short sentences in plain words and name
|
||||||
|
buttons exactly as the UI does.
|
||||||
|
|
||||||
|
## Auth and protocols
|
||||||
|
|
||||||
|
Login methods, second factors and SSH auth types need `auth:provide`, which is high risk and reviewed
|
||||||
|
closely. Declare ids in `contributes.auth` and register them with `ctx.auth.registerLoginMethod`,
|
||||||
|
`registerSecondFactor` or `registerSshAuthProvider`. Connection protocols go in `contributes.protocols`
|
||||||
|
and `app.registerHostProtocol`. Read https://docs.termix.site/develop/auth before starting one.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// vitest.config.ts
|
||||||
|
import { pluginVitestConfig } from "@termix-ssh/plugin-sdk/vitest-preset";
|
||||||
|
export default pluginVitestConfig(import.meta.url);
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend tests go in `tests/backend/` (Node), frontend tests in `tests/frontend/` (jsdom). Never next to the
|
||||||
|
source.
|
||||||
|
|
||||||
|
Backend, from `@termix-ssh/plugin-sdk/testing`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const db = await createTestDb(pluginDir); // in memory SQLite, your migrations, stub users/ssh_data/roles
|
||||||
|
let router: Router | null = null;
|
||||||
|
const mock = createMockCtx({
|
||||||
|
pluginId: manifest.id,
|
||||||
|
manifest,
|
||||||
|
capabilities: manifest.capabilities, // calls outside these throw, like the real thing
|
||||||
|
db: db.database,
|
||||||
|
router: () => (router = express.Router()),
|
||||||
|
permissions: ["hello.use"], // leave out to allow everything
|
||||||
|
});
|
||||||
|
await activate(mock.ctx);
|
||||||
|
mock.setActor("user-1");
|
||||||
|
```
|
||||||
|
|
||||||
|
`createMockCtx` also takes `hosts` and `services`. `createFakeContext()` is a looser double that skips the
|
||||||
|
capability checks. The template's `tests/backend/routes.test.ts` serves the router with Express and calls
|
||||||
|
it with `fetch`: start from it.
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { renderWithApp } from "@termix-ssh/plugin-sdk/testing";
|
||||||
|
import * as plugin from "../../src/frontend/index.js";
|
||||||
|
|
||||||
|
const app = await renderWithApp(plugin, {
|
||||||
|
api: { get: async () => ({ data: [] }) },
|
||||||
|
});
|
||||||
|
expect(app.registered.tabs()).toContain("hello");
|
||||||
|
```
|
||||||
|
|
||||||
|
Test every route, with and without the permission. Test that disable then enable works, and that the plugin
|
||||||
|
works when an optional dependency is missing.
|
||||||
|
|
||||||
|
## Running it on a server
|
||||||
|
|
||||||
|
1. You need Node 24 and a Termix server (26.10 or newer) you are an admin on. A local Docker one is fine.
|
||||||
|
2. In Termix, open Settings, General and turn on Plugin developer mode.
|
||||||
|
3. Make an API key in Settings, API keys.
|
||||||
|
4. Run `npx termix-plugin dev --server http://localhost:30001 --key tmx_...` (or set `TERMIX_SERVER_URL`
|
||||||
|
and `TERMIX_API_KEY`). It builds, installs and reinstalls on every save. Reload the page for frontend
|
||||||
|
changes. `--once` installs once.
|
||||||
|
|
||||||
|
Or `npm run build && npm run pack` and install the `.tmxplug` from Plugins, Install from file. Dev installs
|
||||||
|
are marked unverified. You can't use the id of a plugin that ships with Termix, and a registry copy of the
|
||||||
|
same plugin must be uninstalled first.
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
Plugins use semver. Work happens on a `dev-X.Y.Z` branch made from `main`, named after the version it will
|
||||||
|
ship as. `main` only changes when a stable release merges a dev branch into it.
|
||||||
|
|
||||||
|
`CHANGELOG.md`, newest first, no `## Unreleased` section and no dates:
|
||||||
|
|
||||||
|
```md
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A wave button in the terminal toolbar
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Notes keep their order after a restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Sections: `### Added`, `### Changed`, `### Fixed`, `### Removed`, `### Deprecated`, `### Security`.
|
||||||
|
`termix-plugin changelog --version 1.1.0` prints one version's notes.
|
||||||
|
|
||||||
|
Run the Release workflow by hand from the dev branch on GitHub:
|
||||||
|
|
||||||
|
| Choice | What it does |
|
||||||
|
| --------- | --------------------------------------------------------------------------------- |
|
||||||
|
| stable | Tests, builds and publishes `X.Y.Z`, merges the branch into `main` and deletes it |
|
||||||
|
| beta | Publishes `X.Y.Z-beta.N`, N counts up on its own |
|
||||||
|
| overwrite | Releases the manifest version again from this commit |
|
||||||
|
| dry-run | Builds and packs, publishes nothing |
|
||||||
|
|
||||||
|
No secrets are needed. The workflow records GitHub build provenance for the `.tmxplug`, and the run summary
|
||||||
|
shows the `sha256` for a registry submission. If a dev branch changes `.github/workflows`, add a
|
||||||
|
`TERMIX_PAT` secret (a token with `repo` and `workflow` scopes). To also sign with your own key, set
|
||||||
|
`TERMIX_PLUGIN_SIGNING_KEY`. Keep `.github/workflows/release.yml` as the template has it: the community
|
||||||
|
registry checks provenance from it.
|
||||||
|
|
||||||
|
## Submitting to the community registry
|
||||||
|
|
||||||
|
Before you submit:
|
||||||
|
|
||||||
|
- The repo is public with an open source license, made from the template or using its `release.yml`.
|
||||||
|
- `npm test`, `npm run typecheck`, `npm run validate` and `npm run build` pass.
|
||||||
|
- The `id` isn't used by an official plugin or another submission.
|
||||||
|
- `name`, `description`, `author`, `repository` (your repo) and `docs` (or a README that explains use) are
|
||||||
|
set.
|
||||||
|
|
||||||
|
Then release a stable or beta version and fork https://github.com/Termix-SSH/Termix-Registry. Add
|
||||||
|
`community/plugins/<id>.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "../../community-submission-schema.json",
|
||||||
|
"id": "my-plugin",
|
||||||
|
"repository": "https://github.com/you/termix-plugin-my-plugin",
|
||||||
|
"maintainers": ["your-github-username"],
|
||||||
|
"versions": [
|
||||||
|
{ "version": "1.0.0", "sha256": "<from the release run summary>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Open a pull request. The Check submission job downloads the `v<version>` release and checks the `sha256`,
|
||||||
|
that the manifest inside has your id, version and repo, that it has build provenance from the release
|
||||||
|
workflow, and that you are a maintainer. Then a person reviews the source at the tag and merges it. The
|
||||||
|
registry signs it with the community key and lists it a few minutes later.
|
||||||
|
|
||||||
|
Reviewers check that:
|
||||||
|
|
||||||
|
- The source does what the name and description say, and nothing else.
|
||||||
|
- Nothing is obfuscated or minified, and no code is downloaded and run at runtime.
|
||||||
|
- There are no network calls, tracking or telemetry the user didn't ask for.
|
||||||
|
- Capabilities and permissions are the fewest needed.
|
||||||
|
- `package.json` scripts and dependencies don't fetch or run anything unexpected at install or build.
|
||||||
|
- Secrets are only used for what the plugin is for.
|
||||||
|
- The name and icon don't pretend to be an official plugin.
|
||||||
|
|
||||||
|
Every version needs its own PR that adds it to the top of `versions`. Overwriting a reviewed release drops
|
||||||
|
it from the registry until a new version is submitted. Admins see the plugin once they turn on the community
|
||||||
|
registry in Plugins, Registries.
|
||||||
|
|
||||||
|
To run your own registry instead, see https://docs.termix.site/develop/registries.
|
||||||
|
|
||||||
|
## termix-plugin CLI
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| ------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||||
|
| `build` | Bundle `dist/backend.js`, `dist/frontend.js`, workers, CSS, locales, migrations, `openapi.json` |
|
||||||
|
| `validate` | Check the manifest, its files, migrations for every database, the changelog and `package.json` |
|
||||||
|
| `test` | Apply patches, run Vitest |
|
||||||
|
| `migrations` | Write a migration per database from `tables.ts`. `--check` fails when one is missing |
|
||||||
|
| `openapi` | Write `dist/openapi.json` from `@openapi` blocks |
|
||||||
|
| `pack` | Validate, then write `<id>-<version>.tmxplug`. `--out <dir>` |
|
||||||
|
| `sign <file>` | Sign a `.tmxplug` with `TERMIX_PLUGIN_SIGNING_KEY` |
|
||||||
|
| `verify <file> --key <base64>` | Check a `.tmxplug` against its `.sig` |
|
||||||
|
| `keygen` | Write a new signing key pair. `--out <dir>` |
|
||||||
|
| `changelog` | Print one version's notes. `--version x.y.z` |
|
||||||
|
| `patch` | Apply the plugin's dependency patches (`build` and `test` do this too) |
|
||||||
|
| `dev` | Build, pack and install on a server, again on every change. `--server`, `--key`, `--once` |
|
||||||
|
|
||||||
|
## SDK entry points
|
||||||
|
|
||||||
|
| Import | What it is |
|
||||||
|
| -------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||||
|
| `@termix-ssh/plugin-sdk/backend` | `PluginContext` and the backend types |
|
||||||
|
| `@termix-ssh/plugin-sdk/frontend` | `TermixApp`, `TabProps` and the hooks |
|
||||||
|
| `@termix-ssh/plugin-sdk/ui` | The UI kit, served by Termix at runtime |
|
||||||
|
| `@termix-ssh/plugin-sdk/db` | `defineTable` and column builders |
|
||||||
|
| `@termix-ssh/plugin-sdk/manifest` | `PluginManifest` type and manifest checks |
|
||||||
|
| `@termix-ssh/plugin-sdk/capabilities` | The capability catalog |
|
||||||
|
| `@termix-ssh/plugin-sdk/settings` | Settings field validation |
|
||||||
|
| `@termix-ssh/plugin-sdk/testing` | `createMockCtx`, `createTestDb`, `renderWithApp`, `createFakeContext` |
|
||||||
|
| `@termix-ssh/plugin-sdk/vitest-preset` | `pluginVitestConfig` |
|
||||||
|
| `@termix-ssh/plugin-sdk/host-commands` | OS detection, package manager and sudo helpers over ssh2 |
|
||||||
|
| `@termix-ssh/plugin-sdk/ssh-certs` | `applyCertificateAuth()` for certificate based SSH auth |
|
||||||
|
| `@termix-ssh/plugin-sdk/desktop` | The desktop app bridge, typed. Check it exists before use |
|
||||||
|
| `@termix-ssh/plugin-sdk/openapi` | `buildOpenApi()` |
|
||||||
|
| `@termix-ssh/plugin-sdk/changelog` | Changelog parsing and checks |
|
||||||
|
| `@termix-ssh/plugin-sdk/docs` | Docs link and env var helpers |
|
||||||
|
| `@termix-ssh/plugin-sdk/ddl`, `@termix-ssh/plugin-sdk/table-builder` | Table to SQL and Drizzle mapping. You rarely need these |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Capability | Risk | What it allows |
|
||||||
|
| -------------------- | -------- | ------------------------------------------------------- |
|
||||||
|
| `credentials:read` | critical | Read stored secrets for hosts the user can reach |
|
||||||
|
| `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks |
|
||||||
|
| `ssh:connect` | high | Run any command on hosts, with the user's access |
|
||||||
|
| `process:spawn` | high | Run programs on the Termix server |
|
||||||
|
| `users:write` | high | Add users and change their roles |
|
||||||
|
| `users:impersonate` | high | Act as another user, or get a session for them, audited |
|
||||||
|
| `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
|
||||||
|
| `auth:provide` | high | Add login methods, second factors, SSH auth types |
|
||||||
|
| `device:serial` | high | Open serial and USB ports on the server |
|
||||||
|
| `notify:hub` | high | Receive every alert (the alerts hub only) |
|
||||||
|
| `hosts:write` | medium | Add and edit hosts |
|
||||||
|
| `credentials:use` | medium | Connect with saved credentials without seeing them |
|
||||||
|
| `credentials:write` | medium | Save new credentials |
|
||||||
|
| `network:outbound` | medium | Outbound HTTP through `ctx.fetch` |
|
||||||
|
| `network:serve` | medium | HTTP routes and sockets |
|
||||||
|
| `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN |
|
||||||
|
| `users:read` | medium | Usernames, roles and UI language |
|
||||||
|
| `events:core` | medium | Listen to core events |
|
||||||
|
| `notify:send` | medium | Send alerts |
|
||||||
|
| `audit:read` | medium | Read the audit log |
|
||||||
|
| `desktop:window` | medium | Open windows in the desktop app |
|
||||||
|
| `hosts:read` | low | Read hosts the user can see |
|
||||||
|
| `db:own` | low | Your own tables |
|
||||||
|
| `kv:own` | low | Small key value state |
|
||||||
|
| `files:own` | low | Your own folder on disk |
|
||||||
|
| `secrets:own` | low | Your own encrypted secrets |
|
||||||
|
| `settings:read-core` | low | Read Termix server settings |
|
||||||
|
| `plugins:read` | low | See installed plugins |
|
||||||
|
| `ui:surface` | low | Add tabs, panels and settings |
|
||||||
|
|
||||||
|
An update that adds a capability waits for an admin to approve it.
|
||||||
|
|
||||||
|
## Common mistakes
|
||||||
|
|
||||||
|
- Tests fail after `npm install`: usually a missing devDependency (`@testing-library/dom`, `jsdom`,
|
||||||
|
`better-sqlite3`) or a newer TypeScript major pulled in by another package. Pin `typescript` to the
|
||||||
|
template's range.
|
||||||
|
- `hello-world`, `hello_world` or `Hello World` left over from the template in `package.json`, locales,
|
||||||
|
index names, API paths, tab ids or tests.
|
||||||
|
- Changed `tables.ts` without running `npm run migrations`, or kept the template's `migrations/` folder
|
||||||
|
after renaming the id. Delete it and run `npm run migrations` once on a fresh plugin.
|
||||||
|
- A rail item or tab id that isn't declared in `contributes`.
|
||||||
|
- A string typed straight into JSX instead of `t()`.
|
||||||
|
- Forgetting `await ctx.db.persist()` after a write.
|
||||||
|
- Frontend changes not showing during `dev`: reload the page.
|
||||||
|
- An id clash with an official plugin: dev installs and registry submissions are refused.
|
||||||
|
|
||||||
|
## Where to look next
|
||||||
|
|
||||||
|
- Docs: https://docs.termix.site/develop
|
||||||
|
- Official plugins are all open source and built the same way. Plugin-Workspaces is a small one to start
|
||||||
|
from, Plugin-Docker runs commands on hosts, Plugin-Snippets offers a service, Plugin-Automations uses
|
||||||
|
services, Plugin-SSO and Plugin-TOTP add sign in, Plugin-Vault adds an SSH auth type and
|
||||||
|
Plugin-Remote-Desktop adds a protocol. All at https://github.com/Termix-SSH.
|
||||||
|
- Help: the Discord at https://discord.gg/jVQGdvHDrf. SDK bugs go in
|
||||||
|
https://github.com/Termix-SSH/Termix/issues.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg" width="120" height="120" alt="Termix Logo" />
|
<img src="https://termix.site/img/logo.svg" width="120" alt="Termix Logo" />
|
||||||
|
|
||||||
<h1>Termix Plugin SDK</h1>
|
<h1>Termix Plugin SDK</h1>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`@termix-ssh/plugin-sdk` is what every [Termix](https://github.com/Termix-SSH/Termix) plugin builds against, including the ones that ship with Termix. It has the types for a plugin's backend and frontend, table definitions, the manifest schema, test helpers, and the `termix-plugin` CLI. The easiest way to start is the [Termix Plugin Template](https://github.com/Termix-SSH/Termix-Plugin-Template).
|
`@termix-ssh/plugin-sdk` is what every [Termix](https://github.com/Termix-SSH/Termix) plugin builds against, including the ones that ship with Termix. It has the types for a plugin's backend and frontend, table definitions, the manifest schema, test helpers, and the `termix-plugin` CLI. The easiest way to start is the [Termix Plugin Template](https://github.com/Termix-SSH/Termix-Plugin-Template). The full guide is at [docs.termix.site/develop](https://docs.termix.site/develop).
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@@ -28,8 +28,10 @@ npm install --save-dev @termix-ssh/plugin-sdk
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
|
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
|
||||||
|
npx termix-plugin patch # apply the plugin's dependency patches (build does this too)
|
||||||
npx termix-plugin test # run the plugin's Vitest suite
|
npx termix-plugin test # run the plugin's Vitest suite
|
||||||
npx termix-plugin validate # check manifest.json and the files it names
|
npx termix-plugin validate # check manifest.json and the files it names
|
||||||
|
npx termix-plugin openapi # write dist/openapi.json from @openapi comments
|
||||||
npx termix-plugin migrations # write migrations from the table definitions
|
npx termix-plugin migrations # write migrations from the table definitions
|
||||||
npx termix-plugin pack # write <id>-<version>.tmxplug
|
npx termix-plugin pack # write <id>-<version>.tmxplug
|
||||||
npx termix-plugin changelog # print this version's notes from CHANGELOG.md
|
npx termix-plugin changelog # print this version's notes from CHANGELOG.md
|
||||||
@@ -39,7 +41,7 @@ npx termix-plugin keygen --out <dir> # make a new signing key pair
|
|||||||
npx termix-plugin dev --server <url> --key <tmx_...> # install on a server and reinstall on every change
|
npx termix-plugin dev --server <url> --key <tmx_...> # install on a server and reinstall on every change
|
||||||
```
|
```
|
||||||
|
|
||||||
`dev` needs developer mode on (Admin settings, Plugins) and an API key from an
|
`dev` needs developer mode on (Settings, General) and an API key from an
|
||||||
admin account. It installs the plugin from a file, marked unverified, and
|
admin account. It installs the plugin from a file, marked unverified, and
|
||||||
reinstalls it each time you save. `TERMIX_SERVER_URL` and `TERMIX_API_KEY` work
|
reinstalls it each time you save. `TERMIX_SERVER_URL` and `TERMIX_API_KEY` work
|
||||||
in place of the flags. Reload the page to pick up frontend changes.
|
in place of the flags. Reload the page to pick up frontend changes.
|
||||||
@@ -48,7 +50,7 @@ in place of the flags. Reload the page to pick up frontend changes.
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Bugs and ideas for the SDK or the `termix-plugin` CLI go in the [Termix repo](https://github.com/Termix-SSH/Termix/issues/new/choose). You need to be logged in to GitHub. Please be as detailed as possible, preferably in English.
|
Bugs and ideas for the SDK or the `termix-plugin` CLI go in the [Termix repo](https://github.com/Termix-SSH/Termix/issues/new/choose). Add as much detail as you can, in English if possible.
|
||||||
|
|
||||||
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server.
|
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server.
|
||||||
|
|
||||||
@@ -56,4 +58,4 @@ For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf)
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the Apache License Version 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE) for more information.
|
Apache License 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE).
|
||||||
@@ -6,6 +6,8 @@ import * as esbuild from "esbuild";
|
|||||||
import { BACKEND_EXTERNALS, FRONTEND_EXTERNALS } from "../lib/externals.mjs";
|
import { BACKEND_EXTERNALS, FRONTEND_EXTERNALS } from "../lib/externals.mjs";
|
||||||
import { staticUrlImports } from "../lib/static-url-imports.mjs";
|
import { staticUrlImports } from "../lib/static-url-imports.mjs";
|
||||||
import { readManifest, resolveEntry, copyDir } from "../lib/plugin-dir.mjs";
|
import { readManifest, resolveEntry, copyDir } from "../lib/plugin-dir.mjs";
|
||||||
|
import { openapi } from "./openapi.mjs";
|
||||||
|
import { splitByCoreClasses } from "../lib/css-classes.mjs";
|
||||||
|
|
||||||
const BACKEND_ENTRIES = [
|
const BACKEND_ENTRIES = [
|
||||||
"src/backend/index.ts",
|
"src/backend/index.ts",
|
||||||
@@ -50,6 +52,43 @@ export function externalRequireInterop(externals) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A .wasm or font import becomes its bytes (a Uint8Array) inside the bundle,
|
||||||
|
// so it ships in backend.js or a worker with no separate file to find.
|
||||||
|
export const BINARY_EXTENSIONS = [".wasm", ".ttf", ".otf"];
|
||||||
|
const BINARY_LOADERS = Object.fromEntries(
|
||||||
|
BINARY_EXTENSIONS.map((ext) => [ext, "binary"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const REQUIRE_BANNER =
|
||||||
|
"import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* package.json "termix": { "workers": { "engine": "src/backend/engine/worker.ts" } }
|
||||||
|
* bundles each entry on its own into dist/workers/<name>.js, for
|
||||||
|
* new Worker(new URL("./workers/<name>.js", import.meta.url)). Nothing is left
|
||||||
|
* external: core's import fallback only runs on the main thread, so a worker
|
||||||
|
* can't rely on host packages and must not import the SDK.
|
||||||
|
*/
|
||||||
|
export function readWorkerEntries(cwd, pluginId) {
|
||||||
|
const pkgPath = path.join(cwd, "package.json");
|
||||||
|
const pkg = fs.existsSync(pkgPath)
|
||||||
|
? JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
||||||
|
: {};
|
||||||
|
const workers = pkg.termix?.workers ?? {};
|
||||||
|
return Object.entries(workers).map(([name, entry]) => {
|
||||||
|
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||||
|
throw new Error(`${pluginId}: worker name "${name}" must be lowercase`);
|
||||||
|
}
|
||||||
|
const file = path.resolve(cwd, String(entry));
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error(
|
||||||
|
`${pluginId}: worker "${name}" entry ${entry} is missing`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { name, file };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const SDK_PATCHES = path.join(
|
const SDK_PATCHES = path.join(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
"..",
|
"..",
|
||||||
@@ -96,14 +135,36 @@ export function applyPatches(cwd, pluginId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_CSS = path.join(
|
const HOST_DIR = path.join(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
"..",
|
"..",
|
||||||
"..",
|
"..",
|
||||||
"dist",
|
"dist",
|
||||||
"host",
|
"host",
|
||||||
"theme.css",
|
|
||||||
);
|
);
|
||||||
|
const THEME_CSS = path.join(HOST_DIR, "theme.css");
|
||||||
|
const CORE_CLASSES = path.join(HOST_DIR, "core-classes.json");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a plugin's Tailwind layers under their own names. Core orders
|
||||||
|
* termix-plugin-utilities above its base styles and below its own utilities,
|
||||||
|
* so a plugin's copy of `.hidden` can never beat core's `md:flex` just
|
||||||
|
* because the plugin's file loaded later. Only classes core also ships go
|
||||||
|
* there; `utilities: false` keeps the rest in core's own utilities layer.
|
||||||
|
*/
|
||||||
|
export function lowerPluginLayers(css, { utilities = true } = {}) {
|
||||||
|
const layers = utilities ? "properties|utilities" : "properties";
|
||||||
|
return css.replace(
|
||||||
|
new RegExp(`@layer (${layers})(?=[\\s{;,])`, "g"),
|
||||||
|
"@layer termix-plugin-$1",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Classes core's CSS has rules for, or null with an SDK built before 1.0.4. */
|
||||||
|
function readCoreClasses() {
|
||||||
|
if (!fs.existsSync(CORE_CLASSES)) return null;
|
||||||
|
return new Set(JSON.parse(fs.readFileSync(CORE_CLASSES, "utf8")));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiles the Tailwind classes the plugin's frontend uses into
|
* Compiles the Tailwind classes the plugin's frontend uses into
|
||||||
@@ -112,19 +173,6 @@ const THEME_CSS = path.join(
|
|||||||
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
|
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
|
||||||
* produced (a library's stylesheet the plugin imports).
|
* produced (a library's stylesheet the plugin imports).
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* Moves a plugin's Tailwind layers under their own names. Core orders
|
|
||||||
* termix-plugin-utilities above its base styles and below its own utilities,
|
|
||||||
* so a plugin's copy of `.hidden` can never beat core's `md:flex` just
|
|
||||||
* because the plugin's file loaded later.
|
|
||||||
*/
|
|
||||||
export function lowerPluginLayers(css) {
|
|
||||||
return css.replace(
|
|
||||||
/@layer (properties|utilities)(?=[\s{;,])/g,
|
|
||||||
"@layer termix-plugin-$1",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buildTailwind(cwd, outDir) {
|
export async function buildTailwind(cwd, outDir) {
|
||||||
const { compile, optimize } = await import("@tailwindcss/node");
|
const { compile, optimize } = await import("@tailwindcss/node");
|
||||||
const { Scanner } = await import("@tailwindcss/oxide");
|
const { Scanner } = await import("@tailwindcss/oxide");
|
||||||
@@ -136,10 +184,9 @@ export async function buildTailwind(cwd, outDir) {
|
|||||||
`@import ${JSON.stringify(THEME_CSS.replaceAll("\\", "/"))};`,
|
`@import ${JSON.stringify(THEME_CSS.replaceAll("\\", "/"))};`,
|
||||||
'@import "tailwindcss/utilities.css" layer(utilities);',
|
'@import "tailwindcss/utilities.css" layer(utilities);',
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const compiler = await compile(input, {
|
// build() adds to what a compiler already emitted, so each half gets its own.
|
||||||
base: path.dirname(THEME_CSS),
|
const compiler = () =>
|
||||||
onDependency: () => {},
|
compile(input, { base: path.dirname(THEME_CSS), onDependency: () => {} });
|
||||||
});
|
|
||||||
const scanner = new Scanner({
|
const scanner = new Scanner({
|
||||||
sources: [
|
sources: [
|
||||||
{
|
{
|
||||||
@@ -151,9 +198,23 @@ export async function buildTailwind(cwd, outDir) {
|
|||||||
});
|
});
|
||||||
// Flattened and minified the way core's own CSS is, so it does not rely
|
// Flattened and minified the way core's own CSS is, so it does not rely
|
||||||
// on native CSS nesting.
|
// on native CSS nesting.
|
||||||
const css = lowerPluginLayers(
|
const emit = async (candidates, options) =>
|
||||||
optimize(compiler.build(scanner.scan()), { minify: true }).code.trim(),
|
candidates.length === 0
|
||||||
);
|
? ""
|
||||||
|
: lowerPluginLayers(
|
||||||
|
optimize((await compiler()).build(candidates), {
|
||||||
|
minify: true,
|
||||||
|
}).code.trim(),
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
const candidates = scanner.scan();
|
||||||
|
const coreClasses = readCoreClasses();
|
||||||
|
const { shared, own } = coreClasses
|
||||||
|
? splitByCoreClasses(candidates, coreClasses)
|
||||||
|
: { shared: candidates, own: [] };
|
||||||
|
const css = [await emit(shared), await emit(own, { utilities: false })]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
if (!css) return;
|
if (!css) return;
|
||||||
const target = path.join(outDir, "frontend.css");
|
const target = path.join(outDir, "frontend.css");
|
||||||
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
||||||
@@ -192,13 +253,27 @@ export async function build({ cwd }) {
|
|||||||
// node_modules import instead, resolved at runtime like a host-provided
|
// node_modules import instead, resolved at runtime like a host-provided
|
||||||
// package.
|
// package.
|
||||||
external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
|
external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
|
||||||
|
loader: BINARY_LOADERS,
|
||||||
// Bundled CJS deps still call require() for Node builtins at runtime.
|
// Bundled CJS deps still call require() for Node builtins at runtime.
|
||||||
// ESM has no ambient require, so give esbuild's require shim a real one.
|
// ESM has no ambient require, so give esbuild's require shim a real one.
|
||||||
banner: {
|
banner: { js: REQUIRE_BANNER },
|
||||||
js: "import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const worker of readWorkerEntries(cwd, pluginId)) {
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [worker.file],
|
||||||
|
outfile: path.join(outDir, "workers", `${worker.name}.js`),
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
platform: "node",
|
||||||
|
target: "node22",
|
||||||
|
sourcemap: true,
|
||||||
|
logLevel: "warning",
|
||||||
|
loader: BINARY_LOADERS,
|
||||||
|
banner: { js: REQUIRE_BANNER },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Without this Node finds the host's typeless package.json, tries backend.js
|
// Without this Node finds the host's typeless package.json, tries backend.js
|
||||||
// as CommonJS first and warns before reparsing it as ESM.
|
// as CommonJS first and warns before reparsing it as ESM.
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -241,6 +316,7 @@ export async function build({ cwd }) {
|
|||||||
|
|
||||||
copyDir(path.join(cwd, "locales"), path.join(outDir, "locales"));
|
copyDir(path.join(cwd, "locales"), path.join(outDir, "locales"));
|
||||||
copyDir(path.join(cwd, "migrations"), path.join(outDir, "migrations"));
|
copyDir(path.join(cwd, "migrations"), path.join(outDir, "migrations"));
|
||||||
|
await openapi({ cwd, quiet: true });
|
||||||
|
|
||||||
console.log(`built ${pluginId}`);
|
console.log(`built ${pluginId}`);
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ export function resolveDevOptions(args, env = process.env) {
|
|||||||
|
|
||||||
const HINTS = {
|
const HINTS = {
|
||||||
DEVELOPER_MODE_OFF:
|
DEVELOPER_MODE_OFF:
|
||||||
"Turn on developer mode in Admin settings, under Plugins, then save again.",
|
"Turn on Plugin developer mode in Settings, General, then save again.",
|
||||||
SIGNED_ONLY:
|
SIGNED_ONLY:
|
||||||
"The server sets TERMIX_REQUIRE_SIGNED_PLUGINS, so it never installs unsigned files.",
|
"The server sets TERMIX_REQUIRE_SIGNED_PLUGINS, so it never installs unsigned files.",
|
||||||
BUNDLED_ID: "That id ships with Termix. Use a different id while developing.",
|
BUNDLED_ID: "That id ships with Termix. Use a different id while developing.",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
import { BACKEND_EXTERNALS } from "../lib/externals.mjs";
|
import { BACKEND_EXTERNALS } from "../lib/externals.mjs";
|
||||||
import { readManifest, resolveEntry } from "../lib/plugin-dir.mjs";
|
import { readManifest, resolveEntry } from "../lib/plugin-dir.mjs";
|
||||||
@@ -53,7 +53,7 @@ async function loadDefinitions(cwd) {
|
|||||||
external: BACKEND_EXTERNALS,
|
external: BACKEND_EXTERNALS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const module = await import(`file://${outfile.split(path.sep).join("/")}`);
|
const module = await import(pathToFileURL(outfile).href);
|
||||||
const tables = module.tables ?? module.default;
|
const tables = module.tables ?? module.default;
|
||||||
if (!Array.isArray(tables)) {
|
if (!Array.isArray(tables)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { readManifest } from "../lib/plugin-dir.mjs";
|
||||||
|
import { loadSdkModule } from "./validate.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes dist/openapi.json from the @openapi blocks in src/backend. A plugin
|
||||||
|
* with no documented routes gets no file.
|
||||||
|
*/
|
||||||
|
export async function openapi({ cwd, quiet = false }) {
|
||||||
|
const manifest = readManifest(cwd);
|
||||||
|
const pluginId = manifest.id ?? path.basename(cwd);
|
||||||
|
const backendDir = path.join(cwd, "src", "backend");
|
||||||
|
if (!fs.existsSync(backendDir)) return null;
|
||||||
|
|
||||||
|
const { buildOpenApi, hasOpenApiPaths } = await loadSdkModule("openapi");
|
||||||
|
const spec = await buildOpenApi({
|
||||||
|
title: `${manifest.name ?? pluginId} API`,
|
||||||
|
version: manifest.version ?? "0.0.0",
|
||||||
|
description: manifest.description,
|
||||||
|
files: [path.join(backendDir, "**", "*.{ts,tsx,js,mjs}")],
|
||||||
|
});
|
||||||
|
|
||||||
|
const target = path.join(cwd, "dist", "openapi.json");
|
||||||
|
if (!hasOpenApiPaths(spec)) {
|
||||||
|
fs.rmSync(target, { force: true });
|
||||||
|
if (!quiet) console.log(`${pluginId}: no documented routes`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
|
fs.writeFileSync(target, `${JSON.stringify(spec, null, 2)}\n`);
|
||||||
|
if (!quiet) {
|
||||||
|
console.log(
|
||||||
|
`wrote dist/openapi.json (${Object.keys(spec.paths).length} paths)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
@@ -41,6 +41,10 @@ export async function validate({ cwd }) {
|
|||||||
problems.push(...validatePackage(cwd, raw));
|
problems.push(...validatePackage(cwd, raw));
|
||||||
problems.push(...(await validateChangelogFile(cwd, raw)));
|
problems.push(...(await validateChangelogFile(cwd, raw)));
|
||||||
|
|
||||||
|
for (const warning of docsWarnings(cwd, raw)) {
|
||||||
|
console.warn(` warn ${warning}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (problems.length > 0) {
|
if (problems.length > 0) {
|
||||||
for (const problem of problems) console.error(` ${problem}`);
|
for (const problem of problems) console.error(` ${problem}`);
|
||||||
throw new Error(`${raw.id ?? path.basename(cwd)}: manifest is not valid.`);
|
throw new Error(`${raw.id ?? path.basename(cwd)}: manifest is not valid.`);
|
||||||
@@ -49,6 +53,69 @@ export async function validate({ cwd }) {
|
|||||||
console.log(`ok ${raw.id ?? path.basename(cwd)}`);
|
console.log(`ok ${raw.id ?? path.basename(cwd)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Env vars core sets for everyone, so a plugin need not list them. */
|
||||||
|
const CORE_ENV = new Set([
|
||||||
|
"DATA_DIR",
|
||||||
|
"NODE_ENV",
|
||||||
|
"PORT",
|
||||||
|
"BASE_PATH",
|
||||||
|
"VERSION",
|
||||||
|
"HOME",
|
||||||
|
"TMPDIR",
|
||||||
|
"TEMP",
|
||||||
|
"TMP",
|
||||||
|
"PATH",
|
||||||
|
"APPDATA",
|
||||||
|
"LOCALAPPDATA",
|
||||||
|
"USERPROFILE",
|
||||||
|
"ELECTRON_EMBEDDED",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ENV_READS = [
|
||||||
|
/process\.env\.([A-Z][A-Z0-9_]*)/g,
|
||||||
|
/process\.env\[\s*["'`]([A-Z][A-Z0-9_]*)["'`]\s*\]/g,
|
||||||
|
/\benv\.([A-Z][A-Z0-9_]*)\b/g,
|
||||||
|
/\benv\[\s*["'`]([A-Z][A-Z0-9_]*)["'`]\s*\]/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
function walkSources(dir, out = []) {
|
||||||
|
if (!fs.existsSync(dir)) return out;
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walkSources(full, out);
|
||||||
|
else if (/\.(ts|tsx|js|mjs|cjs)$/.test(entry.name)) out.push(full);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docs gaps that do not block a release: no docs/index.md, or a process.env
|
||||||
|
* read the manifest does not list in env.
|
||||||
|
*/
|
||||||
|
export function docsWarnings(cwd, raw) {
|
||||||
|
const warnings = [];
|
||||||
|
if (!fs.existsSync(path.join(cwd, "docs", "index.md"))) {
|
||||||
|
warnings.push("docs/index.md is missing, so the plugin has no docs page");
|
||||||
|
}
|
||||||
|
const declared = new Set(
|
||||||
|
Array.isArray(raw.env) ? raw.env.map((e) => e?.name) : [],
|
||||||
|
);
|
||||||
|
const missing = new Set();
|
||||||
|
for (const file of walkSources(path.join(cwd, "src"))) {
|
||||||
|
const text = fs.readFileSync(file, "utf8");
|
||||||
|
for (const pattern of ENV_READS) {
|
||||||
|
for (const match of text.matchAll(pattern)) {
|
||||||
|
const name = match[1];
|
||||||
|
if (!declared.has(name) && !CORE_ENV.has(name)) missing.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const name of [...missing].sort()) {
|
||||||
|
warnings.push(`reads ${name} but manifest env does not list it`);
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* package.json has to agree with the manifest: the same version, and a real
|
* package.json has to agree with the manifest: the same version, and a real
|
||||||
* SDK range rather than "*", so a plugin built against a newer SDK says so.
|
* SDK range rather than "*", so a plugin built against a newer SDK says so.
|
||||||
@@ -151,6 +218,11 @@ async function validateMigrations(cwd, pluginId) {
|
|||||||
const present = DIALECTS.filter((dialect) =>
|
const present = DIALECTS.filter((dialect) =>
|
||||||
fs.existsSync(path.join(root, dialect)),
|
fs.existsSync(path.join(root, dialect)),
|
||||||
);
|
);
|
||||||
|
for (const dialect of DIALECTS) {
|
||||||
|
if (present.length > 0 && !present.includes(dialect)) {
|
||||||
|
problems.push(`migrations/${dialect} is missing`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const byDialect = new Map();
|
const byDialect = new Map();
|
||||||
for (const dialect of present) {
|
for (const dialect of present) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { sign, verify, keygen } from "./commands/sign.mjs";
|
|||||||
import { migrations } from "./commands/migrations.mjs";
|
import { migrations } from "./commands/migrations.mjs";
|
||||||
import { dev } from "./commands/dev.mjs";
|
import { dev } from "./commands/dev.mjs";
|
||||||
import { changelog } from "./commands/changelog.mjs";
|
import { changelog } from "./commands/changelog.mjs";
|
||||||
|
import { openapi } from "./commands/openapi.mjs";
|
||||||
|
|
||||||
const COMMANDS = {
|
const COMMANDS = {
|
||||||
build,
|
build,
|
||||||
@@ -32,6 +33,7 @@ const COMMANDS = {
|
|||||||
migrations,
|
migrations,
|
||||||
dev,
|
dev,
|
||||||
changelog,
|
changelog,
|
||||||
|
openapi,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [command, ...args] = process.argv.slice(2);
|
const [command, ...args] = process.argv.slice(2);
|
||||||
@@ -50,6 +52,7 @@ if (!command || command === "--help" || command === "-h") {
|
|||||||
" verify Check a .tmxplug's .sig <file> --key <base64>[,...]",
|
" verify Check a .tmxplug's .sig <file> --key <base64>[,...]",
|
||||||
" keygen Write a new signing key pair [--out dir]",
|
" keygen Write a new signing key pair [--out dir]",
|
||||||
" migrations Generate migrations from the plugin's table definitions",
|
" migrations Generate migrations from the plugin's table definitions",
|
||||||
|
" openapi Write dist/openapi.json from the @openapi blocks in src/backend",
|
||||||
" changelog Print a version's notes from CHANGELOG.md [--version x.y.z]",
|
" changelog Print a version's notes from CHANGELOG.md [--version x.y.z]",
|
||||||
" dev Build, pack and install on a running server, again on every change",
|
" dev Build, pack and install on a running server, again on every change",
|
||||||
" [--server url] [--key tmx_...] [--once]",
|
" [--server url] [--key tmx_...] [--once]",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* The class names a stylesheet has rules for, unescaped, so `.md\:w-44`
|
||||||
|
* reads as the candidate `md:w-44` a plugin writes.
|
||||||
|
*/
|
||||||
|
export function classNamesInCss(css) {
|
||||||
|
const names = new Set();
|
||||||
|
// Declarations hold numbers like 0.25rem, so only selectors are read.
|
||||||
|
const selectors = css.replace(/\{[^{}]*\}/g, "{}");
|
||||||
|
for (const match of selectors.matchAll(
|
||||||
|
/\.((?:\\[0-9a-fA-F]{1,6} ?|\\.|[\w-])+)/g,
|
||||||
|
)) {
|
||||||
|
names.add(
|
||||||
|
match[1].replace(/\\([0-9a-fA-F]{1,6}) ?|\\(.)/g, (_all, hex, char) =>
|
||||||
|
hex ? String.fromCodePoint(parseInt(hex, 16)) : char,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a plugin's candidates by whether core's own CSS already has them.
|
||||||
|
* A plain class core ships keeps the plugin's copy below core's utilities, so
|
||||||
|
* it can never reorder core's rules. Everything else goes in core's utilities
|
||||||
|
* layer: plugin-only classes, and every variant (md:, hover:, ...) even when
|
||||||
|
* core ships it, so `max-h-56 md:max-h-none` is ordered in one build and a
|
||||||
|
* variant still beats core's own base class.
|
||||||
|
*/
|
||||||
|
export function splitByCoreClasses(candidates, coreClasses) {
|
||||||
|
const shared = [];
|
||||||
|
const own = [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const plain = !candidate.includes(":");
|
||||||
|
(plain && coreClasses.has(candidate) ? shared : own).push(candidate);
|
||||||
|
}
|
||||||
|
return { shared, own };
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ export const TMXPLUG_CONTENTS = [
|
|||||||
"README.md",
|
"README.md",
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"icon.svg",
|
"icon.svg",
|
||||||
|
// Licence texts, so third-party notices travel with the plugin.
|
||||||
|
"LICENSE",
|
||||||
|
"NOTICE",
|
||||||
];
|
];
|
||||||
|
|
||||||
const BLOCK = 512;
|
const BLOCK = 512;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.0.2",
|
"version": "1.0.11",
|
||||||
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -53,6 +53,14 @@
|
|||||||
"types": "./dist/changelog.d.ts",
|
"types": "./dist/changelog.d.ts",
|
||||||
"default": "./dist/changelog.js"
|
"default": "./dist/changelog.js"
|
||||||
},
|
},
|
||||||
|
"./docs": {
|
||||||
|
"types": "./dist/docs.d.ts",
|
||||||
|
"default": "./dist/docs.js"
|
||||||
|
},
|
||||||
|
"./openapi": {
|
||||||
|
"types": "./dist/openapi.d.ts",
|
||||||
|
"default": "./dist/openapi.js"
|
||||||
|
},
|
||||||
"./settings": {
|
"./settings": {
|
||||||
"types": "./dist/settings.d.ts",
|
"types": "./dist/settings.d.ts",
|
||||||
"default": "./dist/settings.js"
|
"default": "./dist/settings.js"
|
||||||
@@ -95,6 +103,7 @@
|
|||||||
"tsconfig.plugin.json",
|
"tsconfig.plugin.json",
|
||||||
"README.md",
|
"README.md",
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
|
"PLUGIN-GUIDE.md",
|
||||||
"client.d.ts"
|
"client.d.ts"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -102,6 +111,7 @@
|
|||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@deadendjs/swagger-jsdoc": "^8.1.2",
|
||||||
"@tailwindcss/node": "^4.2.4",
|
"@tailwindcss/node": "^4.2.4",
|
||||||
"@tailwindcss/oxide": "^4.2.4",
|
"@tailwindcss/oxide": "^4.2.4",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.1",
|
||||||
@@ -139,6 +149,18 @@
|
|||||||
},
|
},
|
||||||
"@testing-library/react": {
|
"@testing-library/react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sonner": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -85,6 +85,27 @@
|
|||||||
"items": { "type": "string", "minLength": 1, "maxLength": 160 },
|
"items": { "type": "string", "minLength": 1, "maxLength": 160 },
|
||||||
"description": "Short lines listing what the plugin does, shown on its page."
|
"description": "Short lines listing what the plugin does, shown on its page."
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^https://",
|
||||||
|
"description": "Where the plugin's docs live. Shown as a Documentation link in Termix."
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Environment variables the plugin reads, listed in its docs.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["name", "description"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]{0,63}$" },
|
||||||
|
"description": { "type": "string", "minLength": 1, "maxLength": 300 },
|
||||||
|
"default": { "type": "string" },
|
||||||
|
"required": { "type": "boolean" },
|
||||||
|
"secret": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"engine": {
|
"engine": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -357,6 +357,13 @@ export interface PluginRouterOptions {
|
|||||||
* (multer, a raw body for signature checking, a proxied stream).
|
* (multer, a raw body for signature checking, a proxied stream).
|
||||||
*/
|
*/
|
||||||
rawBody?: boolean;
|
rawBody?: boolean;
|
||||||
|
/**
|
||||||
|
* Requests per minute one address may make to this plugin's public routes.
|
||||||
|
* Defaults to 120, which a proxied page loading its own scripts and images
|
||||||
|
* can pass on its own. Capped at 10000, and an admin's
|
||||||
|
* TERMIX_PLUGIN_PUBLIC_RATE_LIMIT wins over it.
|
||||||
|
*/
|
||||||
|
publicRateLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1457,6 +1464,20 @@ export interface PluginSecondFactor {
|
|||||||
reset?: (userId: string) => Promise<void>;
|
reset?: (userId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginUserTokenOptions {
|
||||||
|
/** Why the token exists, kept in the audit log and on the session. */
|
||||||
|
purpose: string;
|
||||||
|
/** Seconds until it expires. Defaults to 300 and is capped at 900. */
|
||||||
|
ttlSeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUserToken {
|
||||||
|
/** Send as `Authorization: Bearer <token>` to core's own API. */
|
||||||
|
token: string;
|
||||||
|
/** ISO time the session ends. */
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login methods, second factors and SSH auth types. Every register call needs
|
* Login methods, second factors and SSH auth types. Every register call needs
|
||||||
* auth:provide and a matching contributes.auth entry, and is undone on
|
* auth:provide and a matching contributes.auth entry, and is undone on
|
||||||
@@ -1518,6 +1539,26 @@ export interface PluginAuth {
|
|||||||
* delete a provider people still sign in with.
|
* delete a provider people still sign in with.
|
||||||
*/
|
*/
|
||||||
countLinkedUsers: (provider: string) => Promise<number>;
|
countLinkedUsers: (provider: string) => Promise<number>;
|
||||||
|
/**
|
||||||
|
* A short-lived session for an existing user, for calling core's own HTTP
|
||||||
|
* API as them from work with no request behind it, like a scheduled task
|
||||||
|
* driving the Termix CLI. The user's RBAC applies unchanged. Needs
|
||||||
|
* users:impersonate, not auth:provide, and every call is audited.
|
||||||
|
*
|
||||||
|
* The session is marked with the plugin and purpose. It cannot change the
|
||||||
|
* user's password, second factors, API keys or linked logins, so it can
|
||||||
|
* never outlive itself.
|
||||||
|
*/
|
||||||
|
issueUserToken: (
|
||||||
|
userId: string,
|
||||||
|
options: PluginUserTokenOptions,
|
||||||
|
) => Promise<PluginUserToken>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What a plugin may learn about a user. Needs users:read. */
|
||||||
|
export interface PluginUsers {
|
||||||
|
/** The user's UI language, like "zh_CN", or null when they never chose one. */
|
||||||
|
getLocale: (userId: string) => Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOpenIsolatedWindowRequest {
|
export interface PluginOpenIsolatedWindowRequest {
|
||||||
@@ -2006,8 +2047,13 @@ export interface PluginContext {
|
|||||||
readonly hosts: PluginHosts;
|
readonly hosts: PluginHosts;
|
||||||
/** SSH through core's connect pipeline. Needs ssh:connect and credentials:use. */
|
/** SSH through core's connect pipeline. Needs ssh:connect and credentials:use. */
|
||||||
readonly ssh: PluginSsh;
|
readonly ssh: PluginSsh;
|
||||||
/** Login methods, second factors and SSH auth types. Needs auth:provide. */
|
/**
|
||||||
|
* Login methods, second factors and SSH auth types. Needs auth:provide,
|
||||||
|
* except issueUserToken, which needs users:impersonate.
|
||||||
|
*/
|
||||||
readonly auth: PluginAuth;
|
readonly auth: PluginAuth;
|
||||||
|
/** What a plugin may learn about a user. Needs users:read. */
|
||||||
|
readonly users: PluginUsers;
|
||||||
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
||||||
readonly desktop: PluginDesktop;
|
readonly desktop: PluginDesktop;
|
||||||
/** Host protocol credentials and the user's saved SSH keys. See PluginCredentials. */
|
/** Host protocol credentials and the user's saved SSH keys. See PluginCredentials. */
|
||||||
@@ -2032,8 +2078,9 @@ export interface PluginContext {
|
|||||||
* no request behind it. Always audited, and every core API inside still
|
* no request behind it. Always audited, and every core API inside still
|
||||||
* applies that user's RBAC.
|
* applies that user's RBAC.
|
||||||
*
|
*
|
||||||
* Needs users:impersonate. That and a `userId` option on services.get or
|
* Needs users:impersonate. That, auth.issueUserToken and a `userId` option
|
||||||
* secrets.getShared are the only ways a plugin can name a user.
|
* on services.get or secrets.getShared are the only ways a plugin can act
|
||||||
|
* as a user it names.
|
||||||
*/
|
*/
|
||||||
asUser: <T>(userId: string, fn: () => Promise<T> | T) => Promise<T>;
|
asUser: <T>(userId: string, fn: () => Promise<T> | T) => Promise<T>;
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ export function parseChangelog(markdown: string): {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks a CHANGELOG.md. With a manifest version, the newest release has to
|
* Checks a CHANGELOG.md. With a manifest version, the newest release has to
|
||||||
* be that version, so a release never ships without its notes.
|
* be that version, so a release never ships without its notes. A beta like
|
||||||
|
* 1.1.0-beta.2 gets its notes from git, so it only needs the newest release
|
||||||
|
* to be 1.1.0 or older.
|
||||||
*/
|
*/
|
||||||
export function validateChangelog(
|
export function validateChangelog(
|
||||||
markdown: string,
|
markdown: string,
|
||||||
@@ -206,7 +208,16 @@ export function validateChangelog(
|
|||||||
return problems;
|
return problems;
|
||||||
}
|
}
|
||||||
const newest = changelog.releases[0].version;
|
const newest = changelog.releases[0].version;
|
||||||
if (manifestVersion && newest !== manifestVersion) {
|
const base = manifestVersion
|
||||||
|
? semver.valid(semver.coerce(manifestVersion))
|
||||||
|
: null;
|
||||||
|
const betaOk =
|
||||||
|
!!manifestVersion &&
|
||||||
|
!!semver.prerelease(manifestVersion) &&
|
||||||
|
!!base &&
|
||||||
|
!!semver.valid(newest) &&
|
||||||
|
semver.lte(newest, base);
|
||||||
|
if (manifestVersion && newest !== manifestVersion && !betaOk) {
|
||||||
problems.push(
|
problems.push(
|
||||||
`the newest release is ${newest}, but the manifest version is ${manifestVersion}`,
|
`the newest release is ${newest}, but the manifest version is ${manifestVersion}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function builder(column: PluginColumn): Builder {
|
|||||||
export const id = () =>
|
export const id = () =>
|
||||||
builder({ type: "id", primaryKey: true, notNull: true });
|
builder({ type: "id", primaryKey: true, notNull: true });
|
||||||
|
|
||||||
/** Unbounded string. Never indexable: MySQL cannot index TEXT. */
|
/** Unbounded string, LONGTEXT on MySQL. Never indexable: MySQL cannot index TEXT. */
|
||||||
export const text = () => builder({ type: "text" });
|
export const text = () => builder({ type: "text" });
|
||||||
|
|
||||||
/** Bounded string. Indexable on every engine. */
|
/** Bounded string. Indexable on every engine. */
|
||||||
|
|||||||
@@ -80,11 +80,13 @@ function columnType(
|
|||||||
// though it was declared unbounded. defineTable refuses to index a
|
// though it was declared unbounded. defineTable refuses to index a
|
||||||
// text/json column, so in practice this only catches a primary key.
|
// text/json column, so in practice this only catches a primary key.
|
||||||
if (keyed && dialect !== "sqlite") return `varchar(${KEY_LENGTH})`;
|
if (keyed && dialect !== "sqlite") return `varchar(${KEY_LENGTH})`;
|
||||||
|
// MySQL TEXT stops at 64KB, so free text, json and secrets go long.
|
||||||
|
if (dialect === "mysql" && column.type !== "timestamp") return "longtext";
|
||||||
return "text";
|
return "text";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Column types that are TEXT on MySQL when nothing keys them. */
|
/** Column types that are TEXT or LONGTEXT on MySQL when nothing keys them. */
|
||||||
const TEXT_ON_MYSQL: ReadonlySet<PluginColumn["type"]> = new Set([
|
const TEXT_ON_MYSQL: ReadonlySet<PluginColumn["type"]> = new Set([
|
||||||
"text",
|
"text",
|
||||||
"json",
|
"json",
|
||||||
@@ -307,17 +309,27 @@ export function splitStatements(source: string): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (char === ";") {
|
if (char === ";") {
|
||||||
if (current.trim()) statements.push(current.trim());
|
if (hasSql(current)) statements.push(current.trim());
|
||||||
current = "";
|
current = "";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
current += char;
|
current += char;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.trim()) statements.push(current.trim());
|
if (hasSql(current)) statements.push(current.trim());
|
||||||
return statements;
|
return statements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** False for a chunk that is only comments, which a driver refuses to run. */
|
||||||
|
function hasSql(chunk: string): boolean {
|
||||||
|
return (
|
||||||
|
chunk
|
||||||
|
.replace(/--[^\n]*/g, "")
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||||
|
.trim() !== ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** DROP statement for a definition, for removePluginData. */
|
/** DROP statement for a definition, for removePluginData. */
|
||||||
export function dropTableSql(
|
export function dropTableSql(
|
||||||
dialect: SqlDialect,
|
dialect: SqlDialect,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Docs links and env var entries from a manifest. Kept apart from manifest.ts
|
||||||
|
* so the browser can use them without pulling in semver.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A docs link, or null for anything that is not an https URL. Core only ever
|
||||||
|
* opens it in a new tab, but a javascript: or file: link should never get there.
|
||||||
|
*/
|
||||||
|
export function pluginDocsUrl(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
try {
|
||||||
|
const url = new URL(value.trim());
|
||||||
|
if (url.protocol !== "https:") return null;
|
||||||
|
return url.toString().replace(/\/$/, "");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Joins a page and anchor onto a plugin's docs URL. */
|
||||||
|
export function pluginDocsPage(
|
||||||
|
base: string,
|
||||||
|
page?: string,
|
||||||
|
anchor?: string,
|
||||||
|
): string {
|
||||||
|
const clean = (page ?? "").replace(/^\/+|\/+$/g, "");
|
||||||
|
const url = clean ? `${base}/${clean}` : base;
|
||||||
|
return anchor ? `${url}#${anchor.replace(/^#/, "")}` : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
||||||
|
export const MAX_ENV_DESCRIPTION_LENGTH = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An environment variable a plugin reads. Only shown in docs and on the
|
||||||
|
* plugin's page, so people know what they can set.
|
||||||
|
*/
|
||||||
|
export interface PluginEnvVar {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
/** The value used when it is not set, as written in a compose file. */
|
||||||
|
default?: string;
|
||||||
|
required?: boolean;
|
||||||
|
/** Holds a password or key. */
|
||||||
|
secret?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The usable env entries of a manifest. Bad ones are dropped. */
|
||||||
|
export function pluginEnvVars(value: unknown): PluginEnvVar[] {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.filter(
|
||||||
|
(v): v is PluginEnvVar =>
|
||||||
|
typeof v === "object" &&
|
||||||
|
v !== null &&
|
||||||
|
typeof v.name === "string" &&
|
||||||
|
ENV_NAME_PATTERN.test(v.name) &&
|
||||||
|
typeof v.description === "string",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -888,6 +888,16 @@ export interface DesktopApi {
|
|||||||
onRemoteServerChange: (listener: () => void) => Disposer;
|
onRemoteServerChange: (listener: () => void) => Disposer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Links into this plugin's docs, from the manifest's docs field. */
|
||||||
|
export interface DocsApi {
|
||||||
|
/** The docs link, or null when the manifest has none. */
|
||||||
|
readonly url: string | null;
|
||||||
|
/** A page of the docs ("setup"), with an optional anchor. */
|
||||||
|
page: (page?: string, anchor?: string) => string | null;
|
||||||
|
/** Opens a page in a new tab. Does nothing without a docs link. */
|
||||||
|
open: (page?: string, anchor?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TermixAppInfo {
|
export interface TermixAppInfo {
|
||||||
readonly pluginId: string;
|
readonly pluginId: string;
|
||||||
readonly manifest: PluginManifest;
|
readonly manifest: PluginManifest;
|
||||||
@@ -1017,6 +1027,8 @@ export interface TermixApp extends TermixAppInfo {
|
|||||||
confirm: (options: ConfirmRequest) => Promise<boolean>;
|
confirm: (options: ConfirmRequest) => Promise<boolean>;
|
||||||
/** The desktop app, when the frontend runs in it. */
|
/** The desktop app, when the frontend runs in it. */
|
||||||
desktop: DesktopApi;
|
desktop: DesktopApi;
|
||||||
|
/** This plugin's docs. Needs plugin API 1.2. */
|
||||||
|
docs: DocsApi;
|
||||||
/**
|
/**
|
||||||
* Fires after this plugin's settings are saved from the settings screen or
|
* Fires after this plugin's settings are saved from the settings screen or
|
||||||
* the host editor. Disposed automatically.
|
* the host editor. Disposed automatically.
|
||||||
@@ -1091,6 +1103,11 @@ export interface SshAuthTypeInfo {
|
|||||||
*/
|
*/
|
||||||
export interface PluginHostBridge {
|
export interface PluginHostBridge {
|
||||||
usePluginId: () => string;
|
usePluginId: () => string;
|
||||||
|
useDocsUrl: (
|
||||||
|
pluginId: string,
|
||||||
|
page?: string,
|
||||||
|
anchor?: string,
|
||||||
|
) => string | null;
|
||||||
useTranslation: (pluginId: string) => {
|
useTranslation: (pluginId: string) => {
|
||||||
t: TranslateFn;
|
t: TranslateFn;
|
||||||
language: string;
|
language: string;
|
||||||
@@ -1290,6 +1307,15 @@ export function usePluginId(): string {
|
|||||||
return requireHost().usePluginId();
|
return requireHost().usePluginId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A page of this plugin's docs, or null when the manifest has no docs link.
|
||||||
|
* Pass it to PanelShell's docs prop or use DocsLink. Needs plugin API 1.2.
|
||||||
|
*/
|
||||||
|
export function useDocsUrl(page?: string, anchor?: string): string | null {
|
||||||
|
const bridge = requireHost();
|
||||||
|
return bridge.useDocsUrl(bridge.usePluginId(), page, anchor);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translation bound to this plugin's namespace. Keys the plugin does not
|
* Translation bound to this plugin's namespace. Keys the plugin does not
|
||||||
* define fall back to core's shared strings (common.*, hosts.*).
|
* define fall back to core's shared strings (common.*, hosts.*).
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function execCommand(
|
|||||||
if (!settled) {
|
if (!settled) {
|
||||||
settled = true;
|
settled = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(new Error(`Command timeout after ${timeoutMs}ms: ${command}`));
|
reject(new Error(`Command timeout after ${timeoutMs}ms`));
|
||||||
}
|
}
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,21 @@
|
|||||||
|
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { isKnownCapability } from "./capabilities.js";
|
import { isKnownCapability } from "./capabilities.js";
|
||||||
|
import {
|
||||||
|
ENV_NAME_PATTERN,
|
||||||
|
MAX_ENV_DESCRIPTION_LENGTH,
|
||||||
|
pluginDocsUrl,
|
||||||
|
type PluginEnvVar,
|
||||||
|
} from "./docs.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ENV_NAME_PATTERN,
|
||||||
|
MAX_ENV_DESCRIPTION_LENGTH,
|
||||||
|
pluginDocsPage,
|
||||||
|
pluginDocsUrl,
|
||||||
|
pluginEnvVars,
|
||||||
|
type PluginEnvVar,
|
||||||
|
} from "./docs.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The plugin API this build implements. A minor bump adds to the API and
|
* The plugin API this build implements. A minor bump adds to the API and
|
||||||
@@ -21,7 +36,7 @@ import { isKnownCapability } from "./capabilities.js";
|
|||||||
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
||||||
* version by the server.
|
* version by the server.
|
||||||
*/
|
*/
|
||||||
export const PLUGIN_API_VERSION = "1.1.0";
|
export const PLUGIN_API_VERSION = "1.3.0";
|
||||||
|
|
||||||
/** The API major, the default engine.api for a new plugin. */
|
/** The API major, the default engine.api for a new plugin. */
|
||||||
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
||||||
@@ -601,6 +616,10 @@ export interface PluginManifest {
|
|||||||
video?: string;
|
video?: string;
|
||||||
/** Short lines listing what the plugin does, shown on its page. */
|
/** Short lines listing what the plugin does, shown on its page. */
|
||||||
features?: string[];
|
features?: string[];
|
||||||
|
/** Where the plugin's docs live. https only. */
|
||||||
|
docs?: string;
|
||||||
|
/** Environment variables the plugin reads. */
|
||||||
|
env?: PluginEnvVar[];
|
||||||
engine: PluginEngine;
|
engine: PluginEngine;
|
||||||
/** Catalog capability ids. */
|
/** Catalog capability ids. */
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
@@ -654,6 +673,8 @@ const ALLOWED_TOP_LEVEL = new Set([
|
|||||||
"icon",
|
"icon",
|
||||||
"video",
|
"video",
|
||||||
"features",
|
"features",
|
||||||
|
"docs",
|
||||||
|
"env",
|
||||||
"dependencies",
|
"dependencies",
|
||||||
"optionalDependencies",
|
"optionalDependencies",
|
||||||
"provides",
|
"provides",
|
||||||
@@ -816,6 +837,12 @@ export function validateManifest(manifest: unknown): string[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
validateFeatures(m.features, errors);
|
validateFeatures(m.features, errors);
|
||||||
|
if ("docs" in m && pluginDocsUrl(m.docs) === null) {
|
||||||
|
errors.push(
|
||||||
|
`Field "docs" must be an https URL, got: ${JSON.stringify(m.docs)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
validateEnv(m.env, errors);
|
||||||
for (const field of ["backend", "frontend", "locales"] as const) {
|
for (const field of ["backend", "frontend", "locales"] as const) {
|
||||||
if (field in m && requireString(m[field], field, errors)) {
|
if (field in m && requireString(m[field], field, errors)) {
|
||||||
requireRelativePath(m[field] as string, field, errors);
|
requireRelativePath(m[field] as string, field, errors);
|
||||||
@@ -873,6 +900,54 @@ function validateFeatures(features: unknown, errors: string[]): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateEnv(env: unknown, errors: string[]): void {
|
||||||
|
if (env === undefined) return;
|
||||||
|
if (!Array.isArray(env)) {
|
||||||
|
errors.push('Field "env" must be an array');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seen = new Set<string>();
|
||||||
|
env.forEach((entry, i) => {
|
||||||
|
const where = `env[${i}]`;
|
||||||
|
if (!isPlainObject(entry)) {
|
||||||
|
errors.push(`Field "${where}" must be an object`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectUnknown(
|
||||||
|
entry,
|
||||||
|
["name", "description", "default", "required", "secret"],
|
||||||
|
where,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
if (typeof entry.name !== "string" || !ENV_NAME_PATTERN.test(entry.name)) {
|
||||||
|
errors.push(
|
||||||
|
`Field "${where}.name" must be an uppercase name like MY_PLUGIN_URL, got: ${JSON.stringify(entry.name)}`,
|
||||||
|
);
|
||||||
|
} else if (seen.has(entry.name)) {
|
||||||
|
errors.push(`Field "${where}.name" repeats ${entry.name}`);
|
||||||
|
} else {
|
||||||
|
seen.add(entry.name);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof entry.description !== "string" ||
|
||||||
|
entry.description.trim().length === 0 ||
|
||||||
|
entry.description.length > MAX_ENV_DESCRIPTION_LENGTH
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`Field "${where}.description" must be a non-empty string of at most ${MAX_ENV_DESCRIPTION_LENGTH} characters`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("default" in entry && typeof entry.default !== "string") {
|
||||||
|
errors.push(`Field "${where}.default" must be a string`);
|
||||||
|
}
|
||||||
|
for (const flag of ["required", "secret"] as const) {
|
||||||
|
if (flag in entry && typeof entry[flag] !== "boolean") {
|
||||||
|
errors.push(`Field "${where}.${flag}" must be true or false`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function validateAuthor(author: unknown, errors: string[]): void {
|
function validateAuthor(author: unknown, errors: string[]): void {
|
||||||
if (author === undefined) return;
|
if (author === undefined) return;
|
||||||
if (!isPlainObject(author)) {
|
if (!isPlainObject(author)) {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Builds an OpenAPI spec from the @openapi JSDoc blocks above routes. Core and
|
||||||
|
* every plugin use this, so the docs site renders them all the same way.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import swaggerJSDoc from "@deadendjs/swagger-jsdoc";
|
||||||
|
|
||||||
|
export interface OpenApiTag {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildOpenApiOptions {
|
||||||
|
title: string;
|
||||||
|
version: string;
|
||||||
|
description?: string;
|
||||||
|
/** Source files or globs holding @openapi blocks. */
|
||||||
|
files: string[];
|
||||||
|
tags?: OpenApiTag[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenApiSpec = Record<string, unknown> & {
|
||||||
|
paths?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** How every Termix route is signed in to. */
|
||||||
|
export const TERMIX_SECURITY_SCHEMES = {
|
||||||
|
bearerAuth: {
|
||||||
|
type: "http",
|
||||||
|
scheme: "bearer",
|
||||||
|
bearerFormat: "JWT",
|
||||||
|
description:
|
||||||
|
"Session token sent in the Authorization header as Bearer and the token.",
|
||||||
|
},
|
||||||
|
cookieAuth: {
|
||||||
|
type: "apiKey",
|
||||||
|
in: "cookie",
|
||||||
|
name: "jwt",
|
||||||
|
description:
|
||||||
|
"Session JWT cookie set at sign-in. When present, it takes precedence over the Authorization header.",
|
||||||
|
},
|
||||||
|
apiKeyAuth: {
|
||||||
|
type: "http",
|
||||||
|
scheme: "bearer",
|
||||||
|
bearerFormat: "API key starting with tmx_",
|
||||||
|
description:
|
||||||
|
"API key sent in the Authorization header as Bearer and the key, which starts with tmx_. It acts as the key owner and cannot act as anyone else.",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export async function buildOpenApi(
|
||||||
|
options: BuildOpenApiOptions,
|
||||||
|
): Promise<OpenApiSpec> {
|
||||||
|
const spec = (await swaggerJSDoc({
|
||||||
|
definition: {
|
||||||
|
openapi: "3.0.3",
|
||||||
|
info: {
|
||||||
|
title: options.title,
|
||||||
|
version: options.version,
|
||||||
|
...(options.description ? { description: options.description } : {}),
|
||||||
|
},
|
||||||
|
servers: [{ url: "/", description: "Your Termix server" }],
|
||||||
|
components: {
|
||||||
|
securitySchemes: TERMIX_SECURITY_SCHEMES,
|
||||||
|
schemas: {
|
||||||
|
Error: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
details: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [{ bearerAuth: [] }, { cookieAuth: [] }, { apiKeyAuth: [] }],
|
||||||
|
...(options.tags?.length ? { tags: options.tags } : {}),
|
||||||
|
},
|
||||||
|
apis: options.files.map((f) => f.replace(/\\/g, "/")),
|
||||||
|
failOnErrors: true,
|
||||||
|
} as Parameters<typeof swaggerJSDoc>[0])) as OpenApiSpec;
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether a spec documents any route. */
|
||||||
|
export function hasOpenApiPaths(spec: OpenApiSpec): boolean {
|
||||||
|
return Object.keys(spec.paths ?? {}).length > 0;
|
||||||
|
}
|
||||||
@@ -174,6 +174,8 @@ export interface FakeContextOptions {
|
|||||||
linkedUsers?: Record<string, number>;
|
linkedUsers?: Record<string, number>;
|
||||||
/** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
|
/** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
|
||||||
loginAttemptLimit?: number;
|
loginAttemptLimit?: number;
|
||||||
|
/** What ctx.users.getLocale answers, by user id. A missing user answers null. */
|
||||||
|
locales?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FakeProcessOptions {
|
export interface FakeProcessOptions {
|
||||||
@@ -247,6 +249,8 @@ export interface FakeAuthRegistrations {
|
|||||||
}>;
|
}>;
|
||||||
/** Failures recorded through ctx.auth.loginRateLimit, by "<ip>|<key>". */
|
/** Failures recorded through ctx.auth.loginRateLimit, by "<ip>|<key>". */
|
||||||
loginFailures: Map<string, number>;
|
loginFailures: Map<string, number>;
|
||||||
|
/** Every ctx.auth.issueUserToken call, in order. */
|
||||||
|
userTokens: Array<{ userId: string; purpose: string; ttlSeconds?: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FakePluginContext {
|
export interface FakePluginContext {
|
||||||
@@ -574,6 +578,7 @@ export function createFakeContext(
|
|||||||
completedLogins: [],
|
completedLogins: [],
|
||||||
revokedSessions: [],
|
revokedSessions: [],
|
||||||
loginFailures: new Map(),
|
loginFailures: new Map(),
|
||||||
|
userTokens: [],
|
||||||
};
|
};
|
||||||
const sshClient = options.sshClient ?? {};
|
const sshClient = options.sshClient ?? {};
|
||||||
let actor = options.actor;
|
let actor = options.actor;
|
||||||
@@ -1140,6 +1145,18 @@ export function createFakeContext(
|
|||||||
},
|
},
|
||||||
countLinkedUsers: async (provider) =>
|
countLinkedUsers: async (provider) =>
|
||||||
options.linkedUsers?.[provider] ?? 0,
|
options.linkedUsers?.[provider] ?? 0,
|
||||||
|
issueUserToken: async (userId, tokenOptions) => {
|
||||||
|
auth.userTokens.push({ userId, ...tokenOptions });
|
||||||
|
const ttl = Math.min(tokenOptions.ttlSeconds ?? 300, 900);
|
||||||
|
return {
|
||||||
|
token: `fake-token-${auth.userTokens.length}`,
|
||||||
|
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
users: {
|
||||||
|
getLocale: async (userId) => options.locales?.[userId] ?? null,
|
||||||
},
|
},
|
||||||
|
|
||||||
desktop: {
|
desktop: {
|
||||||
@@ -1442,6 +1459,8 @@ export interface MockContextOptions {
|
|||||||
linkedUsers?: Record<string, number>;
|
linkedUsers?: Record<string, number>;
|
||||||
/** See FakeContextOptions. */
|
/** See FakeContextOptions. */
|
||||||
loginAttemptLimit?: number;
|
loginAttemptLimit?: number;
|
||||||
|
/** See FakeContextOptions. */
|
||||||
|
locales?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MockPluginContext extends FakePluginContext {
|
export interface MockPluginContext extends FakePluginContext {
|
||||||
@@ -1458,7 +1477,8 @@ export interface MockPluginContext extends FakePluginContext {
|
|||||||
* topic outside the plugin's own namespace, settings:read-core on
|
* topic outside the plugin's own namespace, settings:read-core on
|
||||||
* ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
|
* ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
|
||||||
* notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
|
* notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
|
||||||
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, and auth:provide on ctx.auth. Reading a plugin's own settings is deliberately
|
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, auth:provide on ctx.auth
|
||||||
|
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
|
||||||
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
||||||
*/
|
*/
|
||||||
export function createMockCtx(
|
export function createMockCtx(
|
||||||
@@ -1495,6 +1515,7 @@ export function createMockCtx(
|
|||||||
baseUrl: options.baseUrl,
|
baseUrl: options.baseUrl,
|
||||||
linkedUsers: options.linkedUsers,
|
linkedUsers: options.linkedUsers,
|
||||||
loginAttemptLimit: options.loginAttemptLimit,
|
loginAttemptLimit: options.loginAttemptLimit,
|
||||||
|
locales: options.locales,
|
||||||
manifest: {
|
manifest: {
|
||||||
capabilities: options.capabilities ?? [],
|
capabilities: options.capabilities ?? [],
|
||||||
...options.manifest,
|
...options.manifest,
|
||||||
@@ -1786,6 +1807,17 @@ export function createMockCtx(
|
|||||||
require("auth:provide");
|
require("auth:provide");
|
||||||
return ctx.auth.countLinkedUsers(provider);
|
return ctx.auth.countLinkedUsers(provider);
|
||||||
},
|
},
|
||||||
|
issueUserToken: async (userId, tokenOptions) => {
|
||||||
|
require("users:impersonate");
|
||||||
|
return ctx.auth.issueUserToken(userId, tokenOptions);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
users: {
|
||||||
|
getLocale: async (userId) => {
|
||||||
|
require("users:read");
|
||||||
|
return ctx.users.getLocale(userId);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
desktop: {
|
desktop: {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const offline = () =>
|
|||||||
Promise.reject(new Error("No plugin API in a unit test; mock it"));
|
Promise.reject(new Error("No plugin API in a unit test; mock it"));
|
||||||
(globalThis as Record<string, unknown>).__termixTestPluginHost = {
|
(globalThis as Record<string, unknown>).__termixTestPluginHost = {
|
||||||
usePluginId: () => "test-plugin",
|
usePluginId: () => "test-plugin",
|
||||||
|
useDocsUrl: () => null,
|
||||||
useTranslation: () => ({
|
useTranslation: () => ({
|
||||||
t: (key: string) => key,
|
t: (key: string) => key,
|
||||||
language: "en",
|
language: "en",
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ function findRepoRoot(startDir: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* termix-plugin build turns a .wasm or font import into its bytes. Tests get the same,
|
||||||
|
* so code that imports a module compiles it the same way under vitest.
|
||||||
|
*/
|
||||||
|
export function wasmBinaryPlugin() {
|
||||||
|
return {
|
||||||
|
name: "termix-wasm-binary",
|
||||||
|
enforce: "pre" as const,
|
||||||
|
load(id: string) {
|
||||||
|
const file = id.split("?")[0];
|
||||||
|
if (![".wasm", ".ttf", ".otf"].some((ext) => file.endsWith(ext))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const base64 = fs.readFileSync(file).toString("base64");
|
||||||
|
return `export default new Uint8Array(Buffer.from(${JSON.stringify(base64)}, "base64"));`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginVitestOptions {
|
export interface PluginVitestOptions {
|
||||||
/** Extra aliases merged over the defaults. */
|
/** Extra aliases merged over the defaults. */
|
||||||
alias?: Record<string, string>;
|
alias?: Record<string, string>;
|
||||||
@@ -96,7 +115,10 @@ export function pluginVitestConfig(
|
|||||||
// unexpectedly". Threads don't.
|
// unexpectedly". Threads don't.
|
||||||
const pool = "threads" as const;
|
const pool = "threads" as const;
|
||||||
|
|
||||||
|
const plugins = [wasmBinaryPlugin()];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
plugins,
|
||||||
resolve: { alias },
|
resolve: { alias },
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
@@ -104,6 +126,7 @@ export function pluginVitestConfig(
|
|||||||
pool,
|
pool,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
plugins,
|
||||||
resolve: { alias },
|
resolve: { alias },
|
||||||
test: {
|
test: {
|
||||||
name: `${pluginId}:backend`,
|
name: `${pluginId}:backend`,
|
||||||
@@ -116,6 +139,7 @@ export function pluginVitestConfig(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
plugins,
|
||||||
resolve: { alias },
|
resolve: { alias },
|
||||||
test: {
|
test: {
|
||||||
name: `${pluginId}:frontend`,
|
name: `${pluginId}:frontend`,
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ DialogOverlay (value): ({ className, ...props }: DialogOverlayProps & RefAttribu
|
|||||||
DialogPortal (value): ({ ...props }: DialogPortalProps) => Element
|
DialogPortal (value): ({ ...props }: DialogPortalProps) => Element
|
||||||
DialogTitle (value): ({ className, ...props }: DialogTitleProps & RefAttributes<HTMLHeadingElement>) => Element
|
DialogTitle (value): ({ className, ...props }: DialogTitleProps & RefAttributes<HTMLHeadingElement>) => Element
|
||||||
DialogTrigger (value): ({ ...props }: DialogTriggerProps & RefAttributes<HTMLButtonElement>) => Element
|
DialogTrigger (value): ({ ...props }: DialogTriggerProps & RefAttributes<HTMLButtonElement>) => Element
|
||||||
|
DocsLink (value): ({ core, page, anchor, href, variant, children, className, }: DocsLinkProps) => Element
|
||||||
|
DocsLinkProps (type): DocsLinkProps
|
||||||
DropdownMenu (value): ({ ...props }: DropdownMenuProps) => Element
|
DropdownMenu (value): ({ ...props }: DropdownMenuProps) => Element
|
||||||
DropdownMenuCheckboxItem (value): ({ className, children, checked, inset, ...props }: DropdownMenuCheckboxItemProps & RefAttributes<HTMLDivElement> & { inset?: boolean; }) => Element
|
DropdownMenuCheckboxItem (value): ({ className, children, checked, inset, ...props }: DropdownMenuCheckboxItemProps & RefAttributes<HTMLDivElement> & { inset?: boolean; }) => Element
|
||||||
DropdownMenuContent (value): ({ className, align, sideOffset, ...props }: DropdownMenuContentProps & RefAttributes<HTMLDivElement>) => Element
|
DropdownMenuContent (value): ({ className, align, sideOffset, ...props }: DropdownMenuContentProps & RefAttributes<HTMLDivElement>) => Element
|
||||||
@@ -93,7 +95,7 @@ HostDefaultBadge (value): ({ settingKey }: { settingKey: string; }) => Element
|
|||||||
HostDefaultField (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
HostDefaultField (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
||||||
HostFeatureFields (value): ({ form, updateForm, }: HostFeatureFieldsProps) => Element
|
HostFeatureFields (value): ({ form, updateForm, }: HostFeatureFieldsProps) => Element
|
||||||
HostFeatureFieldsProps (type): HostFeatureFieldsProps
|
HostFeatureFieldsProps (type): HostFeatureFieldsProps
|
||||||
HostKeyVerificationDialog (value): ({ isOpen, scenario, ip, port, hostname, fingerprint, oldFingerprint, algorithm, onAccept, onReject, backgroundColor, }: HostKeyVerificationDialogProps) => Element
|
HostKeyVerificationDialog (value): ({ isOpen, scenario, ip, port, hostname, fingerprint, oldFingerprint, onAccept, onReject, backgroundColor, }: HostKeyVerificationDialogProps) => Element
|
||||||
InlineView (value): ({ open, onOpenChange, title, icon, status, actions, toolbar, footer, width, onBeforeClose, scroll, bare, className, children, }: { open: boolean; onOpenChange: (open: boolean) => void; title: ReactNode; icon?: ReactNode; status?: ReactNode; actions?: ReactNode; toolbar?: ReactNode; footer?: ReactNode; width?: EditingWidth; onBeforeClose?: () => boolean | Promise<boolean>; scroll?: boolean; bare?: boolean; className?: string; children: ReactNode; }) => ReactPortal
|
InlineView (value): ({ open, onOpenChange, title, icon, status, actions, toolbar, footer, width, onBeforeClose, scroll, bare, className, children, }: { open: boolean; onOpenChange: (open: boolean) => void; title: ReactNode; icon?: ReactNode; status?: ReactNode; actions?: ReactNode; toolbar?: ReactNode; footer?: ReactNode; width?: EditingWidth; onBeforeClose?: () => boolean | Promise<boolean>; scroll?: boolean; bare?: boolean; className?: string; children: ReactNode; }) => ReactPortal
|
||||||
Input (value): ({ className, type, ...props }: DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>) => Element
|
Input (value): ({ className, type, ...props }: DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>) => Element
|
||||||
Kbd (value): ForwardRefExoticComponent<KbdProps & RefAttributes<HTMLElement>>
|
Kbd (value): ForwardRefExoticComponent<KbdProps & RefAttributes<HTMLElement>>
|
||||||
@@ -128,7 +130,7 @@ PanePrompt (value): ({ open, title, titleId, description, icon, tone, actions, o
|
|||||||
PanelDensity (type): PanelDensity
|
PanelDensity (type): PanelDensity
|
||||||
PanelList (value): ({ empty, className, children, }: { empty?: ReactNode; className?: string; children?: ReactNode; }) => Element
|
PanelList (value): ({ empty, className, children, }: { empty?: ReactNode; className?: string; children?: ReactNode; }) => Element
|
||||||
PanelSearch (value): ({ value, onChange, placeholder, fill, autoFocus, disabled, title, onEnter, inputRef, className, }: { value: string; onChange: (next: string) => void; placeholder?: string; fill?: boolean; autoFocus?: boolean; disabled?: boolean; title?: string; onEnter?: () => void; inputRef?: Ref<HTMLInputElement>; className?: string; }) => Element
|
PanelSearch (value): ({ value, onChange, placeholder, fill, autoFocus, disabled, title, onEnter, inputRef, className, }: { value: string; onChange: (next: string) => void; placeholder?: string; fill?: boolean; autoFocus?: boolean; disabled?: boolean; title?: string; onEnter?: () => void; inputRef?: Ref<HTMLInputElement>; className?: string; }) => Element
|
||||||
PanelShell (value): ({ chrome, icon, title, status, leading, actions, toolbar, tabs, footer, scroll, className, children, }: { chrome?: boolean; icon?: ReactNode; title?: ReactNode; status?: ReactNode; leading?: ReactNode; actions?: ReactNode; toolbar?: ReactNode; tabs?: ReactNode; footer?: ReactNode; scroll?: boolean; className?: string; children: ReactNode; }) => Element
|
PanelShell (value): ({ chrome, icon, title, status, leading, actions, toolbar, tabs, footer, scroll, docs, className, children, }: { chrome?: boolean; icon?: ReactNode; title?: ReactNode; status?: ReactNode; leading?: ReactNode; actions?: ReactNode; toolbar?: ReactNode; tabs?: ReactNode; footer?: ReactNode; scroll?: boolean; docs?: string; className?: string; children: ReactNode; }) => Element
|
||||||
PanelViewMode (type): PanelViewMode
|
PanelViewMode (type): PanelViewMode
|
||||||
PassphraseDialog (value): ({ isOpen, onSubmit, onCancel, hostInfo, backgroundColor, }: PassphraseDialogProps) => Element
|
PassphraseDialog (value): ({ isOpen, onSubmit, onCancel, hostInfo, backgroundColor, }: PassphraseDialogProps) => Element
|
||||||
PasswordInput (value): ForwardRefExoticComponent<PasswordInputProps & RefAttributes<HTMLInputElement>>
|
PasswordInput (value): ForwardRefExoticComponent<PasswordInputProps & RefAttributes<HTMLInputElement>>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"only-arches": ["x86_64", "aarch64"],
|
|
||||||
"skip-icons-check": false,
|
|
||||||
"skip-appstream-check": false
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Loaded 100 of 515 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user