mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 15:15:24 +00:00
Compare commits
46
Commits
sdk-v1.0.2
...
sdk-v1.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
931761190f | ||
|
|
28fe98e787 | ||
|
|
2ba11652eb | ||
|
|
2e3c242e34 | ||
|
|
4c68d35c21 | ||
|
|
0ce378ec55 | ||
|
|
83cce344e8 | ||
|
|
6f4198cc83 | ||
|
|
abdbb34ce2 | ||
|
|
0b41672a3c | ||
|
|
0f5c2ac862 | ||
|
|
6f7fc05892 | ||
|
|
4eac6d977d | ||
|
|
406766eddd | ||
|
|
f803bfc140 | ||
|
|
b0a0ee3e00 | ||
|
|
af09aa1028 | ||
|
|
45908b5df3 | ||
|
|
b156829ef3 | ||
|
|
399835d8bc | ||
|
|
e1d7b3e7c6 | ||
|
|
970aae4740 | ||
|
|
0324d27f13 | ||
|
|
ab2307bab4 | ||
|
|
5e7f69456b | ||
|
|
2f975f433e | ||
|
|
4511cc0a42 | ||
|
|
878f1ebaa3 | ||
|
|
d6c4ec01b5 | ||
|
|
f09865ed04 | ||
|
|
cfab6f764c | ||
|
|
7fcda3bb01 | ||
|
|
7161f37984 | ||
|
|
f57c7e8966 | ||
|
|
5774f7cd0e | ||
|
|
5819348064 | ||
|
|
c3624cdf62 | ||
|
|
740d2adaf3 | ||
|
|
8a45d952fd | ||
|
|
ad3a18224f | ||
|
|
cd29e1d087 | ||
|
|
4fd9aebee4 | ||
|
|
acc9484280 | ||
|
|
7508a45f36 | ||
|
|
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)
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# 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: |
|
||||||
@@ -220,6 +225,11 @@ 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
|
||||||
|
|
||||||
|
# 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 }}
|
||||||
@@ -429,6 +439,11 @@ jobs:
|
|||||||
npm install dmg-license
|
npm install dmg-license
|
||||||
node scripts/install-macos-sharp.cjs
|
node scripts/install-macos-sharp.cjs
|
||||||
|
|
||||||
|
# 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,14 +483,15 @@ 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"
|
||||||
@@ -534,7 +550,7 @@ 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 --universal --x64 --arm64 --publish never
|
||||||
@@ -555,7 +571,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 +738,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
|
||||||
@@ -952,8 +968,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: |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -462,7 +462,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
|
||||||
@@ -500,7 +500,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 }} \
|
||||||
@@ -591,6 +591,18 @@ 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-refresh:
|
||||||
|
needs: [prep, electron-release]
|
||||||
|
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' }}
|
||||||
|
|||||||
+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
|
|
||||||
}
|
|
||||||
+21
-2
@@ -4,12 +4,31 @@
|
|||||||
|
|
||||||
### 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
|
||||||
+1
-1
@@ -57,7 +57,7 @@ npm run format # format the code with Prettier
|
|||||||
## 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.
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"drips": {
|
|
||||||
"ethereum": {
|
|
||||||
"ownedBy": "0x67e0C779119D9BcC2187564A66B80a58767d05d1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,202 @@
|
|||||||
Copyright 2025 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.
|
||||||
@@ -92,7 +92,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). Its community registry is open for submissions now, and Termix will be able to install community plugins in a later update. See [submitting a plugin](https://docs.termix.site/develop/community-registry).
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@@ -109,14 +109,14 @@ Want to build your own? Start from the [plugin template](https://github.com/Term
|
|||||||
</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>
|
||||||
@@ -149,6 +149,10 @@ 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"
|
||||||
|
GUACD_DRIVE_PATH: "/termix-data/rdp-drive"
|
||||||
depends_on:
|
depends_on:
|
||||||
- guacd
|
- guacd
|
||||||
networks:
|
networks:
|
||||||
@@ -158,8 +162,8 @@ services:
|
|||||||
image: guacamole/guacd:1.6.0
|
image: guacamole/guacd:1.6.0
|
||||||
container_name: guacd
|
container_name: guacd
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
volumes:
|
||||||
- "4822:4822"
|
- termix-data:/termix-data
|
||||||
networks:
|
networks:
|
||||||
- termix-net
|
- termix-net
|
||||||
|
|
||||||
@@ -254,4 +258,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.
|
Distributed under the Apache License Version 2.0. See [LICENSE](LICENSE) for more information.
|
||||||
-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"
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -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/*
|
||||||
@@ -37,7 +37,7 @@ COPY . .
|
|||||||
RUN npm run build:backend
|
RUN 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 +58,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/*
|
||||||
@@ -81,7 +81,7 @@ RUN npm ci --omit=dev --ignore-scripts && \
|
|||||||
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
|
||||||
|
|||||||
+60
-46
@@ -4,152 +4,162 @@
|
|||||||
"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.0/acme-ssl-1.0.0.tmxplug",
|
||||||
"sha256": "1725735a058065b211436b166c0ef7d4ca6e74438102764664381160a2b305d3"
|
"sha256": "05b7b4c27897135437af6340c5c31501d2e96ac013dca0672940b74c76085433"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0.0/ai-1.0.0.tmxplug",
|
||||||
"sha256": "ea121658305bed9d28e046e096fc458976258d7cb77387e1b8ac82ccb9a001cb"
|
"sha256": "ed7a64a4be5978238427d4abbd2153ef2b44c17ebfec78fcb5df579967d01eed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/alerts-1.0.0.tmxplug",
|
||||||
"sha256": "93274529eb3445db9e1dddc630b7608b15250eb179bb7919a43c3e6707fa20b5",
|
"sha256": "97e7be293ace897cd45615a8764e8395cf613546b86de82601ab49669dc36591",
|
||||||
"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.0/automations-1.0.0.tmxplug",
|
||||||
"sha256": "6cb00cf9464e42dbc46786a6ef74b07d62da39ece9905197584d2934265b9b10"
|
"sha256": "0972aef1c14917321c035f7497db81fe95f6b9e47d854b95b1c762444d9cb2a2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/docker-1.0.0.tmxplug",
|
||||||
"sha256": "d1d1dd441b51a97231d5da94e38a12b2d2b25db6da5e2b13b4a4b9486881ed2d",
|
"sha256": "ca9b8cf573d27a71adb544fd6ff4d19278349f10fdca98f9cc63d7fec63bc441",
|
||||||
"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.0/file-manager-1.0.0.tmxplug",
|
||||||
"sha256": "8d60f6299a38fcc69a17ca30abb0e2a168aa880c0cfb50d57f1dd37158d811a5",
|
"sha256": "d1d92ae051c79c7ce6f05c7dbf33c96de9d8b58083eaccb104d1d21a6bd8b58f",
|
||||||
"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.0/fleets-1.0.0.tmxplug",
|
||||||
"sha256": "4a774d4a3685b0ee8da5b73d14c60d40f10b6a3f1c5b31a5dfefb94ba30fb7ad"
|
"sha256": "410b19f3a6f47e2a292821b11876b8b5a0bdf779f8d852d5ac04a7501d0f12c0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/homepage-1.0.0.tmxplug",
|
||||||
"sha256": "403513bc1f96a7c21d39d55c8fd1d4e55fde7b8e6ec63228839bf3243d93e68a"
|
"sha256": "5652e378dbb208c7bb349ba2262034077f1cfca95b2d6d029f4186c70ed27999"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/host-metrics-1.0.0.tmxplug",
|
||||||
"sha256": "1cfc81f724418fd68306e3ac30daa529b03aa065740a866f6fbcaa67edb440ac",
|
"sha256": "ca50e27f06bc7a275bd1bfb1deb5ac97f19023c84567105fe74e075602ec86da",
|
||||||
"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.0/ldap-1.0.0.tmxplug",
|
||||||
"sha256": "2d8cb269242c12522a5195fa44ec14e95ade3b00d7e7be12e9204631518a0f88"
|
"sha256": "44d3f9e9be913b9791a084aff588d7d4217dd5cd3f61178006ed4fa2455cad23"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/network-topology-1.0.0.tmxplug",
|
||||||
"sha256": "5c0a585dcffd1cd523e94d45f2eda32800c403a4c832854fec7331b651da7b7d"
|
"sha256": "ba3db2a6bc87533c8273648549c466b95f2c7538700407b2d62e48f813526bac"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/opkssh-1.0.0.tmxplug",
|
||||||
"sha256": "7e8a17aa9053954e937ac2274928e29b821dfd671759e356551b27c13e23c9ad"
|
"sha256": "05884e2244b4cc7f7f0465fe4b775ff95a99183ce6aba60848b68f1f3ec4078f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/proxmox-1.0.0.tmxplug",
|
||||||
"sha256": "cb6149ab69e348e5a0a1939b31943cb64aded3e1ee930b91dc77c46a715990d0"
|
"sha256": "3a304ba327352093f4bf5dff1a7d6fc981a83180ffc2e06fe1c63e59600e8e3c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0.0/remote-desktop-1.0.0.tmxplug",
|
||||||
"sha256": "5201dc1ce4846ef9f509a4c609698e2b2970e31fbc152bb2fe5c39c72c5c6d39",
|
"sha256": "2dd13c79066c99816f78b3c8aa5f3e31e17d3d2568f2e48575ef1e01f1195c28",
|
||||||
"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.0/secret-sources-1.0.0.tmxplug",
|
||||||
"sha256": "b6fbe38cd6d62d0b361c102f13f050ee8a281983dc806e32e8d8feeb416ceb20"
|
"sha256": "fdab87ca97ea05c60d7008b22da4f56c4897abf11505b6fa8b5c7bd8bc1a53e3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/serial-1.0.0.tmxplug",
|
||||||
"sha256": "c8c3ac8c916253c928195005773e558ee87555fb47b7b56f3e43c0df0fb852c4",
|
"sha256": "a645144a11e591f9668f2e7b23b8423274006180862be9c5f3e59df361c27e5e",
|
||||||
"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.0/session-recording-1.0.0.tmxplug",
|
||||||
"sha256": "ff0e6948806e0127399d41a6abd0f41fce404e65ba2c9d8044fb6dced401f4bb"
|
"sha256": "3a9ab3a3638d0cf13a4c7debbcd315d9a324b9991faea1c192ae1e8fda1e525b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/session-sharing-1.0.0.tmxplug",
|
||||||
"sha256": "4d2e363086a8fe79815bf76b4ed359ac82d5926373ab24854028e4dbc5a57ac6"
|
"sha256": "f838e53401f6d75923821797829806b9ca03f0dd64fe87f8524d14a2e05f684e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/snippets-1.0.0.tmxplug",
|
||||||
"sha256": "0239e63df6d33cb38a1212935dae972402b174a7eab8b351084dab45196f1fe5",
|
"sha256": "a434dd442c54fd7a124f4ebc9579a0af1aa4b07b1a90e3875f8ae72c35ced410",
|
||||||
"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.0/ssh-terminal-1.0.0.tmxplug",
|
||||||
"sha256": "66d2a4e87a2b5ac5551c74a4fad5808a6ee81d6e8f0a43c1f6456a12d6232714",
|
"sha256": "64031970a7b6eb1c860aac79c1587a582ccd38b2ffcd5974b51a716240a8474d",
|
||||||
"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.0/sso-1.0.0.tmxplug",
|
||||||
"sha256": "88d7fcf8eb3b0916d69bcc7349a58d98e37c493bfadc8b90f81b8aa5c845abc5",
|
"sha256": "2045d1da03dcd47089cb6566a7f25fe021326cb556cbc36b01563a2c234d1230",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
|
"recommended": false,
|
||||||
|
"consent": false,
|
||||||
"enabledByEnv": ["OIDC_CLIENT_ID"]
|
"enabledByEnv": ["OIDC_CLIENT_ID"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -157,19 +167,19 @@
|
|||||||
"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.0/step-ca-1.0.0.tmxplug",
|
||||||
"sha256": "997af2068f88c017c76a057683ac5e367566b5b8bc0961fc1e29c32b702a9059"
|
"sha256": "fc38ac6dcf8c43c550b7a7c65836a095ec5a8bae76c8797b5563b2c6c8e8dad1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/tailscale-1.0.0.tmxplug",
|
||||||
"sha256": "963a84859f8411bcb27e2950d46a7a43b239ace1295ac2abc4a77ecec43806d9"
|
"sha256": "f7732afe584044258cc46205fee327223cdd290e1ba6e6858ddd22be6b3ef12d"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/telemetry-1.0.0.tmxplug",
|
||||||
"sha256": "9f973902320a01eed5fead9b31288face503a790bf68a2826176e48fb663c30c",
|
"sha256": "103e2ef2d52993bf15fdf2cda0addfa4aeba5817b4340f28e3fdea634aba6af0",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": true
|
"consent": true
|
||||||
@@ -179,72 +189,76 @@
|
|||||||
"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.0/termix-identity-1.0.0.tmxplug",
|
||||||
"sha256": "00dfe3da65f78d7d085817c93a4e01bfef6931fb70ff5576c76e2f641e337555"
|
"sha256": "f1f712178b5d64ae263227640b61d1f5ea5a301eae3ff8adb8b52de5f20892fe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/tmux-monitor-1.0.0.tmxplug",
|
||||||
"sha256": "bbc508410296ca3434b5ba8c40fa5ef87646d104ec6f75da3bf0e22313bbbf18"
|
"sha256": "d6a98bcf99dbb31201d7079baf348100bca1c0eda0928aec4aeb075ea373a8d5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/totp-1.0.0.tmxplug",
|
||||||
"sha256": "c0b6b43d778dfd47af7bdbca099667bf46539de62936c3291947ac2425c02de9",
|
"sha256": "aed42422ccc38c661f1aac2f7409f7872acf271552d6623b79bbcc43813d4fca",
|
||||||
"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.0/tunnels-1.0.0.tmxplug",
|
||||||
"sha256": "82df362cfc820437db8c56ebd1e3140d7fcef677d98a5439d7e868486cc21ad4",
|
"sha256": "215eae643d61d032bb6190bab7ed42140fd91c2165e2c4de079ff541e1522763",
|
||||||
"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.0/vault-1.0.0.tmxplug",
|
||||||
"sha256": "d84b0ef1a309bb3a89288f488a242dd19df9e46b237df778c2f2a77e77867960"
|
"sha256": "0bbd95002866807ddd469f102d09bff9cf1b39e005dec6b96530066c6c3b0d19"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/wake-on-lan-1.0.0.tmxplug",
|
||||||
"sha256": "e680c22279438a566d1033939e4a3063a0fbbeb7f57b6db3bd6712bed09a4388"
|
"sha256": "39b97642e44e7b128df3632b10962bff6a26313f2e989e5acd29760cf91c4e04"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/warpgate-1.0.0.tmxplug",
|
||||||
"sha256": "c974be686488905277a52440b248d14370f3f543a7bd53f3485d5b6db4fab40b"
|
"sha256": "a7b7ed6d39639d0d6050958417d33d646a0304f99dec4eb9da71a46e42ae80f1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/web-endpoint-1.0.0.tmxplug",
|
||||||
"sha256": "e93d8535617783bc538d415f25a5acc04e3805417002d68d4ebc4044c8fbed8c"
|
"sha256": "22f5f276f6dab85e9b211f8e990f82fcbf63ebb8fa90379463f64d0fa38905ab"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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.0/webauthn-1.0.0.tmxplug",
|
||||||
"sha256": "fba89362c503813a12a7b73c9aed6994a10a1d010db8e1237a1f901131eb8713",
|
"sha256": "50994ae148b2a5e68cf924b922eb2fb22cf6215905049dd5b61a16e30a376a50",
|
||||||
"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.0/workspaces-1.0.0.tmxplug",
|
||||||
"sha256": "31d4d2ba02b59f2c2189c0400eb22d07aa8ba72728cd73a4512995581cf3afcd",
|
"sha256": "23c6d2b0845b0e854dc6dde0ab4b60b9a58af020099fd4f6fcc1463a48d9ee62",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"consent": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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`;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,13 @@
|
|||||||
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "users" DROP COLUMN "donation_modal_dismissed";
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,13 @@
|
|||||||
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -456,6 +456,13 @@
|
|||||||
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,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}"
|
||||||
|
|||||||
+2
-2
@@ -1041,7 +1041,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);
|
||||||
@@ -1303,7 +1303,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")
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
{
|
||||||
|
"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_PLUGIN_STATS_URL",
|
||||||
|
"group": "plugins",
|
||||||
|
"description": "Where install 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": "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": "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.
|
||||||
|
|||||||
-29
@@ -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
+14
-35
@@ -142,7 +142,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 +158,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 +167,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 +1089,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 +2697,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"
|
||||||
@@ -6236,7 +6230,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 +6994,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 +7010,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 +7209,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 +7381,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 +7495,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"
|
||||||
@@ -7748,7 +7736,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 +7961,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 +8258,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",
|
||||||
@@ -9578,7 +9563,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 +9590,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 +9758,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",
|
||||||
@@ -10038,7 +10020,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 +10565,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 +10623,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 +10679,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 +10777,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": {
|
||||||
@@ -11490,7 +11467,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 +11980,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 +12050,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 +12059,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 +12075,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 +12877,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"
|
||||||
@@ -13236,7 +13207,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 +13219,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 +13307,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"
|
||||||
@@ -14805,7 +14773,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 +14944,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 +15015,10 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin-sdk": {
|
"packages/plugin-sdk": {
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"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 +15054,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
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-6
@@ -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/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",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"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 && 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",
|
||||||
@@ -166,10 +166,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,36 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -96,14 +98,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 +136,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 +147,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 +161,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") : "";
|
||||||
@@ -241,6 +265,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 };
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"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"
|
||||||
@@ -102,6 +110,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 +148,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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.2.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;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -8,11 +8,12 @@
|
|||||||
*
|
*
|
||||||
* For development, `--local <dir>` (or TERMIX_LOCAL_PLUGINS) points at a
|
* For development, `--local <dir>` (or TERMIX_LOCAL_PLUGINS) points at a
|
||||||
* folder of plugin repos like ../Termix-Plugins. A plugin there whose sources
|
* folder of plugin repos like ../Termix-Plugins. A plugin there whose sources
|
||||||
* changed since its last build is rebuilt first, then any built one (has
|
* changed since its last build is rebuilt first with this checkout's SDK, so
|
||||||
|
* SDK edits apply without publishing, then any built one (has
|
||||||
* dist/) is copied in place of its pin, or added when it has no pin.
|
* dist/) is copied in place of its pin, or added when it has no pin.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execSync } = require("node:child_process");
|
const { execFileSync } = require("node:child_process");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const {
|
const {
|
||||||
@@ -21,6 +22,7 @@ const {
|
|||||||
extractArtifact,
|
extractArtifact,
|
||||||
findLocalPluginBuilds,
|
findLocalPluginBuilds,
|
||||||
rebuildLocalPlugins,
|
rebuildLocalPlugins,
|
||||||
|
localSdkBuildCommand,
|
||||||
copyLocalBuild,
|
copyLocalBuild,
|
||||||
buildBundledIndex,
|
buildBundledIndex,
|
||||||
} = require("./lib/bundled-plugins.cjs");
|
} = require("./lib/bundled-plugins.cjs");
|
||||||
@@ -34,9 +36,10 @@ async function main() {
|
|||||||
const localArg =
|
const localArg =
|
||||||
flag !== -1 ? process.argv[flag + 1] : process.env.TERMIX_LOCAL_PLUGINS;
|
flag !== -1 ? process.argv[flag + 1] : process.env.TERMIX_LOCAL_PLUGINS;
|
||||||
const localDir = localArg ? path.resolve(root, localArg) : null;
|
const localDir = localArg ? path.resolve(root, localArg) : null;
|
||||||
|
const [node, buildArgs] = localSdkBuildCommand();
|
||||||
rebuildLocalPlugins(localDir, null, (repo, id) => {
|
rebuildLocalPlugins(localDir, null, (repo, id) => {
|
||||||
console.log(`building ${id}`);
|
console.log(`building ${id}`);
|
||||||
execSync("npm run build", { cwd: repo, stdio: "inherit" });
|
execFileSync(node, buildArgs, { cwd: repo, stdio: "inherit" });
|
||||||
});
|
});
|
||||||
const local = findLocalPluginBuilds(localDir);
|
const local = findLocalPluginBuilds(localDir);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*
|
*
|
||||||
* dist/host/ui.js @termix-ssh/plugin-sdk/ui, the shell's UI kit
|
* dist/host/ui.js @termix-ssh/plugin-sdk/ui, the shell's UI kit
|
||||||
* dist/host/testing-host.js what renderWithApp() renders plugins into
|
* dist/host/testing-host.js what renderWithApp() renders plugins into
|
||||||
|
* dist/host/core-classes.json the classes core's CSS has rules for
|
||||||
*
|
*
|
||||||
* Inside Termix the page's import map points @termix-ssh/plugin-sdk/ui at the
|
* Inside Termix the page's import map points @termix-ssh/plugin-sdk/ui at the
|
||||||
* shell's own copy, so ui.js is never loaded there. It exists so a plugin in
|
* shell's own copy, so ui.js is never loaded there. It exists so a plugin in
|
||||||
@@ -18,6 +19,9 @@ import path from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
|
import { compile, optimize } from "@tailwindcss/node";
|
||||||
|
import { Scanner } from "@tailwindcss/oxide";
|
||||||
|
import { classNamesInCss } from "../packages/plugin-sdk/cli/lib/css-classes.mjs";
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const outDir = path.join(root, "packages", "plugin-sdk", "dist", "host");
|
const outDir = path.join(root, "packages", "plugin-sdk", "dist", "host");
|
||||||
@@ -137,4 +141,30 @@ fs.copyFileSync(
|
|||||||
path.join(outDir, "theme.css"),
|
path.join(outDir, "theme.css"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Every class core's own CSS has a rule for, found the way the Vite build
|
||||||
|
// finds them, so termix-plugin build knows which plugin classes it shares.
|
||||||
|
const indexCss = path.join(root, "src/ui/index.css");
|
||||||
|
const compiler = await compile(fs.readFileSync(indexCss, "utf8"), {
|
||||||
|
base: path.dirname(indexCss),
|
||||||
|
onDependency: () => {},
|
||||||
|
});
|
||||||
|
const rootSource =
|
||||||
|
compiler.root === "none"
|
||||||
|
? []
|
||||||
|
: compiler.root === null
|
||||||
|
? [{ base: root, pattern: "**/*", negated: false }]
|
||||||
|
: [{ ...compiler.root, negated: false }];
|
||||||
|
const scanner = new Scanner({
|
||||||
|
sources: [...rootSource, ...compiler.sources],
|
||||||
|
});
|
||||||
|
const coreClasses = [
|
||||||
|
...classNamesInCss(
|
||||||
|
optimize(compiler.build(scanner.scan()), { minify: true }).code,
|
||||||
|
),
|
||||||
|
].sort();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(outDir, "core-classes.json"),
|
||||||
|
`${JSON.stringify(coreClasses)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Built the SDK host modules into ${path.relative(root, outDir)}`);
|
console.log(`Built the SDK host modules into ${path.relative(root, outDir)}`);
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
buildBundledIndex,
|
buildBundledIndex,
|
||||||
findLocalPluginBuilds,
|
findLocalPluginBuilds,
|
||||||
isLocalBuildStale,
|
isLocalBuildStale,
|
||||||
|
localSdkBuildCommand,
|
||||||
rebuildLocalPlugins,
|
rebuildLocalPlugins,
|
||||||
copyLocalBuild,
|
copyLocalBuild,
|
||||||
} from "./lib/bundled-plugins.cjs";
|
} from "./lib/bundled-plugins.cjs";
|
||||||
@@ -354,6 +355,40 @@ describe("rebuilding local plugins", () => {
|
|||||||
expect(isLocalBuildStale(root)).toBe(true);
|
expect(isLocalBuildStale(root)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("is stale when the SDK CLI changed after the build", () => {
|
||||||
|
const root = repo(tempDir(), "Plugin-A", "a");
|
||||||
|
const cli = path.join(tempDir(), "cli");
|
||||||
|
fs.mkdirSync(cli);
|
||||||
|
fs.writeFileSync(path.join(cli, "index.mjs"), "x");
|
||||||
|
fs.mkdirSync(path.join(root, "dist"));
|
||||||
|
fs.writeFileSync(path.join(root, "dist", "frontend.js"), "x");
|
||||||
|
touch(path.join(root, "manifest.json"), 60);
|
||||||
|
touch(path.join(root, "src", "index.ts"), 60);
|
||||||
|
touch(path.join(cli, "index.mjs"), 60);
|
||||||
|
expect(isLocalBuildStale(root, cli)).toBe(false);
|
||||||
|
|
||||||
|
touch(path.join(root, "dist", "frontend.js"), 30);
|
||||||
|
touch(path.join(cli, "index.mjs"), 0);
|
||||||
|
expect(isLocalBuildStale(root, cli)).toBe(true);
|
||||||
|
expect(isLocalBuildStale(root)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds with this checkout's SDK CLI, not the plugin's own", () => {
|
||||||
|
const [command, args] = localSdkBuildCommand();
|
||||||
|
expect(command).toBe(process.execPath);
|
||||||
|
expect(args).toEqual([
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"packages",
|
||||||
|
"plugin-sdk",
|
||||||
|
"cli",
|
||||||
|
"index.mjs",
|
||||||
|
),
|
||||||
|
"build",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("builds only stale repos for bundled ids that have node_modules", () => {
|
it("builds only stale repos for bundled ids that have node_modules", () => {
|
||||||
const dir = tempDir();
|
const dir = tempDir();
|
||||||
repo(dir, "Plugin-A", "a");
|
repo(dir, "Plugin-A", "a");
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Fails when core reads an env var that env.catalog.json does not list. The
|
||||||
|
// docs site builds its env var page from that file.
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
|
const DIRS = ["src/backend", "electron"];
|
||||||
|
const PATTERNS = [
|
||||||
|
/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,
|
||||||
|
/_ENV\s*=\s*["']([A-Z][A-Z0-9_]*)["']/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
function walk(dir, out = []) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
if (entry.name === "node_modules" || entry.name === "tests") continue;
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(full, out);
|
||||||
|
else if (
|
||||||
|
/\.(ts|cjs|mjs|js)$/.test(entry.name) &&
|
||||||
|
!/\.test\./.test(entry.name)
|
||||||
|
)
|
||||||
|
out.push(full);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNames(root = ROOT) {
|
||||||
|
const found = new Map();
|
||||||
|
for (const dir of DIRS) {
|
||||||
|
const abs = path.join(root, dir);
|
||||||
|
if (!fs.existsSync(abs)) continue;
|
||||||
|
for (const file of walk(abs)) {
|
||||||
|
const text = fs.readFileSync(file, "utf8");
|
||||||
|
for (const pattern of PATTERNS) {
|
||||||
|
for (const match of text.matchAll(pattern)) {
|
||||||
|
if (!found.has(match[1]))
|
||||||
|
found.set(match[1], path.relative(root, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const catalog = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(ROOT, "env.catalog.json"), "utf8"),
|
||||||
|
);
|
||||||
|
const listed = new Set(catalog.vars.map((v) => v.name));
|
||||||
|
const problems = [];
|
||||||
|
for (const v of catalog.vars) {
|
||||||
|
if (!catalog.groups[v.group])
|
||||||
|
problems.push(`${v.name}: unknown group "${v.group}"`);
|
||||||
|
if (!v.description?.trim()) problems.push(`${v.name}: no description`);
|
||||||
|
}
|
||||||
|
for (const [name, file] of readNames()) {
|
||||||
|
if (!listed.has(name))
|
||||||
|
problems.push(
|
||||||
|
`${name} is read in ${file} but missing from env.catalog.json`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (problems.length) {
|
||||||
|
for (const p of problems) console.error(` ${p}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`env catalog ok (${listed.size} vars)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) main();
|
||||||
|
module.exports = { readNames, PATTERNS };
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const { readNames } = require("./check-env-catalog.cjs") as {
|
||||||
|
readNames: (root: string) => Map<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let root: string | null = null;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
root = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function fixture(files: Record<string, string>): string {
|
||||||
|
root = fs.mkdtempSync(path.join(os.tmpdir(), "env-catalog-"));
|
||||||
|
for (const [file, content] of Object.entries(files)) {
|
||||||
|
const full = path.join(root, file);
|
||||||
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||||
|
fs.writeFileSync(full, content);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("readNames", () => {
|
||||||
|
it("finds every way core reads an env var", () => {
|
||||||
|
const dir = fixture({
|
||||||
|
"src/backend/a.ts": [
|
||||||
|
"const a = process.env.FIRST_VAR;",
|
||||||
|
'const b = process.env["SECOND_VAR"];',
|
||||||
|
"const c = env.THIRD_VAR;",
|
||||||
|
'export const X_ENV =\n "FOURTH_VAR";',
|
||||||
|
].join("\n"),
|
||||||
|
"electron/main.cjs": "process.env.DESKTOP_VAR",
|
||||||
|
});
|
||||||
|
expect([...readNames(dir).keys()].sort()).toEqual([
|
||||||
|
"DESKTOP_VAR",
|
||||||
|
"FIRST_VAR",
|
||||||
|
"FOURTH_VAR",
|
||||||
|
"SECOND_VAR",
|
||||||
|
"THIRD_VAR",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips tests", () => {
|
||||||
|
const dir = fixture({
|
||||||
|
"src/backend/tests/a.test.ts": "process.env.ONLY_IN_TESTS",
|
||||||
|
"src/backend/b.test.ts": "process.env.ALSO_TESTS",
|
||||||
|
});
|
||||||
|
expect(readNames(dir).size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -79,6 +79,8 @@ const ALLOWED = [
|
|||||||
"DialogPortal",
|
"DialogPortal",
|
||||||
"DialogTitle",
|
"DialogTitle",
|
||||||
"DialogTrigger",
|
"DialogTrigger",
|
||||||
|
"DocsLink",
|
||||||
|
"DocsLinkProps",
|
||||||
"DropdownMenu",
|
"DropdownMenu",
|
||||||
"DropdownMenuCheckboxItem",
|
"DropdownMenuCheckboxItem",
|
||||||
"DropdownMenuContent",
|
"DropdownMenuContent",
|
||||||
|
|||||||
+8
-3
@@ -45,7 +45,10 @@ import {
|
|||||||
} from "./lib/dev-docker.mjs";
|
} from "./lib/dev-docker.mjs";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const { copyLocalBuild } = require("./lib/bundled-plugins.cjs");
|
const {
|
||||||
|
copyLocalBuild,
|
||||||
|
localSdkBuildCommand,
|
||||||
|
} = require("./lib/bundled-plugins.cjs");
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const BACKEND_ENTRY = path.join(
|
const BACKEND_ENTRY = path.join(
|
||||||
@@ -83,7 +86,9 @@ function run(command, args, cwd = root) {
|
|||||||
const child = spawn(command, args, {
|
const child = spawn(command, args, {
|
||||||
cwd,
|
cwd,
|
||||||
stdio: ["ignore", "inherit", "inherit"],
|
stdio: ["ignore", "inherit", "inherit"],
|
||||||
shell: process.platform === "win32",
|
// npm is a .cmd on Windows and needs the shell, but the shell splits
|
||||||
|
// arguments on spaces, so a path like "Personal Projects" gets cut.
|
||||||
|
shell: process.platform === "win32" && !path.isAbsolute(command),
|
||||||
});
|
});
|
||||||
child.on("error", reject);
|
child.on("error", reject);
|
||||||
child.on("exit", (code) =>
|
child.on("exit", (code) =>
|
||||||
@@ -541,7 +546,7 @@ function watchCoreForDocker() {
|
|||||||
async function rebuildPlugin({ id, repo }) {
|
async function rebuildPlugin({ id, repo }) {
|
||||||
log(`building ${id}`);
|
log(`building ${id}`);
|
||||||
try {
|
try {
|
||||||
await run("npm", ["run", "build"], repo);
|
await run(...localSdkBuildCommand(), repo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
warn(`${id} failed to build: ${error.message}`);
|
warn(`${id} failed to build: ${error.message}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
* drizzle/mysql. Nothing imports them to run a query.
|
* drizzle/mysql. Nothing imports them to run a query.
|
||||||
*
|
*
|
||||||
* That is not an oversight. The query builder needs two things from a table
|
* That is not an oversight. The query builder needs two things from a table
|
||||||
* object — the identifiers to interpolate, and the encoders that turn JS values
|
* object: the identifiers to interpolate, and the encoders that turn JS values
|
||||||
* into driver values — and the sqlite definitions supply both correctly for
|
* into driver values. The sqlite definitions supply both correctly for
|
||||||
* every engine, which is why all 44 repositories import schema.ts directly:
|
* every engine, which is why all 44 repositories import schema.ts directly:
|
||||||
*
|
*
|
||||||
* - text and integer encode as themselves everywhere
|
* - text and integer encode as themselves everywhere
|
||||||
@@ -17,15 +17,15 @@
|
|||||||
* which is true for JS `true` as well as for 1
|
* which is true for JS `true` as well as for 1
|
||||||
* - real is a plain number on all three
|
* - real is a plain number on all three
|
||||||
*
|
*
|
||||||
* What genuinely differs between the dialects is DDL — column types, key
|
* What genuinely differs between the dialects is DDL (column types, key
|
||||||
* lengths, autoincrement syntax — and DDL is exactly what these files exist to
|
* lengths, autoincrement syntax), and DDL is exactly what these files exist to
|
||||||
* generate. See scripts/verify-dialects.mjs, which asserts the round-trips
|
* generate. See scripts/verify-dialects.mjs, which asserts the round-trips
|
||||||
* above against real servers rather than trusting this comment.
|
* above against real servers rather than trusting this comment.
|
||||||
*
|
*
|
||||||
* The schema is declared once, in sqlite-core, and the other two dialects are
|
* The schema is declared once, in sqlite-core, and the other two dialects are
|
||||||
* derived. Hand-maintaining three copies of 52 tables would mean a renamed
|
* derived. Hand-maintaining three copies of 52 tables would mean a renamed
|
||||||
* table has to land in three places consistently or a foreign key silently
|
* table has to land in three places consistently or a foreign key silently
|
||||||
* points at the wrong one — and the schema is regular enough that the mapping
|
* points at the wrong one, and the schema is regular enough that the mapping
|
||||||
* is mechanical.
|
* is mechanical.
|
||||||
*
|
*
|
||||||
* What varies between dialects is small and closed:
|
* What varies between dialects is small and closed:
|
||||||
@@ -198,7 +198,7 @@ function transform(source, dialect) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Timestamps are stored as text (see sql-timestamp.ts). MySQL only accepts
|
// Timestamps are stored as text (see sql-timestamp.ts). MySQL only accepts
|
||||||
// DEFAULT CURRENT_TIMESTAMP on a DATETIME or TIMESTAMP column — on a TEXT
|
// DEFAULT CURRENT_TIMESTAMP on a DATETIME or TIMESTAMP column. On a TEXT
|
||||||
// one it is ER_INVALID_DEFAULT, "Invalid default value". Since 8.0.13 an
|
// one it is ER_INVALID_DEFAULT, "Invalid default value". Since 8.0.13 an
|
||||||
// expression default works on any type, and an expression is written
|
// expression default works on any type, and an expression is written
|
||||||
// parenthesised. MariaDB accepts the bare form, which is why this only
|
// parenthesised. MariaDB accepts the bare form, which is why this only
|
||||||
@@ -272,13 +272,13 @@ function transform(source, dialect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function header(dialect) {
|
function header(dialect) {
|
||||||
return `// GENERATED FILE — do not edit.
|
return `// GENERATED FILE, do not edit.
|
||||||
//
|
//
|
||||||
// Produced from schema.ts by scripts/generate-dialect-schema.cjs.
|
// Produced from schema.ts by scripts/generate-dialect-schema.cjs.
|
||||||
// Edit the sqlite schema and re-run \`node scripts/generate-dialect-schema.cjs\`.
|
// Edit the sqlite schema and re-run \`node scripts/generate-dialect-schema.cjs\`.
|
||||||
// Target dialect: ${dialect}.
|
// Target dialect: ${dialect}.
|
||||||
//
|
//
|
||||||
// DDL source for drizzle-kit. NOT imported to run queries — repositories use
|
// DDL source for drizzle-kit. NOT imported to run queries. Repositories use
|
||||||
// schema.ts on every dialect. See the generator header for why that is correct.
|
// schema.ts on every dialect. See the generator header for why that is correct.
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ describe("mysql output", () => {
|
|||||||
expect(out).toContain('double("score")');
|
expect(out).toContain('double("score")');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("makes key columns varchar — MySQL cannot index unbounded TEXT", () => {
|
it("makes key columns varchar since MySQL cannot index unbounded TEXT", () => {
|
||||||
expect(out).toContain('varchar("user_id", { length: 255 })');
|
expect(out).toContain('varchar("user_id", { length: 255 })');
|
||||||
expect(out).toContain('text("name")');
|
expect(out).toContain('text("name")');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ await sharp(svgBuffer)
|
|||||||
.toFile(join(publicDir, "full-icon.png"));
|
.toFile(join(publicDir, "full-icon.png"));
|
||||||
console.log(" ✓ full-icon.png");
|
console.log(" ✓ full-icon.png");
|
||||||
|
|
||||||
// favicon.ico — embed 16, 32, 48 px layers
|
// favicon.ico: embed 16, 32, 48 px layers
|
||||||
console.log("Generating favicon.ico...");
|
console.log("Generating favicon.ico...");
|
||||||
const icoSizes = [16, 32, 48];
|
const icoSizes = [16, 32, 48];
|
||||||
const icoBuffers = await Promise.all(
|
const icoBuffers = await Promise.all(
|
||||||
@@ -55,7 +55,7 @@ const icoBuffers = await Promise.all(
|
|||||||
writeFileSync(join(publicDir, "favicon.ico"), buildIco(icoBuffers, icoSizes));
|
writeFileSync(join(publicDir, "favicon.ico"), buildIco(icoBuffers, icoSizes));
|
||||||
console.log(" ✓ favicon.ico");
|
console.log(" ✓ favicon.ico");
|
||||||
|
|
||||||
// icon.ico — embed 16, 32, 48, 64, 128, 256 px layers
|
// icon.ico: embed 16, 32, 48, 64, 128, 256 px layers
|
||||||
console.log("Generating icon.ico...");
|
console.log("Generating icon.ico...");
|
||||||
const winSizes = [16, 32, 48, 64, 128, 256];
|
const winSizes = [16, 32, 48, 64, 128, 256];
|
||||||
const winBuffers = await Promise.all(
|
const winBuffers = await Promise.all(
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Writes openapi.json for core from the @openapi blocks in src/backend.
|
||||||
|
// The docs site runs the same builder on each release.
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { buildOpenApi } from "@termix-ssh/plugin-sdk/openapi";
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const pkg = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(root, "package.json"), "utf8"),
|
||||||
|
);
|
||||||
|
const tags = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(root, "src/backend/utils/openapi-tags.json"),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const out = process.argv[2] ?? path.join(root, "openapi.json");
|
||||||
|
|
||||||
|
const spec = await buildOpenApi({
|
||||||
|
title: "Termix API",
|
||||||
|
version: pkg.version,
|
||||||
|
description: "The core Termix API. Each plugin documents its own routes.",
|
||||||
|
files: [path.join(root, "src/backend/**/!(*.test).ts")],
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(out, `${JSON.stringify(spec, null, 2)}\n`);
|
||||||
|
console.log(
|
||||||
|
`wrote ${path.relative(root, out)} (${Object.keys(spec.paths ?? {}).length} paths)`,
|
||||||
|
);
|
||||||
@@ -28,7 +28,7 @@ function fail(message) {
|
|||||||
function buildTable(version, mobileVersion, mobileTag) {
|
function buildTable(version, mobileVersion, mobileTag) {
|
||||||
const tag = `v${version}`;
|
const tag = `v${version}`;
|
||||||
const base = `https://github.com/Termix-SSH/Termix/releases/download/${tag}`;
|
const base = `https://github.com/Termix-SSH/Termix/releases/download/${tag}`;
|
||||||
const mobileBase = `https://github.com/Termix-SSH/Mobile/releases/download/${mobileTag || `release-${mobileVersion}-tag`}`;
|
const mobileBase = `https://github.com/Termix-SSH/Mobile/releases/download/${mobileTag || `v${mobileVersion}`}`;
|
||||||
|
|
||||||
const win = (file) => `${base}/${file}`;
|
const win = (file) => `${base}/${file}`;
|
||||||
const linux = (file) => `${base}/${file}`;
|
const linux = (file) => `${base}/${file}`;
|
||||||
@@ -37,11 +37,11 @@ function buildTable(version, mobileVersion, mobileTag) {
|
|||||||
return [
|
return [
|
||||||
`| Architecture | Windows | Linux | Mac | Android | iOS |`,
|
`| Architecture | Windows | Linux | Mac | Android | iOS |`,
|
||||||
`|------------------|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------------------------------------|-----------------------------------|`,
|
`|------------------|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------------------------------------|-----------------------------------|`,
|
||||||
`| **x86-64 (64-bit)** | [EXE](${win("termix_windows_x64_nsis.exe")}) · [MSI](${win("termix_windows_x64_msi.msi")}) · [Portable](${win("termix_windows_x64_portable.zip")}) | [AppImage](${linux("termix_linux_x64_appimage.AppImage")}) · [DEB](${linux("termix_linux_x64_deb.deb")}) · [Portable](${linux("termix_linux_x64_portable.tar.gz")}) | [DMG](${mac("termix_macos_x64_dmg.dmg")}) | — | — |`,
|
`| **x86-64 (64-bit)** | [EXE](${win("termix_windows_x64_nsis.exe")}) · [MSI](${win("termix_windows_x64_msi.msi")}) · [Portable](${win("termix_windows_x64_portable.zip")}) | [AppImage](${linux("termix_linux_x64_appimage.AppImage")}) · [DEB](${linux("termix_linux_x64_deb.deb")}) · [Portable](${linux("termix_linux_x64_portable.tar.gz")}) | [DMG](${mac("termix_macos_x64_dmg.dmg")}) | - | - |`,
|
||||||
`| **AArch64 (ARM64)** | — | [AppImage](${linux("termix_linux_arm64_appimage.AppImage")}) · [DEB](${linux("termix_linux_arm64_deb.deb")}) · [Portable](${linux("termix_linux_arm64_portable.tar.gz")}) | [DMG](${mac("termix_macos_arm64_dmg.dmg")}) | [APK (${mobileVersion})](${mobileBase}/termix_android.apk) | [IPA (${mobileVersion})](${mobileBase}/termix_ios.ipa) |`,
|
`| **AArch64 (ARM64)** | - | [AppImage](${linux("termix_linux_arm64_appimage.AppImage")}) · [DEB](${linux("termix_linux_arm64_deb.deb")}) · [Portable](${linux("termix_linux_arm64_portable.tar.gz")}) | [DMG](${mac("termix_macos_arm64_dmg.dmg")}) | [APK (${mobileVersion})](${mobileBase}/termix_android.apk) | [IPA (${mobileVersion})](${mobileBase}/termix_ios.ipa) |`,
|
||||||
`| **ARMv7 (32-bit)** | — | [AppImage](${linux("termix_linux_armv7l_appimage.AppImage")}) · [DEB](${linux("termix_linux_armv7l_deb.deb")}) · [Portable](${linux("termix_linux_armv7l_portable.tar.gz")}) | — | — | — |`,
|
`| **ARMv7 (32-bit)** | - | [AppImage](${linux("termix_linux_armv7l_appimage.AppImage")}) · [DEB](${linux("termix_linux_armv7l_deb.deb")}) · [Portable](${linux("termix_linux_armv7l_portable.tar.gz")}) | - | - | - |`,
|
||||||
`| **x86-32 (32-bit)** | [EXE](${win("termix_windows_ia32_nsis.exe")}) · [MSI](${win("termix_windows_ia32_msi.msi")}) · [Portable](${win("termix_windows_ia32_portable.zip")}) | — | — | — | — |`,
|
`| **x86-32 (32-bit)** | [EXE](${win("termix_windows_ia32_nsis.exe")}) · [MSI](${win("termix_windows_ia32_msi.msi")}) · [Portable](${win("termix_windows_ia32_portable.zip")}) | - | - | - | - |`,
|
||||||
`| **Universal** | [Chocolatey](https://docs.termix.site/install/connector/windows) | [Flatpak](https://docs.termix.site/install/connector/linux) | [DMG](${mac("termix_macos_universal_dmg.dmg")}) · [App Store](https://apps.apple.com/us/app/termix-ssh-companion/id6752672071) · [Homebrew](https://docs.termix.site/install/connector/macos) | — | — |`,
|
`| **Universal** | [Chocolatey](https://docs.termix.site/install/apps/windows) | [Flatpak](https://docs.termix.site/install/apps/linux) | [DMG](${mac("termix_macos_universal_dmg.dmg")}) · [App Store](https://apps.apple.com/us/app/termix-ssh-companion/id6752672071) · [Homebrew](https://docs.termix.site/install/apps/macos) | - | - |`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe("buildBody", () => {
|
|||||||
expect(body).not.toContain("A bug");
|
expect(body).not.toContain("A bug");
|
||||||
expect(body).toContain("/releases/download/v26.10.0/termix_");
|
expect(body).toContain("/releases/download/v26.10.0/termix_");
|
||||||
expect(body).toContain(
|
expect(body).toContain(
|
||||||
"Mobile/releases/download/release-1.2.0-tag/termix_android.apk",
|
"Mobile/releases/download/v1.2.0/termix_android.apk",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -299,14 +299,37 @@ function newestMtime(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** True when a plugin repo has never been built or its sources changed since. */
|
/** True when a plugin repo has never been built or its sources changed since. */
|
||||||
function isLocalBuildStale(repo) {
|
/** The SDK CLI in this checkout, which dev builds local plugins with. */
|
||||||
|
const LOCAL_SDK_CLI = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"packages",
|
||||||
|
"plugin-sdk",
|
||||||
|
"cli",
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the build is missing, a source is newer than it, or the SDK CLI
|
||||||
|
* that builds it changed since (sdkCli, when given).
|
||||||
|
*/
|
||||||
|
function isLocalBuildStale(repo, sdkCli) {
|
||||||
const built = newestMtime(path.join(repo, "dist"));
|
const built = newestMtime(path.join(repo, "dist"));
|
||||||
if (!built) return true;
|
if (!built) return true;
|
||||||
|
if (sdkCli && newestMtime(sdkCli) > built) return true;
|
||||||
return BUILD_INPUTS.some(
|
return BUILD_INPUTS.some(
|
||||||
(entry) => newestMtime(path.join(repo, entry)) > built,
|
(entry) => newestMtime(path.join(repo, entry)) > built,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command that builds a plugin repo with this checkout's SDK instead of
|
||||||
|
* the published one in its node_modules, so SDK edits apply right away.
|
||||||
|
*/
|
||||||
|
function localSdkBuildCommand() {
|
||||||
|
return [process.execPath, [path.join(LOCAL_SDK_CLI, "index.mjs"), "build"]];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds each plugin repo whose build is missing or out of date. ids limits
|
* Builds each plugin repo whose build is missing or out of date. ids limits
|
||||||
* it to those plugins; null builds every one.
|
* it to those plugins; null builds every one.
|
||||||
@@ -325,7 +348,8 @@ function rebuildLocalPlugins(dir, ids, run) {
|
|||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((ids && !ids.has(id)) || !isLocalBuildStale(repo)) continue;
|
if ((ids && !ids.has(id)) || !isLocalBuildStale(repo, LOCAL_SDK_CLI))
|
||||||
|
continue;
|
||||||
run(repo, id);
|
run(repo, id);
|
||||||
rebuilt.push(id);
|
rebuilt.push(id);
|
||||||
}
|
}
|
||||||
@@ -346,6 +370,7 @@ function copyLocalBuild(repo, destination) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
findLocalPluginBuilds,
|
findLocalPluginBuilds,
|
||||||
isLocalBuildStale,
|
isLocalBuildStale,
|
||||||
|
localSdkBuildCommand,
|
||||||
rebuildLocalPlugins,
|
rebuildLocalPlugins,
|
||||||
copyLocalBuild,
|
copyLocalBuild,
|
||||||
parseBundledPlugins,
|
parseBundledPlugins,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
build,
|
build,
|
||||||
lowerPluginLayers,
|
lowerPluginLayers,
|
||||||
} from "../packages/plugin-sdk/cli/commands/build.mjs";
|
} from "../packages/plugin-sdk/cli/commands/build.mjs";
|
||||||
|
import {
|
||||||
|
classNamesInCss,
|
||||||
|
splitByCoreClasses,
|
||||||
|
} from "../packages/plugin-sdk/cli/lib/css-classes.mjs";
|
||||||
|
|
||||||
const cleanups: Array<() => void> = [];
|
const cleanups: Array<() => void> = [];
|
||||||
|
|
||||||
@@ -102,6 +106,76 @@ describe("plugin CSS layers", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can leave a plugin's own classes in core's utilities layer", () => {
|
||||||
|
expect(
|
||||||
|
lowerPluginLayers(
|
||||||
|
"@layer properties{a{b:c}}@layer utilities{.w-px{width:1px}}",
|
||||||
|
{ utilities: false },
|
||||||
|
),
|
||||||
|
).toBe(
|
||||||
|
"@layer termix-plugin-properties{a{b:c}}@layer utilities{.w-px{width:1px}}",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads class names from selectors, not declarations", () => {
|
||||||
|
const names = classNamesInCss(
|
||||||
|
String.raw`.w-28{width:calc(var(--spacing) * 28)}@media (width>=48rem){.md\:w-44{width:0.5rem}}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.top-1\/2{top:50%}.\32 xl\:flex{display:flex}`,
|
||||||
|
);
|
||||||
|
expect([...names].sort()).toEqual(
|
||||||
|
[
|
||||||
|
"2xl:flex",
|
||||||
|
"group",
|
||||||
|
"group-hover:opacity-100",
|
||||||
|
"md:w-44",
|
||||||
|
"top-1/2",
|
||||||
|
"w-28",
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits a plugin's classes by whether core ships them", () => {
|
||||||
|
expect(
|
||||||
|
splitByCoreClasses(
|
||||||
|
["w-28", "md:w-44", "hidden"],
|
||||||
|
new Set(["w-28", "hidden"]),
|
||||||
|
),
|
||||||
|
).toEqual({ shared: ["w-28", "hidden"], own: ["md:w-44"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps variants core ships with the plugin's own classes", () => {
|
||||||
|
expect(
|
||||||
|
splitByCoreClasses(
|
||||||
|
["max-h-56", "md:max-h-none", "flex-col", "md:flex-row"],
|
||||||
|
new Set(["md:max-h-none", "flex-col", "md:flex-row"]),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
shared: ["flex-col"],
|
||||||
|
own: ["max-h-56", "md:max-h-none", "md:flex-row"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps classes core ships below core and the plugin's own beside core's", async () => {
|
||||||
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
// Core's class list comes from scanning this repo, this file included,
|
||||||
|
// so the plugin-only class is put together here instead of written out.
|
||||||
|
const width = ["w-", "[321px]"].join("");
|
||||||
|
const dir = fixturePlugin();
|
||||||
|
fs.mkdirSync(path.join(dir, "src", "frontend"), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, "src", "frontend", "index.tsx"),
|
||||||
|
`export const a = <div className="hidden md:${width}" />;\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await build({ cwd: dir });
|
||||||
|
|
||||||
|
const css = fs.readFileSync(path.join(dir, "dist", "frontend.css"), "utf8");
|
||||||
|
const lowered = css.slice(css.indexOf("@layer termix-plugin-utilities"));
|
||||||
|
const own = css.slice(css.lastIndexOf("@layer utilities{"));
|
||||||
|
expect(lowered).toMatch(/^@layer termix-plugin-utilities\{[^@]*\.hidden\{/);
|
||||||
|
expect(own).toContain(String.raw`.md\:w-\[321px\]`);
|
||||||
|
expect(own).not.toContain(".hidden{");
|
||||||
|
});
|
||||||
|
|
||||||
it("is ordered in core's CSS between base and core's utilities", () => {
|
it("is ordered in core's CSS between base and core's utilities", () => {
|
||||||
const css = fs.readFileSync(
|
const css = fs.readFileSync(
|
||||||
path.join(__dirname, "..", "src", "ui", "index.css"),
|
path.join(__dirname, "..", "src", "ui", "index.css"),
|
||||||
|
|||||||
@@ -131,6 +131,17 @@ describe("validateChangelog", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets a beta through when its notes are not written yet", () => {
|
||||||
|
expect(validateChangelog(FILE, "1.1.0-beta.1")).toEqual([]);
|
||||||
|
expect(validateChangelog(FILE, "1.2.0-beta.3")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags a beta older than the newest release", () => {
|
||||||
|
expect(validateChangelog(FILE, "1.0.1-beta.1")).toContain(
|
||||||
|
"the newest release is 1.1.0, but the manifest version is 1.0.1-beta.1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("flags a file with no releases", () => {
|
it("flags a file with no releases", () => {
|
||||||
expect(validateChangelog("# Changelog\n", "1.0.0")).toEqual([
|
expect(validateChangelog("# Changelog\n", "1.0.0")).toEqual([
|
||||||
'no releases found, add a "## 1.0.0" heading',
|
'no releases found, add a "## 1.0.0" heading',
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { validate } from "../packages/plugin-sdk/cli/commands/validate.mjs";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
for (const root of roots.splice(0))
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function plugin(dialects: string[]) {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-validate-"));
|
||||||
|
roots.push(root);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(root, "manifest.json"),
|
||||||
|
JSON.stringify({ id: "fixture", version: "1.0.0" }),
|
||||||
|
);
|
||||||
|
for (const dialect of dialects) {
|
||||||
|
const dir = path.join(root, "migrations", dialect);
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, "0001_init.sql"),
|
||||||
|
"CREATE TABLE p_fixture_items (id INTEGER PRIMARY KEY);\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function problems(cwd: string) {
|
||||||
|
const lines: string[] = [];
|
||||||
|
vi.spyOn(console, "error").mockImplementation((line) => lines.push(line));
|
||||||
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
await expect(validate({ cwd })).rejects.toThrow();
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("termix-plugin validate migrations", () => {
|
||||||
|
it("flags an engine with no migrations folder", async () => {
|
||||||
|
const output = await problems(plugin(["sqlite", "postgres"]));
|
||||||
|
expect(output).toContain("migrations/mysql is missing");
|
||||||
|
expect(output).not.toContain("migrations/postgres is missing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts all three engines", async () => {
|
||||||
|
const output = await problems(plugin(["sqlite", "postgres", "mysql"]));
|
||||||
|
expect(output).not.toMatch(/migrations\/\w+ is missing/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
* Runs the repository layer against a real Postgres or MySQL server.
|
* Runs the repository layer against a real Postgres or MySQL server.
|
||||||
*
|
*
|
||||||
* The unit tests only ever see SQLite, so the parts of this codebase that
|
* The unit tests only ever see SQLite, so the parts of this codebase that
|
||||||
* differ per engine — the RETURNING replacements, the read-then-write
|
* differ per engine (the RETURNING replacements, the read-then-write
|
||||||
* transactions, the value encoders — have no coverage there at all. This is
|
* transactions, the value encoders) have no coverage there at all. This is
|
||||||
* what covers them, and it needs a live server, which is why it is a script
|
* what covers them, and it needs a live server, which is why it is a script
|
||||||
* rather than a test.
|
* rather than a test.
|
||||||
*
|
*
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
* npm run verify:dialect -- mysql://user:pass@host:3306/db
|
* npm run verify:dialect -- mysql://user:pass@host:3306/db
|
||||||
*
|
*
|
||||||
* Applies the migrations first, through the same runRemoteMigrations() the
|
* Applies the migrations first, through the same runRemoteMigrations() the
|
||||||
* application uses at startup — so a broken migration fails here rather than in
|
* application uses at startup, so a broken migration fails here rather than in
|
||||||
* production. Writes real rows: point it at a scratch database.
|
* production. Writes real rows: point it at a scratch database.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ const { drizzle } = await import(
|
|||||||
dialect === "postgres" ? "drizzle-orm/node-postgres" : "drizzle-orm/mysql2"
|
dialect === "postgres" ? "drizzle-orm/node-postgres" : "drizzle-orm/mysql2"
|
||||||
);
|
);
|
||||||
|
|
||||||
// No schema option on purpose — see connect.ts.
|
// No schema option on purpose, see connect.ts.
|
||||||
const db = drizzle(url);
|
const db = drizzle(url);
|
||||||
const context = { dialect, drizzle: db };
|
const context = { dialect, drizzle: db };
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ check("insert returns the stored row", created?.username, "before");
|
|||||||
check("boolean true survives the round trip", created?.isAdmin, true);
|
check("boolean true survives the round trip", created?.isAdmin, true);
|
||||||
|
|
||||||
// updateReturning must report the state AFTER the write. Reading first would
|
// updateReturning must report the state AFTER the write. Reading first would
|
||||||
// return the value the update replaced — silently, with no error.
|
// return the value the update replaced, silently, with no error.
|
||||||
const updated = await users.update(userId, { username: "after" });
|
const updated = await users.update(userId, { username: "after" });
|
||||||
check("update returns the new value", updated?.username, "after");
|
check("update returns the new value", updated?.username, "after");
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export function termixPluginHost({ repoRoot, sdkUiEntry, sdkFrontendEntry }) {
|
|||||||
if (SHARED_VENDOR_MODULES.includes(specifier)) {
|
if (SHARED_VENDOR_MODULES.includes(specifier)) {
|
||||||
return vendorFacade(repoRoot, specifier);
|
return vendorFacade(repoRoot, specifier);
|
||||||
}
|
}
|
||||||
if (specifier === "@termix-ssh/plugin-sdk/ui") return sourceFacade(sdkUiEntry);
|
if (specifier === "@termix-ssh/plugin-sdk/ui")
|
||||||
|
return sourceFacade(sdkUiEntry);
|
||||||
if (specifier === "@termix-ssh/plugin-sdk/frontend") {
|
if (specifier === "@termix-ssh/plugin-sdk/frontend") {
|
||||||
return sourceFacade(sdkFrontendEntry);
|
return sourceFacade(sdkFrontendEntry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import type { Request, Response } from "express";
|
|||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
import { loginRateLimiter } from "../utils/login-rate-limiter.js";
|
import { loginRateLimiter } from "../utils/login-rate-limiter.js";
|
||||||
import { authLogger } from "../utils/logger.js";
|
import { authLogger } from "../utils/logger.js";
|
||||||
import {
|
import { parseUserAgent } from "../utils/user-agent-parser.js";
|
||||||
generateDeviceFingerprint,
|
|
||||||
getDeviceId,
|
|
||||||
parseUserAgent,
|
|
||||||
} from "../utils/user-agent-parser.js";
|
|
||||||
import { logAudit, getRequestMeta } from "../utils/audit-logger.js";
|
import { logAudit, getRequestMeta } from "../utils/audit-logger.js";
|
||||||
import {
|
import {
|
||||||
createCurrentUserAuthRepository,
|
createCurrentUserAuthRepository,
|
||||||
@@ -37,6 +33,7 @@ import {
|
|||||||
type SecondFactor,
|
type SecondFactor,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
import {
|
import {
|
||||||
|
isNativeAppRequest,
|
||||||
issueSession,
|
issueSession,
|
||||||
sendSession,
|
sendSession,
|
||||||
syncSharedCredentialsForUserRoles,
|
syncSharedCredentialsForUserRoles,
|
||||||
@@ -131,10 +128,33 @@ function shouldRunSecondFactors(methodId: string): boolean {
|
|||||||
return isSecondFactorAfterExternalLoginEnabled();
|
return isSecondFactorAfterExternalLoginEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TRUST_DEVICE_COOKIE = "termix_trust_device";
|
||||||
|
const TRUST_TOKEN_PATTERN = /^[a-f0-9]{64}$/;
|
||||||
|
const TRUST_TOKEN_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remember-this-device token the server handed out after a full login.
|
||||||
|
* Browsers carry it in an HttpOnly cookie; the desktop app, which talks to
|
||||||
|
* the server cross-origin without cookies, sends it back as a header.
|
||||||
|
*/
|
||||||
|
function readTrustToken(req: Request): string | null {
|
||||||
|
const cookies = (req as Request & { cookies?: Record<string, string> })
|
||||||
|
.cookies;
|
||||||
|
const value =
|
||||||
|
cookies?.[TRUST_DEVICE_COOKIE] || req.get("x-termix-trust-token");
|
||||||
|
return typeof value === "string" && TRUST_TOKEN_PATTERN.test(value)
|
||||||
|
? value
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashTrustToken(token: string): string {
|
||||||
|
return crypto.createHash("sha256").update(`trust-v2|${token}`).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
async function isTrustedDevice(req: Request, userId: string): Promise<boolean> {
|
async function isTrustedDevice(req: Request, userId: string): Promise<boolean> {
|
||||||
const deviceInfo = parseUserAgent(req);
|
const token = readTrustToken(req);
|
||||||
const fingerprint = generateDeviceFingerprint(deviceInfo, getDeviceId(req));
|
if (!token) return false;
|
||||||
if (!fingerprint) return false;
|
const fingerprint = hashTrustToken(token);
|
||||||
const trusted = await AuthManager.getInstance().isTrustedDevice(
|
const trusted = await AuthManager.getInstance().isTrustedDevice(
|
||||||
userId,
|
userId,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -151,6 +171,27 @@ async function isTrustedDevice(req: Request, userId: string): Promise<boolean> {
|
|||||||
|
|
||||||
type DeviceType = ReturnType<typeof parseUserAgent>["type"];
|
type DeviceType = ReturnType<typeof parseUserAgent>["type"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An account that only signs in through SSO or LDAP must keep doing so. A
|
||||||
|
* passkey or any other local method would skip the identity provider, so a
|
||||||
|
* user disabled there could still get in.
|
||||||
|
*/
|
||||||
|
export function assertMethodAllowedFor(
|
||||||
|
user: Pick<UserRecord, "isExternal" | "passwordHash">,
|
||||||
|
identity: VerifiedIdentity,
|
||||||
|
methodId: string,
|
||||||
|
): void {
|
||||||
|
if (identity.kind === "external" || !user.isExternal) return;
|
||||||
|
if (user.passwordHash && user.passwordHash.trim() !== "") return;
|
||||||
|
ensureCoreLoginProviders();
|
||||||
|
if (getLoginMethod(methodId)?.external) return;
|
||||||
|
throw new LoginMethodError(
|
||||||
|
"This account signs in through its identity provider",
|
||||||
|
403,
|
||||||
|
"external_login_required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveUser(
|
async function resolveUser(
|
||||||
identity: VerifiedIdentity,
|
identity: VerifiedIdentity,
|
||||||
deviceType: DeviceType,
|
deviceType: DeviceType,
|
||||||
@@ -233,6 +274,7 @@ export async function runLogin(
|
|||||||
): Promise<LoginResult> {
|
): Promise<LoginResult> {
|
||||||
const deviceType = parseUserAgent(req).type;
|
const deviceType = parseUserAgent(req).type;
|
||||||
const user = await resolveUser(identity, deviceType);
|
const user = await resolveUser(identity, deviceType);
|
||||||
|
assertMethodAllowedFor(user, identity, context.methodId);
|
||||||
|
|
||||||
await unlockUser(user, identity, deviceType);
|
await unlockUser(user, identity, deviceType);
|
||||||
if ("password" in identity && identity.password) {
|
if ("password" in identity && identity.password) {
|
||||||
@@ -473,22 +515,29 @@ export async function verifySecondFactorAndRespond(
|
|||||||
? req.body.rememberMe
|
? req.body.rememberMe
|
||||||
: (lookup.pending?.rememberMe ?? false);
|
: (lookup.pending?.rememberMe ?? false);
|
||||||
|
|
||||||
|
let trustToken: string | null = null;
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
const deviceInfo = parseUserAgent(req);
|
const deviceInfo = parseUserAgent(req);
|
||||||
const fingerprint = generateDeviceFingerprint(deviceInfo, getDeviceId(req));
|
trustToken = crypto.randomBytes(32).toString("hex");
|
||||||
if (fingerprint) {
|
await AuthManager.getInstance().addTrustedDevice(
|
||||||
await AuthManager.getInstance().addTrustedDevice(
|
user.id,
|
||||||
user.id,
|
hashTrustToken(trustToken),
|
||||||
fingerprint,
|
deviceInfo.type,
|
||||||
deviceInfo.type,
|
deviceInfo.deviceInfo,
|
||||||
deviceInfo.deviceInfo,
|
);
|
||||||
);
|
res.cookie(
|
||||||
authLogger.info("Device automatically trusted via Remember Me", {
|
TRUST_DEVICE_COOKIE,
|
||||||
operation: "totp_auto_trust",
|
trustToken,
|
||||||
userId: user.id,
|
AuthManager.getInstance().getSecureCookieOptions(
|
||||||
deviceType: deviceInfo.type,
|
req,
|
||||||
});
|
TRUST_TOKEN_MAX_AGE_MS,
|
||||||
}
|
),
|
||||||
|
);
|
||||||
|
authLogger.info("Device automatically trusted via Remember Me", {
|
||||||
|
operation: "totp_auto_trust",
|
||||||
|
userId: user.id,
|
||||||
|
deviceType: deviceInfo.type,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = lookup.pending;
|
const pending = lookup.pending;
|
||||||
@@ -498,6 +547,10 @@ export async function verifySecondFactorAndRespond(
|
|||||||
externalSession: pending?.externalSession ?? null,
|
externalSession: pending?.externalSession ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (trustToken && isNativeAppRequest(req)) {
|
||||||
|
session.body.trustToken = trustToken;
|
||||||
|
}
|
||||||
|
|
||||||
consumePendingLogin(lookup.token);
|
consumePendingLogin(lookup.token);
|
||||||
res.clearCookie(
|
res.clearCookie(
|
||||||
PENDING_LOGIN_COOKIE,
|
PENDING_LOGIN_COOKIE,
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ import {
|
|||||||
} from "./repositories/factory.js";
|
} from "./repositories/factory.js";
|
||||||
import { withCurrentSqliteForeignKeysDisabled } from "./repositories/sqlite-foreign-keys.js";
|
import { withCurrentSqliteForeignKeysDisabled } from "./repositories/sqlite-foreign-keys.js";
|
||||||
import { applyPluginHostImportSettings } from "./routes/host-plugin-settings.js";
|
import { applyPluginHostImportSettings } from "./routes/host-plugin-settings.js";
|
||||||
|
import {
|
||||||
|
selectExportableSettings,
|
||||||
|
type SettingData,
|
||||||
|
} from "../utils/export-settings.js";
|
||||||
import {
|
import {
|
||||||
keepUsableProtocolCredentials,
|
keepUsableProtocolCredentials,
|
||||||
listProtocolLogins,
|
listProtocolLogins,
|
||||||
@@ -107,19 +111,9 @@ const requireAdmin = authManager.createAdminMiddleware();
|
|||||||
app.use(createCompressionMiddleware());
|
app.use(createCompressionMiddleware());
|
||||||
app.use(createCorsMiddleware());
|
app.use(createCorsMiddleware());
|
||||||
|
|
||||||
type SettingData = {
|
async function getExportableSettings(isAdmin: boolean): Promise<SettingData[]> {
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function shouldExportSetting(key: string): boolean {
|
|
||||||
return !key.startsWith("reset_code_") && !key.startsWith("temp_reset_token_");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getExportableSettings(): Promise<SettingData[]> {
|
|
||||||
const settingsRows = await createCurrentSettingsRepository().listAll();
|
const settingsRows = await createCurrentSettingsRepository().listAll();
|
||||||
|
return selectExportableSettings(settingsRows, isAdmin);
|
||||||
return settingsRows.filter((setting) => shouldExportSetting(setting.key));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeSettingsToExportDatabase(
|
function writeSettingsToExportDatabase(
|
||||||
@@ -274,7 +268,7 @@ app.get("/.well-known/acme-challenge/:token", acmeChallengeHandler);
|
|||||||
* localVersion:
|
* localVersion:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Version of the running instance. Use this field for client compatibility checks.
|
* description: Version of the running instance. Use this field for client compatibility checks.
|
||||||
* example: 2.7.1
|
* example: 26.10.0
|
||||||
* status:
|
* status:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Update comparison result, update_check_disabled when explicitly disabled, or unknown when the remote lookup fails or the release tag cannot be parsed.
|
* description: Update comparison result, update_check_disabled when explicitly disabled, or unknown when the remote lookup fails or the release tag cannot be parsed.
|
||||||
@@ -286,12 +280,12 @@ app.get("/.well-known/acme-challenge/:token", acmeChallengeHandler);
|
|||||||
* remoteVersion:
|
* remoteVersion:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Newest release version on the channel. Present only when the update check succeeds.
|
* description: Newest release version on the channel. Present only when the update check succeeds.
|
||||||
* example: 2.8.0
|
* example: 26.10.0
|
||||||
* version:
|
* version:
|
||||||
* type: string
|
* type: string
|
||||||
* deprecated: true
|
* deprecated: true
|
||||||
* description: Legacy alias of remoteVersion, not the running instance's version. Present only when the update check succeeds. Use localVersion for the instance or remoteVersion for the latest release.
|
* description: Legacy alias of remoteVersion, not the running instance's version. Present only when the update check succeeds. Use localVersion for the instance or remoteVersion for the latest release.
|
||||||
* example: 2.8.0
|
* example: 26.10.0
|
||||||
* latest_release:
|
* latest_release:
|
||||||
* type: object
|
* type: object
|
||||||
* description: GitHub release metadata. Present only when the update check succeeds.
|
* description: GitHub release metadata. Present only when the update check succeeds.
|
||||||
@@ -1047,7 +1041,10 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSettingsToExportDatabase(exportDb, await getExportableSettings());
|
writeSettingsToExportDatabase(
|
||||||
|
exportDb,
|
||||||
|
await getExportableSettings(!!user.isAdmin),
|
||||||
|
);
|
||||||
await writeHostDefaultsToExport(exportDb, userId);
|
await writeHostDefaultsToExport(exportDb, userId);
|
||||||
} finally {
|
} finally {
|
||||||
exportDb.close();
|
exportDb.close();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function sslOption(
|
|||||||
/**
|
/**
|
||||||
* Opens a connection to a client-server engine.
|
* Opens a connection to a client-server engine.
|
||||||
*
|
*
|
||||||
* SQLite is not handled here — it has its own lifecycle in db/index.ts, where
|
* SQLite is not handled here, it has its own lifecycle in db/index.ts, where
|
||||||
* the database is decrypted into memory and serialised back to a file. This
|
* the database is decrypted into memory and serialised back to a file. This
|
||||||
* covers the engines that connect to something already running.
|
* covers the engines that connect to something already running.
|
||||||
*
|
*
|
||||||
@@ -109,7 +109,7 @@ export async function connectRemoteDatabase(
|
|||||||
|
|
||||||
// No `schema` option: it only feeds drizzle's relational query API
|
// No `schema` option: it only feeds drizzle's relational query API
|
||||||
// (`db.query.*`), which nothing here uses. The query builder takes its table
|
// (`db.query.*`), which nothing here uses. The query builder takes its table
|
||||||
// names and value encoders from the table objects the repositories import —
|
// names and value encoders from the table objects the repositories import,
|
||||||
// see the note in schema.pg.ts on why the generated schemas are DDL-only.
|
// see the note in schema.pg.ts on why the generated schemas are DDL-only.
|
||||||
if (dialect === "postgres") {
|
if (dialect === "postgres") {
|
||||||
const { drizzle } = await import("drizzle-orm/node-postgres");
|
const { drizzle } = await import("drizzle-orm/node-postgres");
|
||||||
@@ -119,7 +119,7 @@ export async function connectRemoteDatabase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mysql2 names the pool limit differently and wants no `ssl` key at all when
|
// mysql2 names the pool limit differently and wants no `ssl` key at all when
|
||||||
// TLS is off — passing false is not the same as omitting it.
|
// TLS is off, passing false is not the same as omitting it.
|
||||||
const { drizzle } = await import("drizzle-orm/mysql2");
|
const { drizzle } = await import("drizzle-orm/mysql2");
|
||||||
return drizzle({
|
return drizzle({
|
||||||
connection: {
|
connection: {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function resolveDatabaseDialect(
|
|||||||
*
|
*
|
||||||
* SQLite here is an in-memory database serialised back to an encrypted file, so
|
* SQLite here is an in-memory database serialised back to an encrypted file, so
|
||||||
* every write needs a trigger to flush it. Client-server engines have already
|
* every write needs a trigger to flush it. Client-server engines have already
|
||||||
* durably committed by the time the query returns — there is no file to write
|
* durably committed by the time the query returns, there is no file to write
|
||||||
* and nothing to schedule.
|
* and nothing to schedule.
|
||||||
*/
|
*/
|
||||||
export function needsExplicitPersist(dialect: DatabaseDialect): boolean {
|
export function needsExplicitPersist(dialect: DatabaseDialect): boolean {
|
||||||
|
|||||||
@@ -242,8 +242,7 @@ async function initializeCompleteDatabase(): Promise<void> {
|
|||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||||
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
donation_modal_dismissed INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
@@ -970,9 +969,7 @@ const migrateSchema = () => {
|
|||||||
// CURRENT_TIMESTAMP, so the column is added empty and backfilled below.
|
// CURRENT_TIMESTAMP, so the column is added empty and backfilled below.
|
||||||
addColumnIfNotExists("users", "registered_at", "TEXT");
|
addColumnIfNotExists("users", "registered_at", "TEXT");
|
||||||
if (!hadRegisteredAtColumn) {
|
if (!hadRegisteredAtColumn) {
|
||||||
// Pre-existing users are backdated past the 30 day mark so they see the
|
// Pre-existing users get a date in the past, since their real one is unknown.
|
||||||
// donation modal immediately on upgrade instead of waiting a fresh
|
|
||||||
// 30 days as if they had just registered.
|
|
||||||
try {
|
try {
|
||||||
sqlite.exec(
|
sqlite.exec(
|
||||||
`UPDATE users SET registered_at = datetime('now', '-31 days') WHERE registered_at IS NULL`,
|
`UPDATE users SET registered_at = datetime('now', '-31 days') WHERE registered_at IS NULL`,
|
||||||
@@ -1000,12 +997,6 @@ const migrateSchema = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addColumnIfNotExists(
|
|
||||||
"users",
|
|
||||||
"donation_modal_dismissed",
|
|
||||||
"INTEGER NOT NULL DEFAULT 0",
|
|
||||||
);
|
|
||||||
|
|
||||||
addColumnIfNotExists("sessions", "external_session_ref", "TEXT");
|
addColumnIfNotExists("sessions", "external_session_ref", "TEXT");
|
||||||
|
|
||||||
addColumnIfNotExists("ssh_data", "name", "TEXT");
|
addColumnIfNotExists("ssh_data", "name", "TEXT");
|
||||||
@@ -1103,7 +1094,7 @@ const migrateSchema = () => {
|
|||||||
// Derive the replacement table from the live definition instead of
|
// Derive the replacement table from the live definition instead of
|
||||||
// restating it here. The table keeps gaining columns (cert_public_key,
|
// restating it here. The table keeps gaining columns (cert_public_key,
|
||||||
// pin, sort_order, sync_id, ...), and a second copy of the column list
|
// pin, sort_order, sync_id, ...), and a second copy of the column list
|
||||||
// falls behind every time one is added — leaving the copy narrower than
|
// falls behind every time one is added, leaving the copy narrower than
|
||||||
// the table, so the INSERT below fails and the constraint stays put.
|
// the table, so the INSERT below fails and the constraint stays put.
|
||||||
const createSql = sqlite
|
const createSql = sqlite
|
||||||
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'ssh_credentials'")
|
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'ssh_credentials'")
|
||||||
@@ -2108,7 +2099,7 @@ export function getSqlite(): Database.Database {
|
|||||||
if (dialect !== "sqlite") {
|
if (dialect !== "sqlite") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No SQLite handle: DATABASE_DIALECT is "${dialect}". This caller needs a ` +
|
`No SQLite handle: DATABASE_DIALECT is "${dialect}". This caller needs a ` +
|
||||||
`synchronous query, which only SQLite offers — give it an async path instead.`,
|
`synchronous query, which only SQLite offers. Give it an async path instead.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function migrationsFolder(
|
|||||||
* Brings a remote database up to the current schema.
|
* Brings a remote database up to the current schema.
|
||||||
*
|
*
|
||||||
* drizzle's migrator records what it has applied in its own table, so this is
|
* drizzle's migrator records what it has applied in its own table, so this is
|
||||||
* safe to run on every start — including against a database another instance
|
* safe to run on every start, including against a database another instance
|
||||||
* already migrated.
|
* already migrated.
|
||||||
*/
|
*/
|
||||||
export async function runRemoteMigrations(
|
export async function runRemoteMigrations(
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const RETIRED_COLUMNS: Record<string, string[]> = {
|
|||||||
"identifier_path",
|
"identifier_path",
|
||||||
"name_path",
|
"name_path",
|
||||||
"scopes",
|
"scopes",
|
||||||
|
"donation_modal_dismissed",
|
||||||
],
|
],
|
||||||
sessions: ["oidc_sub", "oidc_sid", "sso_provider_id"],
|
sessions: ["oidc_sub", "oidc_sid", "sso_provider_id"],
|
||||||
user_preferences: [
|
user_preferences: [
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// GENERATED FILE — do not edit.
|
// GENERATED FILE, do not edit.
|
||||||
//
|
//
|
||||||
// Produced from schema.ts by scripts/generate-dialect-schema.cjs.
|
// Produced from schema.ts by scripts/generate-dialect-schema.cjs.
|
||||||
// Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`.
|
// Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`.
|
||||||
// Target dialect: mysql.
|
// Target dialect: mysql.
|
||||||
//
|
//
|
||||||
// DDL source for drizzle-kit. NOT imported to run queries — repositories use
|
// DDL source for drizzle-kit. NOT imported to run queries. Repositories use
|
||||||
// schema.ts on every dialect. See the generator header for why that is correct.
|
// schema.ts on every dialect. See the generator header for why that is correct.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -27,9 +27,6 @@ export const users = mysqlTable("users", {
|
|||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
isAdmin: boolean("is_admin").notNull().default(false),
|
isAdmin: boolean("is_admin").notNull().default(false),
|
||||||
registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`),
|
registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`),
|
||||||
donationModalDismissed: boolean("donation_modal_dismissed")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = mysqlTable("settings", {
|
export const settings = mysqlTable("settings", {
|
||||||
@@ -542,7 +539,7 @@ export const sharedHostSecrets = mysqlTable(
|
|||||||
},
|
},
|
||||||
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
||||||
// so the generated Postgres and MySQL schemas allowed duplicates the
|
// so the generated Postgres and MySQL schemas allowed duplicates the
|
||||||
// SQLite deployment forbids — and the upsert had nothing to conflict on.
|
// SQLite deployment forbids, and the upsert had nothing to conflict on.
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex("idx_shared_host_secrets_scope").on(
|
uniqueIndex("idx_shared_host_secrets_scope").on(
|
||||||
table.hostAccessId,
|
table.hostAccessId,
|
||||||
@@ -592,7 +589,7 @@ export const userRoles = mysqlTable(
|
|||||||
},
|
},
|
||||||
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
||||||
// so the generated Postgres and MySQL schemas allowed duplicates the
|
// so the generated Postgres and MySQL schemas allowed duplicates the
|
||||||
// SQLite deployment forbids — and the upsert had nothing to conflict on.
|
// SQLite deployment forbids, and the upsert had nothing to conflict on.
|
||||||
//
|
//
|
||||||
// The unique pair already serves lookups by user, since user_id leads it.
|
// The unique pair already serves lookups by user, since user_id leads it.
|
||||||
// Listing a role's members starts from role_id, which it cannot serve.
|
// Listing a role's members starts from role_id, which it cannot serve.
|
||||||
@@ -1060,7 +1057,7 @@ export const pluginRegistries = mysqlTable("plugin_registries", {
|
|||||||
/**
|
/**
|
||||||
* Install counts populated by a background job (GitHub release download
|
* Install counts populated by a background job (GitHub release download
|
||||||
* counts, aggregated telemetry, or a manual override) rather than by the
|
* counts, aggregated telemetry, or a manual override) rather than by the
|
||||||
* install/uninstall actions themselves — kept separate from `plugins` so
|
* install/uninstall actions themselves, kept separate from `plugins` so
|
||||||
* that job can overwrite counts without touching install state.
|
* that job can overwrite counts without touching install state.
|
||||||
*/
|
*/
|
||||||
export const pluginInstallCounts = mysqlTable(
|
export const pluginInstallCounts = mysqlTable(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// GENERATED FILE — do not edit.
|
// GENERATED FILE, do not edit.
|
||||||
//
|
//
|
||||||
// Produced from schema.ts by scripts/generate-dialect-schema.cjs.
|
// Produced from schema.ts by scripts/generate-dialect-schema.cjs.
|
||||||
// Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`.
|
// Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`.
|
||||||
// Target dialect: postgres.
|
// Target dialect: postgres.
|
||||||
//
|
//
|
||||||
// DDL source for drizzle-kit. NOT imported to run queries — repositories use
|
// DDL source for drizzle-kit. NOT imported to run queries. Repositories use
|
||||||
// schema.ts on every dialect. See the generator header for why that is correct.
|
// schema.ts on every dialect. See the generator header for why that is correct.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -27,9 +27,6 @@ export const users = pgTable("users", {
|
|||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
isAdmin: boolean("is_admin").notNull().default(false),
|
isAdmin: boolean("is_admin").notNull().default(false),
|
||||||
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
donationModalDismissed: boolean("donation_modal_dismissed")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = pgTable("settings", {
|
export const settings = pgTable("settings", {
|
||||||
@@ -542,7 +539,7 @@ export const sharedHostSecrets = pgTable(
|
|||||||
},
|
},
|
||||||
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
||||||
// so the generated Postgres and MySQL schemas allowed duplicates the
|
// so the generated Postgres and MySQL schemas allowed duplicates the
|
||||||
// SQLite deployment forbids — and the upsert had nothing to conflict on.
|
// SQLite deployment forbids, and the upsert had nothing to conflict on.
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex("idx_shared_host_secrets_scope").on(
|
uniqueIndex("idx_shared_host_secrets_scope").on(
|
||||||
table.hostAccessId,
|
table.hostAccessId,
|
||||||
@@ -592,7 +589,7 @@ export const userRoles = pgTable(
|
|||||||
},
|
},
|
||||||
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
||||||
// so the generated Postgres and MySQL schemas allowed duplicates the
|
// so the generated Postgres and MySQL schemas allowed duplicates the
|
||||||
// SQLite deployment forbids — and the upsert had nothing to conflict on.
|
// SQLite deployment forbids, and the upsert had nothing to conflict on.
|
||||||
//
|
//
|
||||||
// The unique pair already serves lookups by user, since user_id leads it.
|
// The unique pair already serves lookups by user, since user_id leads it.
|
||||||
// Listing a role's members starts from role_id, which it cannot serve.
|
// Listing a role's members starts from role_id, which it cannot serve.
|
||||||
@@ -1060,7 +1057,7 @@ export const pluginRegistries = pgTable("plugin_registries", {
|
|||||||
/**
|
/**
|
||||||
* Install counts populated by a background job (GitHub release download
|
* Install counts populated by a background job (GitHub release download
|
||||||
* counts, aggregated telemetry, or a manual override) rather than by the
|
* counts, aggregated telemetry, or a manual override) rather than by the
|
||||||
* install/uninstall actions themselves — kept separate from `plugins` so
|
* install/uninstall actions themselves, kept separate from `plugins` so
|
||||||
* that job can overwrite counts without touching install state.
|
* that job can overwrite counts without touching install state.
|
||||||
*/
|
*/
|
||||||
export const pluginInstallCounts = pgTable(
|
export const pluginInstallCounts = pgTable(
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ export const users = sqliteTable("users", {
|
|||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
|
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
|
||||||
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
donationModalDismissed: integer("donation_modal_dismissed", {
|
|
||||||
mode: "boolean",
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = sqliteTable("settings", {
|
export const settings = sqliteTable("settings", {
|
||||||
@@ -534,7 +529,7 @@ export const sharedHostSecrets = sqliteTable(
|
|||||||
},
|
},
|
||||||
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
||||||
// so the generated Postgres and MySQL schemas allowed duplicates the
|
// so the generated Postgres and MySQL schemas allowed duplicates the
|
||||||
// SQLite deployment forbids — and the upsert had nothing to conflict on.
|
// SQLite deployment forbids, and the upsert had nothing to conflict on.
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex("idx_shared_host_secrets_scope").on(
|
uniqueIndex("idx_shared_host_secrets_scope").on(
|
||||||
table.hostAccessId,
|
table.hostAccessId,
|
||||||
@@ -584,7 +579,7 @@ export const userRoles = sqliteTable(
|
|||||||
},
|
},
|
||||||
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
// Declared inline in the production DDL as UNIQUE(...), but never here,
|
||||||
// so the generated Postgres and MySQL schemas allowed duplicates the
|
// so the generated Postgres and MySQL schemas allowed duplicates the
|
||||||
// SQLite deployment forbids — and the upsert had nothing to conflict on.
|
// SQLite deployment forbids, and the upsert had nothing to conflict on.
|
||||||
//
|
//
|
||||||
// The unique pair already serves lookups by user, since user_id leads it.
|
// The unique pair already serves lookups by user, since user_id leads it.
|
||||||
// Listing a role's members starts from role_id, which it cannot serve.
|
// Listing a role's members starts from role_id, which it cannot serve.
|
||||||
@@ -1058,7 +1053,7 @@ export const pluginRegistries = sqliteTable("plugin_registries", {
|
|||||||
/**
|
/**
|
||||||
* Install counts populated by a background job (GitHub release download
|
* Install counts populated by a background job (GitHub release download
|
||||||
* counts, aggregated telemetry, or a manual override) rather than by the
|
* counts, aggregated telemetry, or a manual override) rather than by the
|
||||||
* install/uninstall actions themselves — kept separate from `plugins` so
|
* install/uninstall actions themselves, kept separate from `plugins` so
|
||||||
* that job can overwrite counts without touching install state.
|
* that job can overwrite counts without touching install state.
|
||||||
*/
|
*/
|
||||||
export const pluginInstallCounts = sqliteTable(
|
export const pluginInstallCounts = sqliteTable(
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export class AuditLogRepository {
|
|||||||
/**
|
/**
|
||||||
* Cached row count backing the cap check. Static because the factory builds
|
* Cached row count backing the cap check. Static because the factory builds
|
||||||
* a repository per call, so a per-instance count would never survive to be
|
* a repository per call, so a per-instance count would never survive to be
|
||||||
* reused. Null means "unknown, re-read" — which is also how any path that
|
* reused. Null means "unknown, re-read", which is also how any path that
|
||||||
* deletes rows invalidates it.
|
* deletes rows invalidates it.
|
||||||
*/
|
*/
|
||||||
private static cachedCount: number | null = null;
|
private static cachedCount: number | null = null;
|
||||||
@@ -233,13 +233,13 @@ export class AuditLogRepository {
|
|||||||
*
|
*
|
||||||
* Both passes used to run inline on every insert: a retention DELETE plus a
|
* Both passes used to run inline on every insert: a retention DELETE plus a
|
||||||
* COUNT over the whole table, thousands of times an hour on a busy install,
|
* COUNT over the whole table, thousands of times an hour on a busy install,
|
||||||
* almost always to find nothing to do — and audit writes sit in the request
|
* almost always to find nothing to do, and audit writes sit in the request
|
||||||
* path of the actions they record.
|
* path of the actions they record.
|
||||||
*
|
*
|
||||||
* They are split by what they cost and what they guarantee. Retention is
|
* They are split by what they cost and what they guarantee. Retention is
|
||||||
* time-based, so nothing is lost by checking it on an interval. The row cap
|
* time-based, so nothing is lost by checking it on an interval. The row cap
|
||||||
* is a disk-space guard that has to react to inserts, so it is still checked
|
* is a disk-space guard that has to react to inserts, so it is still checked
|
||||||
* on the write that crosses it — but against a cached count, so the common
|
* on the write that crosses it, but against a cached count, so the common
|
||||||
* case is an integer compare rather than a COUNT.
|
* case is an integer compare rather than a COUNT.
|
||||||
*/
|
*/
|
||||||
private async pruneIfDue(): Promise<void> {
|
private async pruneIfDue(): Promise<void> {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
|||||||
import type * as schema from "../db/schema.js";
|
import type * as schema from "../db/schema.js";
|
||||||
|
|
||||||
// Re-exported so repositories can keep importing it from here, but defined in
|
// Re-exported so repositories can keep importing it from here, but defined in
|
||||||
// db/dialect.ts — a local copy that said "sqlite" survived here for a while and
|
// db/dialect.ts, a local copy that said "sqlite" survived here for a while and
|
||||||
// typed every context as SQLite-only while the runtime already carried all
|
// typed every context as SQLite-only while the runtime already carried all
|
||||||
// three, which silently made the dialect branches unreachable to the checker.
|
// three, which silently made the dialect branches unreachable to the checker.
|
||||||
export type { DatabaseDialect } from "../db/dialect.js";
|
export type { DatabaseDialect } from "../db/dialect.js";
|
||||||
@@ -18,7 +18,7 @@ import type { DatabaseDialect } from "../db/dialect.js";
|
|||||||
*
|
*
|
||||||
* This is a deliberate approximation, not an accident. The query-builder
|
* This is a deliberate approximation, not an accident. The query-builder
|
||||||
* surface the repositories actually use is the same on all three engines, and
|
* surface the repositories actually use is the same on all three engines, and
|
||||||
* that equivalence is asserted in multi-dialect.test.ts rather than assumed —
|
* that equivalence is asserted in multi-dialect.test.ts rather than assumed,
|
||||||
* identifier quoting, placeholder style and value coercion are all covered
|
* identifier quoting, placeholder style and value coercion are all covered
|
||||||
* there. At runtime this may hold a Postgres or MySQL instance.
|
* there. At runtime this may hold a Postgres or MySQL instance.
|
||||||
*
|
*
|
||||||
@@ -32,7 +32,7 @@ export type PortableDatabase = BetterSQLite3Database<typeof schema>;
|
|||||||
*
|
*
|
||||||
* Deliberately drizzle-only: with no raw driver handle here, no repository can
|
* Deliberately drizzle-only: with no raw driver handle here, no repository can
|
||||||
* reach for engine-specific SQL. Retention queries that previously needed
|
* reach for engine-specific SQL. Retention queries that previously needed
|
||||||
* `datetime('now', ?)` compute their cutoff in JS instead — see
|
* `datetime('now', ?)` compute their cutoff in JS instead, see
|
||||||
* ./sql-timestamp.ts.
|
* ./sql-timestamp.ts.
|
||||||
*/
|
*/
|
||||||
export interface DatabaseContext {
|
export interface DatabaseContext {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import { UserRepository } from "./user-repository.js";
|
|||||||
* write.
|
* write.
|
||||||
*
|
*
|
||||||
* Both cross-dialect harnesses build a DatabaseContext themselves, so neither
|
* Both cross-dialect harnesses build a DatabaseContext themselves, so neither
|
||||||
* exercises this function — see tests/database/repositories/factory-context.
|
* exercises this function, see tests/database/repositories/factory-context.
|
||||||
*/
|
*/
|
||||||
export function createCurrentRepositoryContext(): DatabaseContext {
|
export function createCurrentRepositoryContext(): DatabaseContext {
|
||||||
return {
|
return {
|
||||||
@@ -64,7 +64,7 @@ export function createCurrentRepositoryContext(): DatabaseContext {
|
|||||||
* Only meaningful for SQLite, where the database lives in memory and has to be
|
* Only meaningful for SQLite, where the database lives in memory and has to be
|
||||||
* serialised back to its encrypted file. On Postgres and MySQL the write is
|
* serialised back to its encrypted file. On Postgres and MySQL the write is
|
||||||
* already durable, so no hook is installed at all rather than one that does
|
* already durable, so no hook is installed at all rather than one that does
|
||||||
* nothing — repositories call it as `this.onWrite?.()`.
|
* nothing, repositories call it as `this.onWrite?.()`.
|
||||||
*/
|
*/
|
||||||
export function createCurrentRepositoryWriteHook(
|
export function createCurrentRepositoryWriteHook(
|
||||||
reason: string,
|
reason: string,
|
||||||
@@ -91,7 +91,7 @@ export function createCurrentRepositoryLazyWriteHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw driver handle for the few synchronous call sites that cannot await —
|
* Raw driver handle for the few synchronous call sites that cannot await,
|
||||||
* getCurrentSettingValue below, and settings reads during startup. Repositories
|
* getCurrentSettingValue below, and settings reads during startup. Repositories
|
||||||
* must not use this: they take a DatabaseContext, which is drizzle-only.
|
* must not use this: they take a DatabaseContext, which is drizzle-only.
|
||||||
* Porting to another engine means giving these callers an async path first.
|
* Porting to another engine means giving these callers an async path first.
|
||||||
@@ -403,8 +403,8 @@ let refreshTimer: NodeJS.Timeout | null = null;
|
|||||||
*
|
*
|
||||||
* The cache is per-process and updated in the process that writes. That is
|
* The cache is per-process and updated in the process that writes. That is
|
||||||
* enough for SQLite, where there is only ever one process. On Postgres and
|
* enough for SQLite, where there is only ever one process. On Postgres and
|
||||||
* MySQL — which exist here precisely so more than one instance can share the
|
* MySQL, which exist here precisely so more than one instance can share the
|
||||||
* data — a setting changed on one replica would otherwise never reach the
|
* data, a setting changed on one replica would otherwise never reach the
|
||||||
* others, because the synchronous read has no way to go back to the database.
|
* others, because the synchronous read has no way to go back to the database.
|
||||||
*
|
*
|
||||||
* Periodic re-priming does not make the value immediately consistent. It bounds
|
* Periodic re-priming does not make the value immediately consistent. It bounds
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class HostFolderRepository {
|
|||||||
now = new Date().toISOString(),
|
now = new Date().toISOString(),
|
||||||
): Promise<RenameFolderResult> {
|
): Promise<RenameFolderResult> {
|
||||||
// CAST target: every engine spells the text type differently enough to
|
// CAST target: every engine spells the text type differently enough to
|
||||||
// matter here — MySQL has no `text` cast and wants `char`.
|
// matter here, MySQL has no `text` cast and wants `char`.
|
||||||
const textType = this.context.dialect === "mysql" ? "char" : "text";
|
const textType = this.context.dialect === "mysql" ? "char" : "text";
|
||||||
const oldPrefix = `${oldName} / `;
|
const oldPrefix = `${oldName} / `;
|
||||||
const newPrefix = `${newName} / `;
|
const newPrefix = `${newName} / `;
|
||||||
|
|||||||
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user