mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-13 07:03:20 +08:00
Harden public package installation and release validation
This commit is contained in:
parent
9271494b37
commit
9b2166e5b6
41 changed files with 994 additions and 363 deletions
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.json text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
43
.github/workflows/release-package.yml
vendored
Normal file
43
.github/workflows/release-package.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: Release package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
package:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: '11.7.0'
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: '1.3.13'
|
||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
# Check committed artifacts before rebuilding, so an omitted build fails CI.
|
||||
- run: bun run release:check
|
||||
- run: bun run bundle
|
||||
- run: bun run release:check
|
||||
- run: bun run test:release
|
||||
- run: bun run release:pack
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: netxops-${{ matrix.os }}
|
||||
path: |
|
||||
*.tgz
|
||||
*.tgz.sha256
|
||||
if-no-files-found: error
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -20,4 +20,5 @@ __pycache__/
|
|||
*.pyc
|
||||
.dsh/
|
||||
*.tgz
|
||||
*.tgz.sha256
|
||||
.pnpm-store/
|
||||
|
|
|
|||
243
.release-build.json
Normal file
243
.release-build.json
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
{
|
||||
"format": 1,
|
||||
"builds": {
|
||||
"lib/agent-tools-biz-monitor.js": {
|
||||
"output": "2e870f4d26b8b9560ad4ebced0e8c6b02bc265bd677acae0e85e7f98de1e22ff",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-biz-monitor.ts": "201bd447198d5ee7cb1aa25031a3f6054fe12a8a221edd4915752696cca1537d",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-common.js": {
|
||||
"output": "87742324e6d39db0e42a2a28866a97835aacfceb24d6105ff7a76bc9ec00af2d",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-common.ts": "cf463e940bc2042d7462502de93d45380550da54f1965fa684d72f71b16bc41b",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-nms.js": {
|
||||
"output": "5433fcebca463c2d3bde07f5cc948f572706153c41797e0d805c2a6c9fbff9a0",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-nms.ts": "6505e51bfb2948756440b00724b19d907d410d34ef1dcbf08019774c0f9d7ef5",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-ops.js": {
|
||||
"output": "9945eb9b4e400b5cab9de885f75d29cc43da53f7e129071b22d019c10ed8e698",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-ops.ts": "2f0331106de53f3dcdd8d61ce2d9da363f0a9b854f1dd7d98e27997c82d98a77",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-topology.js": {
|
||||
"output": "44f4fba8d174bdcfc6b55a38d1fb1e32828f0ac8c3c87a14ff02a43693ca46ef",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-topology.ts": "22bf5f1c71a5b1d33bd3a894bca2c6e4c2fc72072c8a9d7bbc00e214dfafa9f8",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools.js": {
|
||||
"output": "a1978da94dae7fc721c386a147a4ddbab9ec1a055ab86ccedc3565e0f96bced5",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools.ts": "b7e4f8476ba9c5f2754d859074c94129642d18883da293b80e310c09336bc5c5",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f"
|
||||
}
|
||||
},
|
||||
"lib/client.js": {
|
||||
"output": "cd0b85687902aae42158838e000f7e9a6bef488abbcbf6c7885d0a87c40e00f8",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/client/NetxopsCard.tsx": "2428aba7db1a92873a890db2ea494701317b254d2eaaff509dce7d83ac7c9509",
|
||||
"src/client/alarm-push-status-view.ts": "9ad84e4a691a6c22de4d85c45047251716929c63cada362205db98428a810a31",
|
||||
"src/client/card-form.ts": "00ec10c453fffae8beac4a35a13299514c120c913100a6e4536e7a00d072b8d9",
|
||||
"src/client/controller.ts": "87a77c1c139cc9d9ea501f5f77a824eebdeac786d64ed64b5de08ded9d7f3c99",
|
||||
"src/client/im-delivery-catalog.ts": "faf18d4a8fdcf30033b6d905f156c6fc953d214f8d734417f3bed783994d6117",
|
||||
"src/client/index.ts": "740dc42992e5930f50125fa4eba01bdbd70cd10b5121f2e6eaaad479dcb6bf06",
|
||||
"src/client/kb-status-view.ts": "458d1dfd9c0f93e6cdd06780c0ad813de54c3aa952fcdb63758a616ab43ffb10",
|
||||
"src/client/locales.ts": "491ebc35a5daa5335f0f804fc320b11203a2c5e9d21f93a40ff57f3623f0fe4a",
|
||||
"src/client/sessions-export-view.ts": "3397b081318c23d9d816596566e19101df7234390a698a07481c7c1a019a66fd",
|
||||
"src/client/settings-form.ts": "0318749c9afd049982a60eee7b2ca4a0a2cdcd4f574f9c87616f2ccbe39779eb",
|
||||
"src/client/snapshot-store.ts": "25fdcae95490a9d7f1f520ec90981cefb4fdd576b340717e9087f1ca82c6563d",
|
||||
"src/client/styles.ts": "bef5bd6a3b2f0213addfd309021029f669a65811fe366ed38b6d26e39cfb63b1",
|
||||
"src/client/tools-status-view.ts": "4a1a4f9eb026a67d43cc02074a3ee915df76f8a56a188e16af9deb819f9ba166",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"
|
||||
}
|
||||
},
|
||||
"lib/index.js": {
|
||||
"output": "514890b88808556213c851071faf0c73a4e5761a5eac73c9a2bd3376a08fb304",
|
||||
"inputs": {
|
||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc",
|
||||
"src/index.ts": "a7c4a08590af7cfe2a9fa671fa5518e9b6de13d96f5360349e4d8a6ed2cb0517",
|
||||
"src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62",
|
||||
"src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf",
|
||||
"src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f",
|
||||
"src/netx/alarm-push.ts": "96f3fca6a7fd50024a1d233ff43984fc0ff77261ce873b69396eb7ed5344f473",
|
||||
"src/netx/alarm-session.ts": "0df97efc3debb262573c19ade84d9d50584d58b21fa2c5ae5badd382915f18e3",
|
||||
"src/netx/capability-groups.ts": "d916fd36bc75e081f567e0545097feb6f113b4c7365b620c1dd8174bb08caeef",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "eb5a8b2bdbbc0c1cf3b2ece7f22e1a7a10c41a5a23be737d0c121cc5e5315b14",
|
||||
"src/netx/group-tools-plugin.ts": "7a5daaead0d1f228cb35d8eba56c8efd8a529e31e0e44463cef2904ceb64397b",
|
||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||
"src/netx/heal-profile-entry.ts": "03b80f6d2e78e4b62f8fd53649f875c94afb6e66fad65197572947279bfca186",
|
||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||
"src/netx/i18n.ts": "9278e4adaa96cae413326f4dd8c796f2712d4749bcf92e5170967e38cee2d082",
|
||||
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
||||
"src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e",
|
||||
"src/netx/netxops-web-rpc.ts": "c6981c91b0a300806d44f45708f07c5102ec40655e5d387394608e5472ad3214",
|
||||
"src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967",
|
||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||
"src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||
"src/netx/topology-handlers.ts": "2735c22d8773b5a7b1f8e181a8bbd350fff580a41018cbddcdb7bc239fded25f",
|
||||
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
README.md
71
README.md
|
|
@ -1,64 +1,51 @@
|
|||
# netxops — Netx Ops for DeepSeek Harness
|
||||
# Netx Ops for DeepSeek Harness
|
||||
|
||||
Public **DeepSeek Harness** package: native `netx__*` REST tools + Plugins card + **agent preset (skills included)**.
|
||||
Network operations, topology and business monitoring tools for DeepSeek Harness Desktop and Web. Publish tools and their skills to Netx Ops, selected presets, or all presets; revoke them at any time.
|
||||
|
||||
- GitHub: https://github.com/hansjone/netxops
|
||||
- Package: `dsh-netxops`
|
||||
- Brand: **Netx Ops**
|
||||
## Install
|
||||
|
||||
## Install (one command)
|
||||
In Harness **Plugins → Add plugin**, enter `github:hansjone/netxops`, or the absolute path to a released `dsh-netxops-<version>.tgz`. A release tarball is recommended for a fixed, tested version and avoids downloading from GitHub during installation.
|
||||
|
||||
```powershell
|
||||
dsh plugin --profile web add `
|
||||
github:hansjone/netxops `
|
||||
github:hansjone/dsh-im-ops `
|
||||
github:hansjone/dsh-ops-cron
|
||||
For the Web CLI:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add github:hansjone/netxops
|
||||
dsh web
|
||||
```
|
||||
|
||||
IM + cron must be **direct** profile deps (same `add` line). Nesting `github:` under netxops hits `blockExoticSubdeps` / `ERR_PNPM_EXOTIC_SUBDEP`. Details: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
Only Netx Ops is required. IM delivery and scheduling are optional, separate plugins. Users do not need Bun, Python, a local source checkout, or a build step; runtime JS and skills are included. Harness supplies its own internal packages and React.
|
||||
|
||||
Then:
|
||||
See the [installation and troubleshooting guide](docs/INSTALL.md).
|
||||
|
||||
1. Settings → **Plugins** → **Netx Ops** → API URL / token / capability groups
|
||||
2. Optional: enable **关键告警推送** — DSH dials out to netx; choose **Deliver to DSH session** and/or **Deliver to WhatsApp / IM** (paste `botId`+`targetId` from IM 投递设置). The card shows live WSS status.
|
||||
3. Optional: **导出全部会话** — download one ZIP of every durable session on this Host.
|
||||
4. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate)
|
||||
5. New session → choose **Netx Ops**
|
||||
## Configure
|
||||
|
||||
Also need: a reachable **netx API** (no local `pip install netx_mcp`).
|
||||
1. Open **Settings → Plugins → Netx Ops** and enter your Netx API URL and token.
|
||||
2. Choose each capability group's target presets. The default enables operations and business monitoring in Netx Ops only; topology and publication to all presets start disabled.
|
||||
3. After installation or upgrade, restart the relevant Desktop or Web Host once and select **Netx Ops** for a new session.
|
||||
|
||||
## Flexible publication
|
||||
Publication switches and preset selection save automatically. Connection fields, model language, alarm settings and manually entered knowledge-base paths use **Save / Discard**, shown when a draft changes. Choosing a knowledge-base directory through the picker saves that path automatically.
|
||||
|
||||
Settings → Plugins → Netx Ops supports a master publication switch and independent
|
||||
targets for ops, topology, biz-monitor, and knowledge-base capabilities:
|
||||
The master publication switch removes all plugin-managed Netx tools and skills while retaining selections. Subsequent publication changes apply on the next model request without restarting. API access requires a reachable Netx service and token permissions matching the requested tools.
|
||||
|
||||
- Enable in the built-in **Netx Ops** preset.
|
||||
- Select specific other presets from the live Host roster.
|
||||
- Publish to **all presets** through the Host layer.
|
||||
Optional features include alarm delivery to a Harness session or IM, knowledge-base skills and local memory, and downloading this Host's persisted sessions as a ZIP. See [knowledge-base configuration](docs/KNOWLEDGE_BASE.md) and the [tool map](docs/TOOL_MAP.md).
|
||||
|
||||
Tools and corresponding skills follow the same policy. Changes auto-save and apply
|
||||
to current and new sessions on their next model request. Turning the master switch
|
||||
off removes all plugin-managed Netx tools and skills while retaining your selections.
|
||||
After upgrading the plugin, restart each installed Host once; subsequent publication
|
||||
changes do not need a restart.
|
||||
## Development and release
|
||||
|
||||
See the [Chinese diagnosis and validation report](docs/PUBLICATION-REVIEW.zh.md).
|
||||
Development checks: `bun run typecheck`, `bun test`, and `bun run preview:client`.
|
||||
```sh
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
bun run bundle
|
||||
bun run release:check
|
||||
bun run test:release
|
||||
bun run release:pack
|
||||
```
|
||||
|
||||
**Skills:** edit only in the sibling **netx** repo `skills/` tree, then `powershell -File .\scripts\sync-skills-from-netx.ps1` before publish. Linked local installs prefer `../netx/skills` or `NETX_SKILLS_ROOT` at runtime.
|
||||
Development requires Node.js 22, pnpm 11.7 and Bun 1.3.13 or later. Building and package installation tests use only public development dependencies. Full Harness integration tests and type checking additionally need a local Harness runtime; see [maintainer instructions](docs/RELEASING.md).
|
||||
|
||||
## What is coupled
|
||||
Release checks detect stale or missing bundles, undeclared runtime dependencies, invalid metadata and missing skills. Tests install the actual npm tarball offline in an isolated profile. CI repeats the package checks on Windows, macOS and Linux.
|
||||
|
||||
| In the plugin | Outside (data / runtime) |
|
||||
|---------------|---------------------------|
|
||||
| `netx__*` REST tools (groups: ops / topology; one group ↔ one skill; optional host publish + `tools-ops|topology`) + settings + credentials | netx HTTP API (URL + token) |
|
||||
| Optional key-alarm push (WSS client → sticky DSH session) | netx `/v1/integrations/dsh-alarm/ws` hub |
|
||||
| **Export all sessions** (Plugins card → browser ZIP download) | this Host’s session store (JSONL) |
|
||||
| Persona + skills (`netx-ops` / `netx-topology`) | |
|
||||
| Agent preset auto-install to `~/.dsh/.agent-presets` | |
|
||||
Skills are maintained in the sibling `netx/skills` repository and copied into this package before release. Published installations use the bundled skill files; a source checkout can use the sibling repository or an explicit `NETX_SKILLS_ROOT`.
|
||||
|
||||
MCP (`python -m netx_mcp`) remains available for OpenClaw / other hosts — not required for DSH.
|
||||
Implementation and UI validation details: [Chinese review](docs/PUBLICATION-REVIEW.zh.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
# Host-only bundle patch (web-safe).
|
||||
# Desktop Netx Ops preset is composed from standard and synced into
|
||||
# profiles/desktop/cordis.patch.yml at Host apply — see src/agent-preset-install.ts.
|
||||
# Netx Ops is composed from the running Host's standard preset and synced into
|
||||
# that Host profile's cordis.patch.yml at activation.
|
||||
|
||||
# Host-plane Netx Ops bridge: settings + credentials → connection publish.
|
||||
# Ops-scoped `netx__*` tools mount from the agent preset (`dsh-netxops/tools`).
|
||||
# Capability groups: ops in preset (not public by default); topology off.
|
||||
# Rule: one group ↔ one skill (netx-ops / netx-topology).
|
||||
# Capability groups: ops + biz-monitor in Netx Ops by default; topology off.
|
||||
# Tools and corresponding skills share the selected publication targets.
|
||||
#
|
||||
# IM + cron are separate profile bundles (pnpm blockExoticSubdeps forbids
|
||||
# github: packages as transitive deps of this package). Install them in the
|
||||
# same `dsh plugin add` line — see docs/INSTALL.md.
|
||||
# github: packages as transitive deps of this package). Install them only when
|
||||
# their delivery or scheduling features are needed — see docs/INSTALL.md.
|
||||
#
|
||||
# After install, open Settings → Plugins to edit apiUrl / lang / thinking+reply
|
||||
# language / groups (settings card client half; see docs/INSTALL.md).
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Host-plane Netx Ops bridge: settings + credentials → connection publish.
|
||||
# Ops-scoped `netx__*` tools mount from the agent preset (`dsh-netxops/tools`).
|
||||
# Capability groups: ops in preset (not public by default); topology off.
|
||||
# Rule: one group ↔ one skill (netx-ops / netx-topology).
|
||||
# Capability groups: ops + biz-monitor in Netx Ops by default; topology off.
|
||||
# Tools and corresponding skills share the selected publication targets.
|
||||
#
|
||||
# IM + cron are separate profile bundles (pnpm blockExoticSubdeps forbids
|
||||
# github: packages as transitive deps of this package). Install them in the
|
||||
# same `dsh plugin add` line — see docs/INSTALL.md.
|
||||
# github: packages as transitive deps of this package). Install them only when
|
||||
# their delivery or scheduling features are needed — see docs/INSTALL.md.
|
||||
#
|
||||
# After install, open Settings → Plugins to edit apiUrl / lang / thinking+reply
|
||||
# language / groups (settings card client half; see docs/INSTALL.md).
|
||||
|
|
|
|||
170
docs/INSTALL.md
170
docs/INSTALL.md
|
|
@ -1,114 +1,84 @@
|
|||
# Install Netx Ops on DeepSeek Harness
|
||||
# 安装 Netx Ops
|
||||
|
||||
## What `dsh plugin add` gives you
|
||||
## 桌面版
|
||||
|
||||
One install wires **all of**:
|
||||
打开 Harness 的插件管理页面,选择“添加插件”,填入:
|
||||
|
||||
| Piece | How you use it |
|
||||
|-------|----------------|
|
||||
| Host tools | Capability groups **ops / topology / biz-monitor**. Each group targets Netx Ops, selected other presets, or all presets; tools and skills share the policy. A master switch revokes all Netx publication. Defaults: ops+bizMonitor in Netx Ops, all public off. |
|
||||
| Companion plugins | **dsh-im-ops** + **dsh-ops-cron** — add as **direct** profile deps in the same command (DSH/pnpm blocks `github:` as transitive deps) |
|
||||
| Plugins card | Settings → Plugins → **Netx Ops** (URL / API lang / thinking+reply language / token / capability groups) |
|
||||
| Agent preset + skills | Settings → Agent presets → **Netx Ops**. Host composes from **this host's** shipped **standard** + Netx Ops overlays (no preset inheritance). DSH ≥0.2 (Desktop **and** web) uses declarative `@deepseek-ai/dsh-agent-preset` rows synced into that profile's `cordis.patch.yml` (from `@deepseek-ai/dsh-web-app/presets/standard.patch.yml`). A directory under `~/.dsh/.agent-presets/netxops` is still written for older hosts, but 0.2 no longer lists it. Restart Desktop / `dsh web` once after upgrade. Playbooks follow the same group toggles |
|
||||
|
||||
You do **not** run `link-preset.ps1` for normal use. That script is only a manual fallback.
|
||||
|
||||
## Still required outside the npm package
|
||||
|
||||
1. **netx API** reachable (default `http://127.0.0.1:8890`) — the data plane.
|
||||
2. **API token** with scopes matching the tools you use (`alarms:read`, `ne:read`, `ne:exec`, `biz-monitor:read`, `sql:query`, …).
|
||||
|
||||
No local Python / `netx_mcp` install is required for DSH. (OpenClaw and other MCP hosts can still use `python -m netx_mcp` separately for NMS/CLI — **not** for biz-monitor; that is netxops host tools only.)
|
||||
|
||||
## Clone / `pnpm install`(仓库源码)
|
||||
|
||||
`@deepseek-ai/*` 由 **DSH Host profile** 提供,**不**声明为可从公共 registry 安装的依赖。仓库根目录 `pnpm install` 只会装公开包(如 `fflate`)。完整运行请用上面的 `dsh plugin add`,不要指望裸 clone 能从 npm 拉齐 Harness 内部包。
|
||||
|
||||
## Desktop 本地开发(`link:` 路径安装)
|
||||
|
||||
Desktop「添加插件」填本地目录时,会写成 `link:D:/…/netxops`。Node 从**工作区真实路径**解析 `@deepseek-ai/*`,不会走到 Desktop profile 的 `node_modules`,于是 Host 行报 `Cannot find package '@deepseek-ai/schemastery'`,插件页显示 **netxops 未运行**(预设行可能仍正常)。
|
||||
|
||||
装完本地路径后在仓库根执行一次(把 Host 的 `@deepseek-ai/*` peer junction 进本仓库 `node_modules`):
|
||||
|
||||
```powershell
|
||||
bun run link:peers
|
||||
# 或 PowerShell: .\scripts\link-dsh-peers.ps1
|
||||
```text
|
||||
github:hansjone/netxops
|
||||
```
|
||||
|
||||
脚本优先从 `~/.dsh/profiles/node_modules` 选择一整套 peers;该目录不完整时才考虑 profile 的完整依赖集。不要单独替换某个内部包版本,混链可能使 Config 或服务上下文不兼容。
|
||||
也可填入发布包 `dsh-netxops-<版本>.tgz` 的绝对路径。面向普通用户推荐使用明确版本的发布包:安装时无需 GitHub 访问、Bun、Python、源码目录或编译步骤。不要将开发者电脑上的目录路径作为公共安装地址。
|
||||
|
||||
然后**重启 Desktop**(或在插件页关掉再启用 dsh-netxops)。症状对照:
|
||||
安装完成后重启桌面版一次,使预设配置加载。
|
||||
|
||||
| 现象 | 原因 |
|
||||
|------|------|
|
||||
| `Cannot find package '@deepseek-ai/schemastery'` / **netxops 未运行** | 未跑 `link:peers` |
|
||||
| **web boot: dsh-netxops failed** | 检查具体加载错误;若 peers 混链,重跑 `bun run link:peers` 后重启 |
|
||||
| `pending (waiting for service: settingsScope)` → 会话无 `netx__*` / 无 `netx-ops` | 旧客户端仍硬/软等待已删除的 `settingsScope`(DSH 0.2);用 **≥0.1.55** 的 tgz 重装并完全重启 Desktop,勿用脏的 `link:` / 混锁 `.modules.yaml` |
|
||||
| Desktop 卡在 **Loading plugins…** / 设置页无 Netx Ops | 旧客户端 `immediately: true` 或同步 `inject(['configForms'])`;用 **≥0.1.55**(`immediately: false` + deferred poll) |
|
||||
| 无 live connection,预设内无 `netx__*` | 检查 Host 插件是否启用及其加载错误。缺少 Host 时现在停止发布,不再通过预设入口自动补回被关闭的插件。 |
|
||||
## Web 版
|
||||
|
||||
改 Host/Client 后照常 `bun run bundle`,一般不用重装;换机器或清过本仓库 `node_modules` 才需再 `link:peers`。
|
||||
|
||||
备选:`npm pack` 后在 Desktop「添加插件」填 **`.tgz` 绝对路径**(复制进 profile,不走 `link:`,也就不用 link peers)。
|
||||
|
||||
当前实现:第三方运行时依赖(含 `fflate`)已打进 `lib/`;六个 agent-tools 入口为自包含包。Host 启动即发布实际策略,凭据软注入并异步补齐。缺少 Host 连接桥接时,预设入口保持关闭。
|
||||
|
||||
插件卡使用 `settings.section` 和官方 `settings.plugin.item`,配合 deferred `configForms`。能力组支持指定其他预设、全预设公开及总开关。详情见 [工具发布与 UI 修复说明](PUBLICATION-REVIEW.zh.md)。
|
||||
|
||||
## Install
|
||||
|
||||
```powershell
|
||||
dsh plugin --profile web add `
|
||||
github:hansjone/netxops `
|
||||
github:hansjone/dsh-im-ops `
|
||||
github:hansjone/dsh-ops-cron
|
||||
# or from DeepSeekHarness source:
|
||||
# pnpm dsh plugin --profile web add github:hansjone/netxops github:hansjone/dsh-im-ops github:hansjone/dsh-ops-cron
|
||||
dsh web # or: pnpm dsh web
|
||||
```sh
|
||||
dsh plugin --profile web add github:hansjone/netxops
|
||||
dsh web
|
||||
```
|
||||
|
||||
One command, three **direct** profile bundles. Do **not** nest IM/cron under netxops — profile pnpm enables `blockExoticSubdeps`, so `github:` subdependencies fail with `ERR_PNPM_EXOTIC_SUBDEP`. If pnpm blocks a git `prepare` script, allowlist the printed package keys under `allowBuilds` in the profile’s `pnpm-workspace.yaml`, then re-run.
|
||||
|
||||
1. **Settings → Plugins → Netx Ops** → API URL (+ token if the field is enabled).
|
||||
Token fallback: `scripts/set-netx-token.ps1` / `.sh`.
|
||||
Optional: enable **关键告警推送** so this DSH dials `ws(s)://<apiUrl>/v1/integrations/dsh-alarm/ws`. Choose **投递到 DSH 会话** and/or **投递到 WhatsApp / IM**. For IM, install `dsh-im-ops`, create a delivery target, then paste `imBotId` / `imTargetId`. The card header shows live WSS status.
|
||||
Optional: **导出全部会话** downloads `dsh-sessions-<host>-<utc>.zip` via `GET /api/netxops.sessions.export` (browser download; works for cloud Hosts).
|
||||
Capability groups: **ops + biz-monitor** default on in Netx Ops only. Use **指定其他预设** for selective publication or **对所有预设公开** for Host-wide publication. The master switch revokes all Netx tools and skills while keeping selections. These toggles auto-save and affect the next model request without a restart.
|
||||
Optional: **知识库** — set package root (MANIFEST v1.0); default mounts `_skills/kb-*` into Netx Ops; optional public. See [KNOWLEDGE_BASE.md](KNOWLEDGE_BASE.md).
|
||||
2. Restart after install/upgrade so the bundle patch layers apply. Open Settings → **Agent presets** → **Netx Ops** should appear.
|
||||
The Netx Ops preset is **standard + netxops tools/persona** (shell, search, plan, todo, web, workflows, … plus `netx__*`). Re-activate (or restart the Host) so `$DSH_HOME/.agent-presets/netxops` is recomposed from the current Harness `standard` preset.
|
||||
3. **New session → preset Netx Ops** → ask e.g. Critical Top / single-host alarms / 「能否登录」.
|
||||
|
||||
## Key-alarm push
|
||||
|
||||
- **netx** (fixed IP) hosts the subscribe hub and fans out matched key alerts.
|
||||
- **netxops** (each DSH) reuses the same URL/token, connects outbound, and delivers into a DSH session and/or IM via `ctx.dshIm.send`.
|
||||
- Live status is exposed on the Plugins card via Connection RPC (`/netxops` → `alarm-push.status`); no curl required for day-to-day checks.
|
||||
- WhatsApp / `dsh-im` is **optional** for the DSH session path; required only when IM delivery is checked.
|
||||
|
||||
## Export all sessions (HQ pickup)
|
||||
|
||||
- **Where:** Settings → Plugins → **Netx Ops** → **导出全部会话**.
|
||||
- **What:** browser downloads one ZIP — `manifest.json` plus `sessions/<id>/session.jsonl` from this Host’s `sessionPersistence`.
|
||||
- **Why:** each edge / cloud harness keeps its own sessions; Ops downloads from the Web UI and ships archives to HQ.
|
||||
- **Limits:** needs a JSONL (raw-artifact) persistence backend; SQLite backends refuse export.
|
||||
|
||||
## Verify
|
||||
|
||||
1. Plugins card **Netx Ops** visible.
|
||||
2. With key-alarm push on: card badge shows **Connected** (or reconnecting / auth failed with detail).
|
||||
3. Agent presets → Custom → **Netx Ops**.
|
||||
4. Tools include `netx__queryNmsAlarms` / `netx__execManagedNe` / `netx__findTopologyPaths` (**ops** → skill `netx-ops`). Cutover analysis: `netx__getBizMonitorContext` … (**bizMonitor** → `netx-biz-monitor`). Native canvas tools need **topology** (`netx-topology`); advanced dual-unit/orbit/polish requires the separate topology MCP.
|
||||
5. Export section shows a session count; **下载 ZIP** starts a browser download.
|
||||
6. Optional knowledge-base badge shows operator/country/version when `kbRoot` + MANIFEST are valid.
|
||||
|
||||
See [TOOL_MAP.md](TOOL_MAP.md) and [KNOWLEDGE_BASE.md](KNOWLEDGE_BASE.md).
|
||||
|
||||
## Path / local checkout (developers)
|
||||
使用已经下载的发布包,例如:
|
||||
|
||||
```powershell
|
||||
dsh plugin --profile web add D:\path\to\netxops
|
||||
powershell -File .\scripts\link-dsh-peers.ps1 # only for path/link: installs
|
||||
dsh plugin --profile web add "D:\Downloads\dsh-netxops-0.1.70.tgz"
|
||||
dsh web
|
||||
```
|
||||
|
||||
See [examples/local-debug/README.md](../examples/local-debug/README.md).
|
||||
按实际位置替换包路径;已有 Web Host 需重启一次。两种安装方式均使用包内预编译 JS,不会在用户电脑上运行安装编译脚本。Desktop 与 Web 是不同的 Host profile,需要分别安装和配置。
|
||||
|
||||
## 首次配置
|
||||
|
||||
1. 设置 → 插件 → **Netx Ops**,填写 Netx API 地址和 API token。
|
||||
2. 为运维、拓扑、业务监控和知识库选择发布范围:Netx Ops、指定预设或全部预设。
|
||||
3. 新建会话,选择 **Netx Ops**,或选择已经获得 Netx 能力的其他预设。
|
||||
|
||||
默认仅向 Netx Ops 发布运维与业务监控能力;拓扑与全预设公开默认关闭。未配置知识库时不会发布知识库能力。关闭总开关会撤销插件管理的工具和技能,同时保留各组选择。
|
||||
|
||||
能力开关与预设选择自动保存,下一次模型请求使用新策略。API 地址、token、模型语言、告警配置和手工输入的知识库路径使用保存/丢弃;有草稿修改时操作栏才显示。通过目录选择器选择知识库路径会自动保存。
|
||||
|
||||
Netx API 服务与对应权限的 token 是调用工具的前提;安装插件本身不需要连接 Netx 服务。默认地址为 `http://127.0.0.1:8890`,远程 Host 应配置从该 Host 可访问的地址。Harness 内部依赖与 React 由 Harness 提供,不需要从 npm 单独安装。
|
||||
|
||||
## 可选功能
|
||||
|
||||
**告警推送到 Harness 会话**直接使用 Netx URL/token,无需 IM 插件。只有选择 IM 投递时才需要单独安装并配置 `dsh-im-ops`;定时能力按需安装 `dsh-ops-cron`:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add github:hansjone/dsh-im-ops
|
||||
dsh plugin --profile web add github:hansjone/dsh-ops-cron
|
||||
```
|
||||
|
||||
两个 companion 插件不是 Netx Ops 的默认安装项或运行依赖。需要时作为 profile 的直接依赖安装,不要写入 Netx Ops 的嵌套 `github:` 依赖。
|
||||
|
||||
**知识库**:选择包含 MANIFEST 的知识库根目录,并设置发布范围。详见 [知识库说明](KNOWLEDGE_BASE.md)。
|
||||
|
||||
**会话导出**:设置页可下载此 Host 的持久化会话 ZIP,需要 JSONL 持久化后端。导出不是安装条件。
|
||||
|
||||
## 安装与启动排查
|
||||
|
||||
- **下载 GitHub 包失败**:检查网络与 GitHub 可达性,或改用已经下载的版本 `.tgz`。更换 npm 镜像无法修复 GitHub 本身的连接失败。
|
||||
- **提示缺少 `@deepseek-ai/*`**:先确认 Harness 本身可以启动。公共发布包无需额外安装内部包;只有开发用 `link:` 路径安装才需要下面的 peer 链接步骤。
|
||||
- **插件激活失败**:从插件管理页展开具体 Host 加载错误。升级后分别完整重启 Desktop/Web,确认两者装入的是同一目标版本。0.1.70 包含 volatile 配置兼容与全新 profile 的 YAML 修复。
|
||||
- **Netx Ops 预设未出现**:检查 Host 是否启用、日志是否有 standard 模板读取或预设写入错误,然后重启对应 Host。预设从当前 Host 的 standard 组合生成。用户自建同名预设与修改过的文件会保留;请按日志处理冲突。
|
||||
- **设置页仍在等待连接**:确认 Host 插件已激活,刷新页面或重启升级后的 Host;持续失败时同时提供 Host 加载日志与插件版本。
|
||||
- **工具不可用**:确认总开关、对应能力组和当前预设的发布范围,再检查 API 地址、服务连通性和 token 权限。
|
||||
|
||||
安装器只更新当前 profile,保留其他 profile、用户配置、用户预设和无法确认未修改的文件。若发现不支持的 profile 文档结构,会保留原文件并记录错误,避免写入损坏的配置。
|
||||
|
||||
## 本地源码开发
|
||||
|
||||
普通用户无需执行本节。源码 `link:` 安装时,Node 会从工作区真实路径解析依赖,因此需要将当前 Harness 的一套完整 peers 链接到工作区:
|
||||
|
||||
```powershell
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
bun run bundle
|
||||
dsh plugin --profile desktop add D:\path\to\netxops
|
||||
node scripts/link-dsh-peers.mjs --profile desktop
|
||||
```
|
||||
|
||||
Web 开发将 profile 改为 `web`。脚本优先选择该 profile 的完整依赖集,再选择共享 Host 依赖;不会混用不同 profile 的内部包。Shell 与 PowerShell 包装脚本使用同一实现。
|
||||
|
||||
手工预设安装仅用于排查,可在源码仓库运行 `node scripts/install-preset.mjs --profile web`。它使用正式安装器的组合、保留与 profile 更新规则,不会先删除已有预设。普通安装由 Host 自动完成,公开包不包含开发脚本。
|
||||
|
||||
详见 [发布流程](RELEASING.md) 和 [工具发布与 UI 修复记录](PUBLICATION-REVIEW.zh.md)。
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ bun test
|
|||
|
||||
回归覆盖目标预设隔离、Host 继承去重、总开关、旧分组入口、注册回滚、异步取消、卸载、快速切换、保存失败重试、空目标、双 profile 与安装文件保留。定向发布集成测试使用本机真实 Cordis 和 dsh-scope 服务上下文。
|
||||
|
||||
最终检查:Host/Client 构建成功;严格类型检查通过;133 项测试通过;打包清单检查通过。发布文件清单排除了本机 `.npmrc` 和私有调试控制脚本。
|
||||
此前 UI 修复检查:Host/Client 构建成功;严格类型检查通过;133 项测试通过;打包清单检查通过。发布文件清单排除了本机 `.npmrc` 和私有调试控制脚本。
|
||||
|
||||
新增启动回归使用本机官方 Schemastery/Cosmokit 的当前源码作为 schema peer,加载已构建的 Host JS;本机安装的旧 `lib` 不支持 volatile 引用,因此单独测试 `Config({})` 无法覆盖新版桌面行为。测试覆盖旧 profile 的缺省字段、新版和旧版配置、开关为 false、保留引用的更新、即时撤销,以及真实 Cordis 中的激活、所属实例事件和无错误卸载。该集成测试使用隔离目录与工具/skill 服务,不连接 Netx 生产服务。
|
||||
|
||||
|
|
@ -101,3 +101,17 @@ bun test
|
|||

|
||||
|
||||
实际桌面设置卡片已核对;Web 界面、当前与新会话的定向能力目录,以及配置好的 Netx API token 权限仍需在相应环境验证。高级拓扑 MCP、IM 投递和生产 Netx 服务连通性不包含在本次离线验证中。
|
||||
|
||||
## 0.1.70 公开安装与发布检查
|
||||
|
||||
发现并修复 Harness 默认 profile 的“注释 + 空列表 `[]`”安装问题:旧实现直接追加预设 block,产生无效 YAML,重启时可能无法加载。现在保留注释与用户条目,转换空列表后再追加;覆盖 BOM、CRLF、文档边界、重装幂等、带引号的用户预设 ID,以及自定义 profile。profile 配置通过同目录临时文件替换,写入前再次核对已有内容。
|
||||
|
||||
公开包仅包含八个预编译入口、预设覆盖文件、分组技能、图标、中英文插件元信息和文档,排除源码、测试、开发脚本、本机配置与开发机生成的 standard 快照。公开安装使用包内技能,避免错误读取相邻 `netx` 包的目录。没有安装编译钩子、第三方运行时依赖或隐式 IM/cron 安装。
|
||||
|
||||
源码锁文件与公开开发依赖声明已对齐,工作区禁用自动安装 Harness peers;默认构建不再读取开发机的 standard 模板。手工预设安装脚本统一复用正式安装器,去掉先删除预设的行为;不同系统的 peer 链接脚本统一选择同一 Host 的完整依赖集。
|
||||
|
||||
新增发布检查核对真实构建输入、预编译输出哈希、导出、客户端注册、声明式 bundle、运行依赖、技能及展示元信息。修改源码但漏构建、修改输出或遗漏文件会阻止发布。`release:pack` 使用与安装测试相同的 npm 打包器,生成 `.tgz` 和 SHA256 校验文件。
|
||||
|
||||
本机验证:全新临时源码目录从公共 registry 安装锁定的开发依赖后,可在没有 Harness peers/本机 standard 的情况下完成构建与发布检查;实际 npm 包在中文与空格路径中离线安装成功,没有拉取私有 Harness 包或 React。安装后的独立副本使用真实 Harness peers 激活,挂载 15 个运维工具及包内技能,全部 Host/工具入口可导入,卸载后无残留。测试不更改真实 profile、凭据或生产设备。
|
||||
|
||||
当前完整回归为 **137 项通过、0 项失败**;严格类型检查与发布检查通过。Windows/macOS/Linux 的独立构建与离线安装检查已加入 GitHub Actions,跨平台结果以该提交对应的 CI 为准。公开发布流程见 [RELEASING.md](RELEASING.md)。
|
||||
|
|
|
|||
46
docs/RELEASING.md
Normal file
46
docs/RELEASING.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Public release checklist
|
||||
|
||||
The public package ships precompiled JS, preset overlays, skills, metadata and user documentation. It has no install/prepare/prepack build hook and no third-party runtime dependencies. Optional peer packages are supplied by Harness. Publish a tested tarball rather than asking users to build or link the repository.
|
||||
|
||||
## Prepare the source
|
||||
|
||||
Use Node.js 22, pnpm 11.7 and Bun >=1.3.13. Update `package.json.version`, synchronize canonical skills if changed, then run:
|
||||
|
||||
```sh
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
bun run bundle
|
||||
bun run release:check
|
||||
bun run test:release
|
||||
```
|
||||
|
||||
The workspace disables automatic peer installation so a fresh source checkout does not fetch private Harness packages. `bundle` does not generate a preset from a developer's local Host: the installed plugin composes it from the user's running standard preset. `gen:preset-patch` is an explicit local debugging command and its generated snapshot is excluded from the public package.
|
||||
|
||||
The build records source inputs and output hashes in `.release-build.json`. Commit this file and all eight `lib/*.js` exports with source changes. `release:check` fails when outputs are missing, modified or stale, or when a package resource or runtime contract is incomplete. New exports must also be included in `files` and package tests.
|
||||
|
||||
## Check Harness behavior
|
||||
|
||||
For full integration and type checks, first start a locally installed Harness and link one coherent runtime:
|
||||
|
||||
```sh
|
||||
node scripts/link-dsh-peers.mjs --profile desktop
|
||||
bun run typecheck
|
||||
bun test test tests
|
||||
```
|
||||
|
||||
Use `web` when testing the Web runtime. Full tests include the current official volatile Schema implementation, real Cordis scopes, publication changes, UI settings transport, preset preservation and fresh commented profile patches.
|
||||
|
||||
`test:release` creates the actual npm tarball and installs it offline into a temporary profile with spaces and Chinese characters. Lifecycle scripts are allowed in this test, proving normal installation needs no build. It verifies all exports, client registration, bundled skills and metadata, and excludes source, tests, local config and developer scripts. When Harness is installed locally, it also activates that installed copy using actual Host peers and verifies tool/skill mounting and clean unload. The fixture never edits the user's profile or credentials and never calls a Netx production API.
|
||||
|
||||
CI runs package checks, builds and offline installation on Windows, macOS and Linux. CI without Harness checks package installation and client registration; Host activation is covered by the local integration suite. Wait for the platform jobs to pass before publishing.
|
||||
|
||||
## Create and distribute
|
||||
|
||||
```sh
|
||||
bun run release:pack
|
||||
```
|
||||
|
||||
This verifies the build, uses the same npm packer as the installation tests, and creates `dsh-netxops-<version>.tgz` plus a `.sha256` checksum. Install this exact file into a separate Desktop/Web profile before announcing a release. Check the plugin version, settings card, Netx Ops preset, tool availability and revocation. Test API connectivity with an appropriate test service/token, plus any optional IM delivery configuration that the release changes.
|
||||
|
||||
Upload the verified tarball to the version's GitHub Release or publish it to npm as a separate maintainer action. `prepublishOnly` protects publishing from the source tree; `npm publish <tarball>` must use the tarball already checked above. Do not publish merely because a local directory boots successfully.
|
||||
|
||||
Public install instructions should use that version's tarball or a fixed repository tag. Keep companion IM/cron plugins optional. A local filesystem path, an absolute developer directory and peer junctions are development aids, not public distribution dependencies.
|
||||
5
icon.svg
Normal file
5
icon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="16" fill="#2563eb"/>
|
||||
<path d="M18 19L45 24M18 19L27 45M45 24L27 45" fill="none" stroke="#bfdbfe" stroke-width="4" stroke-linecap="round"/>
|
||||
<g fill="#fff"><circle cx="18" cy="19" r="6"/><circle cx="45" cy="24" r="6"/><circle cx="27" cy="45" r="6"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
|
|
@ -132,11 +132,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,11 +132,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,11 +132,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,11 +132,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,11 +132,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,11 +132,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
|
|||
47
lib/index.js
47
lib/index.js
|
|
@ -15,10 +15,12 @@ import {
|
|||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { createRequire as createRequire2 } from "node:module";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
|
@ -261,7 +263,11 @@ function extractPluginsBodyFromStandardPatch(patchYaml) {
|
|||
`);
|
||||
}
|
||||
function pluginsBodyToAgentCordis(pluginsBody) {
|
||||
const prefix = PATCH_PLUGIN_INDENT;
|
||||
const normalized = pluginsBody.replace(/\r\n/g, `
|
||||
`);
|
||||
const prefix = /^([ \t]*)-\s/m.exec(normalized)?.[1];
|
||||
if (prefix === undefined)
|
||||
throw new Error("standard preset plugins must be a YAML list");
|
||||
return `${pluginsBody.replace(/\r\n/g, `
|
||||
`).split(`
|
||||
`).map((line) => {
|
||||
|
|
@ -269,7 +275,7 @@ function pluginsBodyToAgentCordis(pluginsBody) {
|
|||
return "";
|
||||
if (line.startsWith(prefix))
|
||||
return line.slice(prefix.length);
|
||||
return line.trimStart();
|
||||
throw new Error("standard preset plugins have inconsistent indentation");
|
||||
}).join(`
|
||||
`).trimEnd()}
|
||||
`;
|
||||
|
|
@ -476,8 +482,8 @@ function installDirectoryPreset(artifacts, logger) {
|
|||
logger.info("netxops: directory preset composed from %s → %s", artifacts.standardSource, dest);
|
||||
}
|
||||
function syncDeclarativeIntoProfilePatch(profileName, declarativePatch, logger) {
|
||||
if (profileName !== "desktop" && profileName !== "web")
|
||||
return;
|
||||
if (!/^[\w-]+$/.test(profileName))
|
||||
throw new Error("invalid Host profile name");
|
||||
const profileDir = join(resolveDshHome(), "profiles", profileName);
|
||||
const patchPath = join(profileDir, "cordis.patch.yml");
|
||||
if (!existsSync(profileDir))
|
||||
|
|
@ -486,12 +492,17 @@ function syncDeclarativeIntoProfilePatch(profileName, declarativePatch, logger)
|
|||
`;
|
||||
let patch = original;
|
||||
patch = stripManagedPresetBlocks(patch);
|
||||
if (/^\s*- id: preset-netxops\s*$/m.test(patch)) {
|
||||
if (/^\s*-\s+id:\s*(?:preset-netxops|"preset-netxops"|'preset-netxops')\s*(?:#.*)?$/m.test(patch)) {
|
||||
logger.warn("netxops: preserving user-declared preset-netxops in %s", patchPath);
|
||||
return;
|
||||
}
|
||||
if (patch.trim() === "" || patch.trim() === "[]")
|
||||
patch = "";
|
||||
const contentLines = patch.replace(/^\uFEFF/, "").split(/\r?\n/).filter((line) => !/^\s*(?:#.*)?$/.test(line) && !/^(?:---|\.\.\.)[ \t]*(?:#.*)?$/.test(line));
|
||||
if (contentLines.length === 1 && /^\[\][ \t]*(?:#.*)?$/.test(contentLines[0])) {
|
||||
patch = patch.replace(/^\uFEFF/, "").replace(/^\[\][ \t]*(#.*)?$/m, (_line, comment) => comment ?? "");
|
||||
} else if (contentLines.length > 0 && !/^\s*-\s/.test(contentLines[0])) {
|
||||
throw new Error("profile patch must be a YAML block list; preserving the existing document");
|
||||
}
|
||||
patch = patch.replace(/^\.\.\.[ \t]*(#.*)?$/gm, (_line, comment) => comment ?? "");
|
||||
if (!patch.endsWith(`
|
||||
`) && patch.length > 0)
|
||||
patch += `
|
||||
|
|
@ -507,7 +518,19 @@ ${DESKTOP_PATCH_END}
|
|||
logger.warn("netxops: profile patch changed during preset installation; preserving %s", patchPath);
|
||||
return;
|
||||
}
|
||||
writeFileSync(patchPath, next, "utf8");
|
||||
const temporary = join(profileDir, `.netxops-patch-${randomUUID()}.tmp`);
|
||||
try {
|
||||
writeFileSync(temporary, next, { encoding: "utf8", flag: "wx" });
|
||||
if ((existsSync(patchPath) ? readFileSync(patchPath, "utf8") : `[]
|
||||
`) !== original) {
|
||||
logger.warn("netxops: profile patch changed during preset installation; preserving %s", patchPath);
|
||||
return;
|
||||
}
|
||||
renameSync(temporary, patchPath);
|
||||
} finally {
|
||||
if (existsSync(temporary))
|
||||
unlinkSync(temporary);
|
||||
}
|
||||
logger.info("netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
|
||||
}
|
||||
function stripManagedPresetBlocks(patch) {
|
||||
|
|
@ -1485,11 +1508,12 @@ function opsSkillsRoot() {
|
|||
if (envRoot && existsSync3(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname2(fileURLToPath2(import.meta.url));
|
||||
const checkout = [join3(here, "..", ".git"), join3(here, "..", "..", ".git")].some(existsSync3);
|
||||
const siblingCandidates = [
|
||||
join3(here, "..", "..", "..", "netx", "skills"),
|
||||
join3(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync3(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
|
@ -7159,6 +7183,7 @@ function extractKbResolvePath(payload) {
|
|||
}
|
||||
export {
|
||||
writePatchIfUnchanged,
|
||||
syncDeclarativeIntoProfilePatch,
|
||||
resolveToolCallTimeoutMs,
|
||||
resolveTokenCredentialRef,
|
||||
resolveLang,
|
||||
|
|
@ -7167,8 +7192,10 @@ export {
|
|||
name,
|
||||
mergeHostConfigForHeal,
|
||||
mergeConfigSkippingNullish,
|
||||
installDirectoryPreset,
|
||||
inject,
|
||||
ensureAgentPresetInstalled,
|
||||
composeNetxopsPresetArtifacts,
|
||||
apply,
|
||||
NETXOPS_SETTINGS_NAMESPACE,
|
||||
NETXOPS_RPC_CHANNEL,
|
||||
|
|
|
|||
6
locale/en.json
Normal file
6
locale/en.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Netx Ops",
|
||||
"description": "Network operations, topology and monitoring tools with flexible preset publication."
|
||||
}
|
||||
}
|
||||
6
locale/zh.json
Normal file
6
locale/zh.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Netx Ops",
|
||||
"description": "网络运维、拓扑与业务监控工具,支持按预设灵活发布工具和技能。"
|
||||
}
|
||||
}
|
||||
42
package.json
42
package.json
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "dsh-netxops",
|
||||
"version": "0.1.69",
|
||||
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
||||
"version": "0.1.70",
|
||||
"description": "Network operations, topology and monitoring for DeepSeek Harness.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"main": "./lib/index.js",
|
||||
"icon": "./icon.svg",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
|
|
@ -18,11 +19,13 @@
|
|||
"./tools-topology": "./lib/agent-tools-topology.js",
|
||||
"./tools-biz-monitor": "./lib/agent-tools-biz-monitor.js",
|
||||
"./client": "./lib/client.js",
|
||||
"./locale/*.json": "./locale/*.json",
|
||||
"./cordis.patch.yml": "./cordis.bundle.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"src/",
|
||||
"icon.svg",
|
||||
"locale/",
|
||||
"lib/index.js",
|
||||
"lib/agent-tools.js",
|
||||
"lib/agent-tools-ops.js",
|
||||
|
|
@ -31,26 +34,12 @@
|
|||
"lib/agent-tools-topology.js",
|
||||
"lib/agent-tools-biz-monitor.js",
|
||||
"lib/client.js",
|
||||
"tests/",
|
||||
"test/",
|
||||
"presets/",
|
||||
"presets/netxops/PERSONA.md",
|
||||
"presets/netxops/preset.yml",
|
||||
"presets/netxops/skills/",
|
||||
"cordis.patch.yml",
|
||||
"cordis.bundle.patch.yml",
|
||||
"docs/",
|
||||
"examples/",
|
||||
"scripts/build-host.mjs",
|
||||
"scripts/build-client.mjs",
|
||||
"scripts/gen-preset-patch.mjs",
|
||||
"scripts/typecheck.mjs",
|
||||
"scripts/preview-client.mjs",
|
||||
"scripts/link-dsh-peers.mjs",
|
||||
"scripts/link-dsh-peers.ps1",
|
||||
"scripts/link-dsh-peers.sh",
|
||||
"scripts/link-preset.ps1",
|
||||
"scripts/link-preset.sh",
|
||||
"scripts/set-netx-token.ps1",
|
||||
"scripts/set-netx-token.sh",
|
||||
"scripts/sync-skills-from-netx.ps1",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
|
|
@ -71,7 +60,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/hansjone/netxops#readme",
|
||||
"scripts": {
|
||||
"bundle": "bun run scripts/gen-preset-patch.mjs && bun run scripts/build-host.mjs && bun run scripts/build-client.mjs",
|
||||
"bundle": "bun run scripts/build-host.mjs && bun run scripts/build-client.mjs",
|
||||
"bundle:host": "bun run scripts/build-host.mjs",
|
||||
"bundle:client": "bun run scripts/build-client.mjs",
|
||||
"gen:preset-patch": "bun run scripts/gen-preset-patch.mjs",
|
||||
|
|
@ -79,7 +68,11 @@
|
|||
"preview:client": "bun scripts/preview-client.mjs",
|
||||
"link:peers": "node scripts/link-dsh-peers.mjs --profile desktop",
|
||||
"link:peers:ps1": "powershell -NoProfile -File ./scripts/link-dsh-peers.ps1",
|
||||
"test": "bun test test tests"
|
||||
"test": "bun test test tests",
|
||||
"release:check": "node scripts/verify-release.mjs",
|
||||
"release:pack": "node scripts/pack-release.mjs",
|
||||
"test:release": "node --test test/release-package.test.mjs",
|
||||
"prepublishOnly": "node scripts/verify-release.mjs"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
|
|
@ -100,6 +93,7 @@
|
|||
"@deepseek-ai/dsh-settings": "*",
|
||||
"@deepseek-ai/dsh-tools": "*",
|
||||
"@deepseek-ai/dsh-scope": "*",
|
||||
"@deepseek-ai/dsh-llm": "*",
|
||||
"@deepseek-ai/schemastery": "*",
|
||||
"react": "^18.2.0 || ^19.0.0"
|
||||
},
|
||||
|
|
@ -119,6 +113,9 @@
|
|||
"@deepseek-ai/dsh-scope": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-llm": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/schemastery": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -129,6 +126,7 @@
|
|||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"fflate": "^0.8.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"@types/react": "~18.3.1",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
|
|
|
|||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
|
|
@ -1,23 +1,22 @@
|
|||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
autoInstallPeers: false
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
fflate:
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.3
|
||||
react:
|
||||
specifier: ^18.2.0 || ^19.0.0
|
||||
version: 19.2.8
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ~18.3.1
|
||||
version: 18.3.31
|
||||
fflate:
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.3
|
||||
js-yaml:
|
||||
specifier: ^4.1.1
|
||||
version: 4.3.2
|
||||
typescript:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
|
|
@ -30,15 +29,18 @@ packages:
|
|||
'@types/react@18.3.31':
|
||||
resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
fflate@0.8.3:
|
||||
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
|
||||
|
||||
react@19.2.8:
|
||||
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
js-yaml@4.3.2:
|
||||
resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
|
|
@ -54,10 +56,14 @@ snapshots:
|
|||
'@types/prop-types': 15.7.15
|
||||
csstype: 3.2.3
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
fflate@0.8.3: {}
|
||||
|
||||
react@19.2.8: {}
|
||||
js-yaml@4.3.2:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
|
|
|||
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
packages:
|
||||
- .
|
||||
|
||||
# Harness provides these peers. Do not try to fetch internal packages from npm.
|
||||
autoInstallPeers: false
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { recordBuild } from './build-integrity.mjs'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const entry = join(root, 'src/client/index.ts')
|
||||
|
|
@ -35,6 +36,7 @@ const result = await Bun.build({
|
|||
format: 'cjs',
|
||||
sourcemap: 'none',
|
||||
minify: false,
|
||||
metafile: true,
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
},
|
||||
|
|
@ -80,4 +82,5 @@ ${body}
|
|||
|
||||
mkdirSync(dirname(outFile), { recursive: true })
|
||||
writeFileSync(outFile, artifact, 'utf8')
|
||||
recordBuild(outFile, result, 'scripts/build-client.mjs')
|
||||
console.log(`wrote ${outFile} (${artifact.length} bytes); requires: ${[...new Set(requires)].join(', ') || '(none)'}`)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { recordBuild } from './build-integrity.mjs'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const outDir = join(root, 'lib')
|
||||
|
|
@ -66,6 +67,7 @@ for (const { entry, out } of entries) {
|
|||
format: 'esm',
|
||||
sourcemap: 'none',
|
||||
minify: false,
|
||||
metafile: true,
|
||||
external,
|
||||
})
|
||||
|
||||
|
|
@ -76,5 +78,6 @@ for (const { entry, out } of entries) {
|
|||
|
||||
const text = await result.outputs[0].text()
|
||||
writeFileSync(out, text, 'utf8')
|
||||
recordBuild(out, result, 'scripts/build-host.mjs')
|
||||
console.log(`wrote ${out} (${text.length} bytes)`)
|
||||
}
|
||||
|
|
|
|||
44
scripts/build-integrity.mjs
Normal file
44
scripts/build-integrity.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/** Track the actual bundle inputs so a release cannot silently ship stale JS. */
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const manifestPath = resolve(root, '.release-build.json')
|
||||
export const hashText = text => createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex')
|
||||
export const hashFile = path => hashText(readFileSync(path, 'utf8'))
|
||||
|
||||
export function recordBuild(output, result, buildScript) {
|
||||
if (!result.metafile) throw new Error('Build metadata is missing; use Bun >=1.3.13.')
|
||||
const inputs = [...new Set([
|
||||
'package.json', 'pnpm-lock.yaml', buildScript, 'scripts/build-integrity.mjs',
|
||||
...Object.keys(result.metafile.inputs).map(path => relative(root, resolve(path)).replaceAll('\\', '/'))
|
||||
.filter(path => path.startsWith('src/')),
|
||||
])].sort()
|
||||
if (!inputs.some(path => path.startsWith('src/'))) throw new Error('Bundle has no recorded source inputs.')
|
||||
for (const path of inputs) {
|
||||
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Build input escapes repository: ${path}`)
|
||||
}
|
||||
let builds = {}
|
||||
try { builds = JSON.parse(readFileSync(manifestPath, 'utf8')).builds ?? {} } catch { /* First build. */ }
|
||||
const key = relative(root, output).replaceAll('\\', '/')
|
||||
builds[key] = { output: hashFile(output), inputs: Object.fromEntries(inputs.map(path => [path, hashFile(resolve(root, path))])) }
|
||||
writeFileSync(manifestPath, `${JSON.stringify({ format: 1, builds: Object.fromEntries(Object.entries(builds).sort()) }, null, 2)}\n`)
|
||||
}
|
||||
|
||||
export function checkBuilds(directory = root) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(directory, '.release-build.json'), 'utf8'))
|
||||
if (manifest.format !== 1) throw new Error('Unknown build manifest format.')
|
||||
const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'))
|
||||
const outputs = [...new Set(Object.values(pkg.exports).filter(path => path.endsWith('.js')).map(path => path.replace(/^\.\//, '')))]
|
||||
for (const output of outputs) {
|
||||
const build = manifest.builds?.[output]
|
||||
if (!build || hashFile(resolve(directory, output)) !== build.output) throw new Error(`Missing or modified artifact: ${output}; run bun run bundle.`)
|
||||
for (const [path, hash] of Object.entries(build.inputs)) {
|
||||
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Invalid build input: ${path}`)
|
||||
if (hashFile(resolve(directory, path)) !== hash) throw new Error(`Stale artifact ${output}: ${path} changed; run bun run bundle.`)
|
||||
}
|
||||
}
|
||||
return outputs
|
||||
}
|
||||
17
scripts/install-preset.mjs
Normal file
17
scripts/install-preset.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/** Developer fallback: use the same composition and preservation rules as Host activation. */
|
||||
import { existsSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { composeNetxopsPresetArtifacts, installDirectoryPreset, syncDeclarativeIntoProfilePatch } from '../lib/index.js'
|
||||
|
||||
const index = process.argv.indexOf('--profile')
|
||||
const profile = index < 0 ? 'web' : process.argv[index + 1]
|
||||
if (!profile || !/^[\w-]+$/.test(profile)) throw new Error('Invalid profile name.')
|
||||
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
||||
if (!existsSync(join(dshHome, 'profiles', profile, 'package.json'))) {
|
||||
throw new Error(`Harness profile is not installed: ${profile}. Start this Host first.`)
|
||||
}
|
||||
const artifacts = composeNetxopsPresetArtifacts(profile)
|
||||
installDirectoryPreset(artifacts, console)
|
||||
syncDeclarativeIntoProfilePatch(profile, artifacts.declarativePatch, console)
|
||||
console.log(`Netx Ops preset prepared for ${profile}. Restart this Host to load its patch.`)
|
||||
|
|
@ -38,18 +38,13 @@ function profileArg() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Prefer the shared healed fallback `profiles/node_modules` (one coherent peer
|
||||
* set). Do NOT prefer the Desktop
|
||||
* profile's schemastery first — Desktop 0.2 ships 3.18.4 which, when linked
|
||||
* alone into this package, makes `Config({})` return cosmokit volatile proxies
|
||||
* instead of defaults and Host apply fails ("web boot: dsh-netxops failed").
|
||||
* Do NOT createRequire() — that can walk into an unrelated checkout via NODE_PATH.
|
||||
* Select one complete set from the requested profile or shared Host fallback.
|
||||
* Never combine packages from different Host generations or other profiles.
|
||||
*/
|
||||
function candidateDirs(profile) {
|
||||
return [
|
||||
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'),
|
||||
join(dshHome, 'profiles', profile, 'node_modules', '@deepseek-ai'),
|
||||
join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai'),
|
||||
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +77,7 @@ function linkOne(shortName, target) {
|
|||
}
|
||||
|
||||
const profile = profileArg()
|
||||
if (!/^[\w-]+$/.test(profile)) throw new Error('Invalid profile name.')
|
||||
const dirs = candidateDirs(profile)
|
||||
console.log(`DSH_HOME=${dshHome} profile=${profile}`)
|
||||
console.log(`search:\n ${dirs.join('\n ')}`)
|
||||
|
|
|
|||
|
|
@ -1,82 +1,13 @@
|
|||
# Link DSH peer packages into this checkout for local `dsh plugin add <path>` / `link:`.
|
||||
#
|
||||
# Node resolves imports from the realpath of a linked package. A junction out of
|
||||
# ~/.dsh/profiles/<name>/node_modules therefore cannot see the profile's
|
||||
# hoisted peers or the healed profiles/node_modules fallback. Point the peers
|
||||
# this package imports at the shared installation copy instead.
|
||||
#
|
||||
# Usage (PowerShell):
|
||||
# .\scripts\link-dsh-peers.ps1
|
||||
# Optional:
|
||||
# .\scripts\link-dsh-peers.ps1 -DshHome $env:DSH_HOME
|
||||
|
||||
param(
|
||||
[string]$DshHome = $(if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }),
|
||||
[string]$Profile = 'desktop'
|
||||
[string]$DshHome = $env:DSH_HOME,
|
||||
[ValidatePattern('^[\w-]+$')][string]$Profile = 'desktop'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$DstRoot = Join-Path $Root 'node_modules\@deepseek-ai'
|
||||
# Select one complete peer set, avoiding packages from different Host generations.
|
||||
$SearchRoots = @(
|
||||
(Join-Path $DshHome 'profiles\node_modules\@deepseek-ai'),
|
||||
(Join-Path $DshHome "profiles\$Profile\node_modules\@deepseek-ai"),
|
||||
(Join-Path $DshHome 'profiles\web\node_modules\@deepseek-ai')
|
||||
)
|
||||
|
||||
# Runtime imports from src/index.ts (and peers they commonly need nearby).
|
||||
$Peers = @(
|
||||
'schemastery',
|
||||
'cordis',
|
||||
'dsh-credentials',
|
||||
'dsh-settings',
|
||||
'dsh-tools',
|
||||
'dsh-scope',
|
||||
'dsh-llm'
|
||||
)
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $DstRoot | Out-Null
|
||||
Write-Host "DSH_HOME=$DshHome profile=$Profile"
|
||||
$CoherentRoot = $null
|
||||
foreach ($searchRoot in $SearchRoots) {
|
||||
$complete = $true
|
||||
foreach ($peer in $Peers) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $searchRoot "$peer\package.json"))) { $complete = $false; break }
|
||||
}
|
||||
if ($complete) { $CoherentRoot = $searchRoot; break }
|
||||
$PreviousDshHome = $env:DSH_HOME
|
||||
try {
|
||||
if ($DshHome) { $env:DSH_HOME = $DshHome }
|
||||
& node (Join-Path $PSScriptRoot 'link-dsh-peers.mjs') --profile $Profile
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Peer linking failed.' }
|
||||
} finally {
|
||||
$env:DSH_HOME = $PreviousDshHome
|
||||
}
|
||||
if (-not $CoherentRoot) { throw 'No complete Harness peer set found; refusing to mix package generations.' }
|
||||
|
||||
foreach ($name in $Peers) {
|
||||
$src = $null
|
||||
foreach ($root in @($CoherentRoot)) {
|
||||
$candidate = Join-Path $root $name
|
||||
if (Test-Path (Join-Path $candidate 'package.json')) {
|
||||
$src = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $src) {
|
||||
throw "Peer @deepseek-ai/$name not found under:`n $($SearchRoots -join "`n ")"
|
||||
}
|
||||
$dst = Join-Path $DstRoot $name
|
||||
$absoluteDst = [IO.Path]::GetFullPath($dst)
|
||||
$absolutePeerRoot = [IO.Path]::GetFullPath($DstRoot).TrimEnd('\')
|
||||
if ([IO.Path]::GetDirectoryName($absoluteDst) -ne $absolutePeerRoot) {
|
||||
throw "Refusing to replace a path outside the peer directory: $absoluteDst"
|
||||
}
|
||||
if (Test-Path $dst) {
|
||||
$item = Get-Item $dst -Force
|
||||
if ($item.LinkType -eq 'Junction' -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Item -LiteralPath $absoluteDst -Force
|
||||
} else {
|
||||
throw "Refusing to replace a non-link peer directory: $absoluteDst"
|
||||
}
|
||||
}
|
||||
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
|
||||
$ver = (Get-Content (Join-Path $src 'package.json') -Raw | ConvertFrom-Json).version
|
||||
Write-Host "linked @deepseek-ai/$name@$ver <- $src"
|
||||
}
|
||||
|
||||
Write-Host "Done. Restart DeepSeek Harness Desktop (or disable/enable dsh-netxops)."
|
||||
|
|
|
|||
|
|
@ -1,29 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Link DSH peer packages into this checkout for local `dsh plugin add <path>` / `link:`.
|
||||
# See link-dsh-peers.ps1 for why.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
||||
SRC_ROOT="$DSH_HOME/profiles/node_modules/@deepseek-ai"
|
||||
DST_ROOT="$ROOT/node_modules/@deepseek-ai"
|
||||
PEERS=(schemastery cordis dsh-credentials dsh-settings dsh-mcp-client dsh-tools)
|
||||
|
||||
if [[ ! -d "$SRC_ROOT" ]]; then
|
||||
echo "DSH installation fallback missing: $SRC_ROOT" >&2
|
||||
echo "Run dsh web once so dsh heals profiles/node_modules." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_ROOT"
|
||||
for name in "${PEERS[@]}"; do
|
||||
src="$SRC_ROOT/$name"
|
||||
dst="$DST_ROOT/$name"
|
||||
if [[ ! -e "$src" ]]; then
|
||||
echo "Peer not found: $src" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$dst"
|
||||
ln -s "$src" "$dst"
|
||||
echo "linked @deepseek-ai/$name -> $src"
|
||||
done
|
||||
echo "Done. Re-run: dsh web"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec node "$SCRIPT_DIR/link-dsh-peers.mjs" "$@"
|
||||
|
|
|
|||
|
|
@ -1,21 +1,4 @@
|
|||
# Link / install presets/netxops into the DSH user agent-presets root.
|
||||
# Prefer a real directory copy: DSH discovery skips Windows junctions
|
||||
# (Dirent.isDirectory() is false for reparse points).
|
||||
$ErrorActionPreference = "Stop"
|
||||
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$Source = Join-Path $RepoRoot "presets\netxops"
|
||||
$DshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE ".dsh" }
|
||||
$TargetParent = Join-Path $DshHome ".agent-presets"
|
||||
$Target = Join-Path $TargetParent "netxops"
|
||||
|
||||
if (-not (Test-Path (Join-Path $Source "agent.cordis.yml"))) {
|
||||
throw "Missing preset dir: $Source"
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $TargetParent | Out-Null
|
||||
if (Test-Path $Target) {
|
||||
Remove-Item -Recurse -Force $Target
|
||||
}
|
||||
Copy-Item -Recurse -Force $Source $Target
|
||||
Set-Content -Path (Join-Path $Target ".dsh-netxops-managed") -Value ((Get-Date).ToString("o"))
|
||||
Write-Host "Installed $Target (copy from $Source)"
|
||||
Write-Host "Open Settings → Agent presets → Custom → Netx Ops (or new session)."
|
||||
param([ValidatePattern('^[\w-]+$')][string]$Profile = 'desktop')
|
||||
$ErrorActionPreference = 'Stop'
|
||||
& node (Join-Path $PSScriptRoot 'install-preset.mjs') --profile $Profile
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Preset installation failed.' }
|
||||
|
|
|
|||
|
|
@ -1,19 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install presets/netxops into $DSH_HOME/.agent-presets (copy, not symlink).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC="$ROOT/presets/netxops"
|
||||
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
||||
DEST_PARENT="$DSH_HOME/.agent-presets"
|
||||
DEST="$DEST_PARENT/netxops"
|
||||
|
||||
if [[ ! -f "$SRC/agent.cordis.yml" ]]; then
|
||||
echo "Missing preset dir: $SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$DEST_PARENT"
|
||||
rm -rf "$DEST"
|
||||
cp -R "$SRC" "$DEST"
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ > "$DEST/.dsh-netxops-managed"
|
||||
echo "Installed $DEST"
|
||||
echo "Open Settings → Agent presets → Custom → Netx Ops"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec node "$SCRIPT_DIR/install-preset.mjs" "$@"
|
||||
|
|
|
|||
17
scripts/npm-cli.mjs
Normal file
17
scripts/npm-cli.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/** Resolve the npm bundled with Node instead of an unrelated global npm shim. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
export function npmRuntime() {
|
||||
const node = process.versions.bun
|
||||
? spawnSync('node', ['-p', 'process.execPath'], { encoding: 'utf8', windowsHide: true }).stdout.trim()
|
||||
: process.execPath
|
||||
const npm = [process.env.NETXOPS_NPM_CLI,
|
||||
join(dirname(node), 'node_modules/npm/bin/npm-cli.js'),
|
||||
resolve(dirname(node), '../lib/node_modules/npm/bin/npm-cli.js'),
|
||||
].filter(Boolean).find(existsSync)
|
||||
assert.ok(npm, 'Node.js must include npm to build and test the public installation package.')
|
||||
return { node, npm }
|
||||
}
|
||||
21
scripts/pack-release.mjs
Normal file
21
scripts/pack-release.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { root } from './build-integrity.mjs'
|
||||
import { npmRuntime } from './npm-cli.mjs'
|
||||
import { verifyRelease } from './verify-release.mjs'
|
||||
|
||||
const verified = verifyRelease()
|
||||
const { node, npm } = npmRuntime()
|
||||
const packed = spawnSync(node, [npm, 'pack', '--json', '--ignore-scripts', '--cache', join(tmpdir(), 'netxops-npm-cache')], {
|
||||
cwd: root, encoding: 'utf8', windowsHide: true,
|
||||
})
|
||||
if (packed.status !== 0) throw new Error(packed.stderr || String(packed.error))
|
||||
const info = JSON.parse(packed.stdout)[0]
|
||||
const file = join(root, info.filename)
|
||||
const sha = createHash('sha256').update(readFileSync(file)).digest('hex')
|
||||
writeFileSync(`${file}.sha256`, `${sha} ${info.filename}\n`)
|
||||
console.log(`Release ${verified.version}: ${info.files.length} files, ${(info.size / 1024).toFixed(1)} KiB`)
|
||||
console.log(`Package: ${file}\nSHA256: ${sha}`)
|
||||
94
scripts/verify-release.mjs
Normal file
94
scripts/verify-release.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/** Publisher check. No install lifecycle invokes this on a user's machine. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { load } from 'js-yaml'
|
||||
import ts from 'typescript'
|
||||
import { checkBuilds, root } from './build-integrity.mjs'
|
||||
|
||||
export function verifyRelease(directory = root) {
|
||||
const read = path => readFileSync(resolve(directory, path), 'utf8')
|
||||
const pkg = JSON.parse(read('package.json'))
|
||||
const outputs = checkBuilds(directory)
|
||||
assert.equal(pkg.name, 'dsh-netxops')
|
||||
assert.match(pkg.version, /^\d+\.\d+\.\d+$/)
|
||||
assert.equal(pkg.type, 'module')
|
||||
for (const script of ['preinstall', 'install', 'postinstall', 'prepare', 'prepack', 'postpack', 'prepublish']) {
|
||||
assert.ok(!pkg.scripts?.[script], `User installs must not run ${script}.`)
|
||||
}
|
||||
assert.deepEqual(pkg.dependencies, {}, 'Runtime third-party dependencies must be bundled.')
|
||||
for (const [name, range] of Object.entries(pkg.peerDependencies)) {
|
||||
assert.equal(pkg.peerDependenciesMeta[name]?.optional, true, `${name} must be supplied by Harness.`)
|
||||
assert.doesNotMatch(range, /workspace:|file:|link:|github:/)
|
||||
}
|
||||
for (const path of Object.values(pkg.exports)) {
|
||||
assert.ok(path.startsWith('./') && !path.includes('..'), `Unsafe export: ${path}`)
|
||||
if (!path.includes('*')) assert.ok(existsSync(resolve(directory, path)), `Missing export: ${path}`)
|
||||
}
|
||||
assert.ok(read(pkg.icon).includes('<svg'), 'Missing plugin icon.')
|
||||
for (const lang of ['en', 'zh']) {
|
||||
const locale = JSON.parse(read(`locale/${lang}.json`))
|
||||
assert.ok(locale.meta.title && locale.meta.description, `Missing ${lang} plugin metadata.`)
|
||||
}
|
||||
assert.equal(pkg.dsh.client.immediately, false)
|
||||
const patch = load(read(pkg.dsh.bundle.patch))
|
||||
assert.ok(Array.isArray(patch) && patch.length > 0)
|
||||
const hosts = patch.flatMap(row => row.insert ?? []).filter(row => row.name === pkg.name)
|
||||
assert.equal(hosts.length, 1, 'Bundle must insert exactly one Host bridge.')
|
||||
assert.equal(hosts[0].id, 'netxops')
|
||||
const declared = new Set(Object.keys(pkg.peerDependencies))
|
||||
for (const output of outputs) {
|
||||
const text = read(output)
|
||||
const parsed = spawnSync(process.execPath, ['--check', resolve(directory, output)], { encoding: 'utf8', windowsHide: true })
|
||||
assert.equal(parsed.status, 0, `Invalid JS in ${output}: ${parsed.stderr}`)
|
||||
assert.doesNotMatch(text, /\bBun\.|react\/jsx-dev-runtime|agent-tools-shared|(?:from|import\()\s*["']\.\//)
|
||||
if (output !== 'lib/client.js') {
|
||||
const source = ts.createSourceFile(output, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
const visit = node => {
|
||||
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
|
||||
? node.moduleSpecifier
|
||||
: ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword ? node.arguments[0] : undefined
|
||||
if (specifier && ts.isStringLiteral(specifier)) {
|
||||
assert.ok(isBuiltin(specifier.text) || declared.has(specifier.text), `${output} needs undeclared dependency ${specifier.text}`)
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
}
|
||||
}
|
||||
let client
|
||||
vm.runInNewContext(read('lib/client.js'), { window: { __ModuleLoader__: { load(value) { client = value } } } })
|
||||
assert.equal(client.id, pkg.name)
|
||||
assert.equal(typeof client.factory, 'function')
|
||||
const clientExports = client.factory(name => {
|
||||
assert.ok(['react', 'react/jsx-runtime'].includes(name), `Unseeded browser dependency: ${name}`)
|
||||
return {}
|
||||
})
|
||||
assert.equal(typeof clientExports.apply, 'function')
|
||||
assert.deepEqual(Array.from(clientExports.inject), ['slots', 'locale'])
|
||||
for (const group of ['ops/netx-ops', 'topology/netx-topology', 'biz-monitor/netx-biz-monitor']) {
|
||||
const skill = read(`presets/netxops/skills/${group}/SKILL.md`).replace(/\r\n/g, '\n')
|
||||
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(skill)
|
||||
assert.ok(frontmatter, `Missing skill frontmatter: ${group}`)
|
||||
const parsed = load(frontmatter[1])
|
||||
assert.equal(typeof parsed.name, 'string')
|
||||
assert.equal(typeof parsed.description, 'string')
|
||||
assert.ok(skill.slice(frontmatter[0].length).trim())
|
||||
}
|
||||
for (const path of ['presets/netxops/PERSONA.md', 'presets/netxops/preset.yml']) assert.ok(read(path).trim())
|
||||
return { version: pkg.version, outputs }
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
try {
|
||||
const result = verifyRelease()
|
||||
console.log(`Release ${result.version}: ${result.outputs.length} JS artifacts, bundle and skills verified.`)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@ import {
|
|||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
|
@ -328,14 +330,16 @@ export function extractPluginsBodyFromStandardPatch(patchYaml: string): string {
|
|||
|
||||
/** Convert patch-indented plugins body into a top-level `agent.cordis.yml` list. */
|
||||
export function pluginsBodyToAgentCordis(pluginsBody: string): string {
|
||||
const prefix = PATCH_PLUGIN_INDENT
|
||||
const normalized = pluginsBody.replace(/\r\n/g, '\n')
|
||||
const prefix = /^([ \t]*)-\s/m.exec(normalized)?.[1]
|
||||
if (prefix === undefined) throw new Error('standard preset plugins must be a YAML list')
|
||||
return `${pluginsBody
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line.length === 0) return ''
|
||||
if (line.startsWith(prefix)) return line.slice(prefix.length)
|
||||
return line.trimStart()
|
||||
throw new Error('standard preset plugins have inconsistent indentation')
|
||||
})
|
||||
.join('\n')
|
||||
.trimEnd()}\n`
|
||||
|
|
@ -568,7 +572,7 @@ export function syncDeclarativeIntoProfilePatch(
|
|||
declarativePatch: string,
|
||||
logger: Context['logger'],
|
||||
): void {
|
||||
if (profileName !== 'desktop' && profileName !== 'web') return
|
||||
if (!/^[\w-]+$/.test(profileName)) throw new Error('invalid Host profile name')
|
||||
const profileDir = join(resolveDshHome(), 'profiles', profileName)
|
||||
const patchPath = join(profileDir, 'cordis.patch.yml')
|
||||
if (!existsSync(profileDir)) return
|
||||
|
|
@ -578,11 +582,21 @@ export function syncDeclarativeIntoProfilePatch(
|
|||
// Drop previous managed blocks (literal markers — do not RegExp `(managed)`).
|
||||
patch = stripManagedPresetBlocks(patch)
|
||||
// Drop a bare enable stub left by older installs.
|
||||
if (/^\s*- id: preset-netxops\s*$/m.test(patch)) {
|
||||
if (/^\s*-\s+id:\s*(?:preset-netxops|"preset-netxops"|'preset-netxops')\s*(?:#.*)?$/m.test(patch)) {
|
||||
logger.warn('netxops: preserving user-declared preset-netxops in %s', patchPath)
|
||||
return
|
||||
}
|
||||
if (patch.trim() === '' || patch.trim() === '[]') patch = ''
|
||||
// New Harness profiles contain comments followed by `[]`, rather than just
|
||||
// `[]`. Appending a block to that flow list produces invalid YAML on restart.
|
||||
const contentLines = patch.replace(/^\uFEFF/, '').split(/\r?\n/)
|
||||
.filter(line => !/^\s*(?:#.*)?$/.test(line) && !/^(?:---|\.\.\.)[ \t]*(?:#.*)?$/.test(line))
|
||||
if (contentLines.length === 1 && /^\[\][ \t]*(?:#.*)?$/.test(contentLines[0]!)) {
|
||||
patch = patch.replace(/^\uFEFF/, '').replace(/^\[\][ \t]*(#.*)?$/m, (_line, comment) => comment ?? '')
|
||||
} else if (contentLines.length > 0 && !/^\s*-\s/.test(contentLines[0]!)) {
|
||||
throw new Error('profile patch must be a YAML block list; preserving the existing document')
|
||||
}
|
||||
// Keep a single YAML document when an otherwise valid patch has an explicit end.
|
||||
patch = patch.replace(/^\.\.\.[ \t]*(#.*)?$/gm, (_line, comment) => comment ?? '')
|
||||
if (!patch.endsWith('\n') && patch.length > 0) patch += '\n'
|
||||
|
||||
const managed = `${DESKTOP_PATCH_BEGIN}\n${declarativePatch.trimEnd()}\n${DESKTOP_PATCH_END}\n`
|
||||
|
|
@ -592,7 +606,18 @@ export function syncDeclarativeIntoProfilePatch(
|
|||
logger.warn('netxops: profile patch changed during preset installation; preserving %s', patchPath)
|
||||
return
|
||||
}
|
||||
writeFileSync(patchPath, next, 'utf8')
|
||||
const temporary = join(profileDir, `.netxops-patch-${randomUUID()}.tmp`)
|
||||
try {
|
||||
writeFileSync(temporary, next, { encoding: 'utf8', flag: 'wx' })
|
||||
// Check again before replacing the file; interruption cannot leave a partial patch.
|
||||
if ((existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : '[]\n') !== original) {
|
||||
logger.warn('netxops: profile patch changed during preset installation; preserving %s', patchPath)
|
||||
return
|
||||
}
|
||||
renameSync(temporary, patchPath)
|
||||
} finally {
|
||||
if (existsSync(temporary)) unlinkSync(temporary)
|
||||
}
|
||||
logger.info(
|
||||
'netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)',
|
||||
profileName,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ import type {} from '@deepseek-ai/dsh-credentials'
|
|||
import * as DshSettings from '@deepseek-ai/dsh-settings'
|
||||
import type {} from '@deepseek-ai/dsh-settings'
|
||||
export {
|
||||
composeNetxopsPresetArtifacts,
|
||||
ensureAgentPresetInstalled,
|
||||
installDirectoryPreset,
|
||||
NETXOPS_PRESET_ID,
|
||||
syncDeclarativeIntoProfilePatch,
|
||||
} from './agent-preset-install.ts'
|
||||
export {
|
||||
mergeHostConfigForHeal,
|
||||
|
|
|
|||
|
|
@ -36,12 +36,13 @@ export function opsSkillsRoot(): string {
|
|||
if (envRoot && existsSync(envRoot)) return envRoot
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const checkout = [join(here, '..', '.git'), join(here, '..', '..', '.git')].some(existsSync)
|
||||
const siblingCandidates = [
|
||||
// src/netx → ../../../netx/skills ; lib → ../../netx/skills
|
||||
join(here, '..', '..', '..', 'netx', 'skills'),
|
||||
join(here, '..', '..', 'netx', 'skills'),
|
||||
]
|
||||
for (const candidate of siblingCandidates) {
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import assert from 'node:assert/strict'
|
|||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { composeNetxopsPresetArtifacts, hostResolveBases, installDirectoryPreset, resolveActiveProfile, resolveStandardPresetDir, syncDeclarativeIntoProfilePatch } from '../src/agent-preset-install.ts'
|
||||
|
||||
const log = { info() {}, warn() {}, error() {} }
|
||||
const { load } = createRequire(import.meta.url)('js-yaml')
|
||||
const patch = '- insert:\n - id: preset-netxops\n name: preset-plugin\n'
|
||||
function home(t) {
|
||||
const previous = process.env.DSH_HOME
|
||||
|
|
@ -27,6 +29,45 @@ test('updating Desktop patch leaves Web patch intact', t => {
|
|||
syncDeclarativeIntoProfilePatch('desktop', patch, log)
|
||||
assert.equal(readFileSync(web, 'utf8'), original)
|
||||
assert.match(readFileSync(join(root, 'profiles', 'desktop', 'cordis.patch.yml'), 'utf8'), /preset-netxops/)
|
||||
assert.equal(load(readFileSync(join(root, 'profiles', 'desktop', 'cordis.patch.yml'), 'utf8'))[0].insert[0].id, 'preset-netxops')
|
||||
})
|
||||
|
||||
test('fresh commented and delimited profile patches remain valid after install and upgrade', t => {
|
||||
const root = home(t)
|
||||
for (const [index, original] of [
|
||||
'# Your patch layer\n[]\n',
|
||||
'\uFEFF# profile\r\n[] # empty\r\n',
|
||||
'---\n# profile\n[]\n...\n',
|
||||
'# user config\n- id: preserve-me\n config: {value: 7}\n...\n',
|
||||
].entries()) {
|
||||
const profile = `custom-${index}`, folder = join(root, 'profiles', profile)
|
||||
mkdirSync(folder, { recursive: true })
|
||||
const path = join(folder, 'cordis.patch.yml')
|
||||
writeFileSync(path, original)
|
||||
syncDeclarativeIntoProfilePatch(profile, patch, log)
|
||||
const first = readFileSync(path, 'utf8'), rows = load(first)
|
||||
assert.ok(Array.isArray(rows))
|
||||
assert.equal(rows.filter(row => row.insert?.[0]?.id === 'preset-netxops').length, 1)
|
||||
if (index === 3) assert.equal(rows[0].config.value, 7)
|
||||
syncDeclarativeIntoProfilePatch(profile, patch, log)
|
||||
assert.equal(readFileSync(path, 'utf8'), first, 'upgrade is idempotent')
|
||||
}
|
||||
})
|
||||
|
||||
test('quoted or commented user preset IDs are preserved and unsupported roots are not overwritten', t => {
|
||||
const root = home(t), folder = join(root, 'profiles', 'web')
|
||||
mkdirSync(folder, { recursive: true })
|
||||
const path = join(folder, 'cordis.patch.yml')
|
||||
for (const id of ['preset-netxops # custom', '"preset-netxops"', "'preset-netxops'"]) {
|
||||
const original = `- insert:\n - id: ${id}\n name: custom-preset\n`
|
||||
writeFileSync(path, original)
|
||||
syncDeclarativeIntoProfilePatch('web', patch, log)
|
||||
assert.equal(readFileSync(path, 'utf8'), original)
|
||||
}
|
||||
writeFileSync(path, 'config: user-data\n')
|
||||
assert.throws(() => syncDeclarativeIntoProfilePatch('web', patch, log), /preserving/)
|
||||
assert.equal(readFileSync(path, 'utf8'), 'config: user-data\n')
|
||||
assert.throws(() => syncDeclarativeIntoProfilePatch('../outside', patch, log), /invalid/)
|
||||
})
|
||||
|
||||
test('unterminated managed block does not truncate user configuration', t => {
|
||||
|
|
|
|||
120
test/release-package.test.mjs
Normal file
120
test/release-package.test.mjs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import vm from 'node:vm'
|
||||
import { checkBuilds } from '../scripts/build-integrity.mjs'
|
||||
import { npmRuntime } from '../scripts/npm-cli.mjs'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
||||
const { node, npm } = npmRuntime()
|
||||
const run = (args, cwd, env = {}) => {
|
||||
const result = spawnSync(node, args, { cwd, env: { ...process.env, ...env }, encoding: 'utf8', windowsHide: true, timeout: 120_000 })
|
||||
assert.equal(result.status, 0, `${args[0]} failed: ${result.error ?? ''}\n${result.stderr}\n${result.stdout}`)
|
||||
return result.stdout
|
||||
}
|
||||
function fixture(t) {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'netxops-release-'))
|
||||
t.after(() => {
|
||||
assert.equal(dirname(resolve(directory)), resolve(tmpdir()), 'Cleanup must stay in the temporary fixture.')
|
||||
assert.ok(dirname(directory) !== directory && directory.includes('netxops-release-'))
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
})
|
||||
return directory
|
||||
}
|
||||
|
||||
test('the npm tarball installs offline and supplies every exported artifact and skill', async t => {
|
||||
const directory = fixture(t), cache = join(directory, 'cache')
|
||||
const packed = JSON.parse(run([npm, 'pack', '--json', '--ignore-scripts', '--pack-destination', directory, '--cache', cache], root))[0]
|
||||
const names = new Set(packed.files.map(file => file.path))
|
||||
for (const path of Object.values(pkg.exports).filter(path => !path.includes('*'))) assert.ok(names.has(path.slice(2)), `Missing packed export ${path}`)
|
||||
for (const path of ['icon.svg', 'locale/en.json', 'locale/zh.json', pkg.dsh.bundle.patch.slice(2),
|
||||
'presets/netxops/PERSONA.md', 'presets/netxops/preset.yml',
|
||||
'presets/netxops/skills/ops/netx-ops/SKILL.md',
|
||||
'presets/netxops/skills/topology/netx-topology/SKILL.md',
|
||||
'presets/netxops/skills/biz-monitor/netx-biz-monitor/SKILL.md',
|
||||
]) assert.ok(names.has(path), `Missing packed resource ${path}`)
|
||||
for (const path of names) assert.doesNotMatch(path, /^(?:src|test|tests|examples|scripts|node_modules)\/|\.env|\.npmrc/, `Private/developer file included: ${path}`)
|
||||
const profile = join(directory, '全新安装 profile with spaces')
|
||||
mkdirSync(profile)
|
||||
writeFileSync(join(profile, 'package.json'), JSON.stringify({ name: 'isolated-netxops-profile', private: true }))
|
||||
// Deliberately allow lifecycle scripts: a normal install must not invoke a build.
|
||||
run([npm, 'install', '--offline', '--omit=dev', '--no-audit', '--no-fund', '--cache', cache, join(directory, packed.filename)], profile)
|
||||
const installed = join(profile, 'node_modules', pkg.name)
|
||||
assert.equal(lstatSync(installed).isSymbolicLink(), false, 'Must test a real package copy, not the developer checkout.')
|
||||
assert.equal(existsSync(join(profile, 'node_modules', '@deepseek-ai')), false, 'Installation must not fetch private Harness packages.')
|
||||
assert.equal(existsSync(join(profile, 'node_modules', 'react')), false)
|
||||
for (const path of Object.values(pkg.exports).filter(path => path.endsWith('.js'))) run(['--check', join(installed, path)], profile)
|
||||
let client
|
||||
vm.runInNewContext(readFileSync(join(installed, 'lib/client.js'), 'utf8'), { window: { __ModuleLoader__: { load(value) { client = value } } } })
|
||||
assert.equal(client.id, pkg.name)
|
||||
assert.equal(typeof client.factory, 'function')
|
||||
const api = client.factory(name => { assert.ok(['react', 'react/jsx-runtime'].includes(name)); return {} })
|
||||
assert.equal(typeof api.apply, 'function')
|
||||
|
||||
{
|
||||
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
||||
const anchors = [process.env.NETXOPS_HOST_PACKAGE_JSON,
|
||||
join(dshHome, 'profiles', 'desktop', 'package.json'), join(dshHome, 'profiles', 'web', 'package.json'),
|
||||
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'dsh-web-app', 'package.json'),
|
||||
].filter(Boolean).filter(existsSync)
|
||||
let peers
|
||||
for (const anchor of anchors) {
|
||||
try {
|
||||
const req = createRequire(anchor)
|
||||
peers = Object.fromEntries(Object.keys(pkg.peerDependencies).filter(name => name.startsWith('@deepseek-ai/')).map(name => [name, pathToFileURL(req.resolve(name)).href]))
|
||||
break
|
||||
} catch { /* Try a complete Host generation, never mix peers. */ }
|
||||
}
|
||||
if (!peers) { console.log('No installed Harness runtime: offline installation verified; Host activation requires a local Harness.'); return }
|
||||
// A neighboring package must never override the skills shipped to users.
|
||||
const trap = join(profile, 'node_modules', 'netx', 'skills', 'ops', 'netx-ops')
|
||||
mkdirSync(trap, { recursive: true })
|
||||
writeFileSync(join(trap, 'SKILL.md'), '---\nname: unexpected-neighbor\ndescription: Must not load\n---\nwrong package')
|
||||
const loader = join(directory, 'host-loader.mjs')
|
||||
writeFileSync(loader, `let peers; export function initialize(data) { peers = data }
|
||||
export async function resolve(name, ctx, next) { return peers[name] ? { url: peers[name], shortCircuit: true } : next(name, ctx) }`)
|
||||
const smoke = join(directory, 'activate-installed.mjs')
|
||||
writeFileSync(smoke, `import assert from 'node:assert/strict'; import { register } from 'node:module';
|
||||
register(${JSON.stringify(pathToFileURL(loader).href)}, { data: ${JSON.stringify(peers)} });
|
||||
const { Context, Service } = await import('@deepseek-ai/cordis');
|
||||
const host = await import(${JSON.stringify(pathToFileURL(join(installed, 'lib/index.js')).href)});
|
||||
const root = new Context(), tools = new Map(), skills = new Map(), owners = [];
|
||||
class Tools extends Service { constructor(ctx) { super(ctx, 'tools') } register(entry) { assert.ok(!tools.has(entry.name)); tools.set(entry.name, entry); return () => tools.delete(entry.name) } }
|
||||
class Skills extends Service { constructor(ctx) { super(ctx, 'skills') } register(entry) { assert.ok(!skills.has(entry.name)); skills.set(entry.name, entry); return () => skills.delete(entry.name) } }
|
||||
owners.push(await root.plugin(Tools), await root.plugin(Skills));
|
||||
const owner = await root.plugin(host, { installAgentPreset: false, alarmPushEnabled: false, groupOpsPublic: true }); owners.push(owner);
|
||||
assert.equal(owner.state, 2); assert.equal(tools.size, 15);
|
||||
await new Promise(resolve => setTimeout(resolve, 80));
|
||||
assert.ok(skills.has('netx-ops')); assert.equal(skills.has('unexpected-neighbor'), false);
|
||||
assert.ok(skills.get('netx-ops').path.startsWith(${JSON.stringify(installed)}));
|
||||
for (const path of ${JSON.stringify(Object.values(pkg.exports).filter(path => path.endsWith('.js') && path !== './lib/client.js'))}) {
|
||||
const api = await import(new URL(path, ${JSON.stringify(pathToFileURL(join(installed, 'package.json')).href)})); assert.equal(typeof api.apply, 'function');
|
||||
}
|
||||
for (const owner of owners.reverse()) await owner.dispose();
|
||||
await new Promise(resolve => setTimeout(resolve, 40)); assert.equal(tools.size, 0); assert.equal(skills.size, 0);
|
||||
console.log('Installed Host active: 15 tools, bundled ops skill, clean unload.');`)
|
||||
run([smoke], profile, { DSH_HOME: join(directory, 'isolated-dsh-home'), NETX_SKILLS_ROOT: '' })
|
||||
}
|
||||
})
|
||||
|
||||
test('the release gate rejects modified or stale precompiled files', t => {
|
||||
const directory = fixture(t)
|
||||
const manifest = JSON.parse(readFileSync(join(root, '.release-build.json'), 'utf8'))
|
||||
const paths = new Set(['package.json', '.release-build.json', ...Object.keys(manifest.builds),
|
||||
...Object.values(manifest.builds).flatMap(build => Object.keys(build.inputs))])
|
||||
for (const path of paths) { mkdirSync(dirname(join(directory, path)), { recursive: true }); cpSync(join(root, path), join(directory, path)) }
|
||||
assert.equal(checkBuilds(directory).length, 8)
|
||||
const output = join(directory, 'lib/index.js')
|
||||
writeFileSync(output, `${readFileSync(output, 'utf8')}\n// unverified change`)
|
||||
assert.throws(() => checkBuilds(directory), /modified artifact/)
|
||||
cpSync(join(root, 'lib/index.js'), output)
|
||||
const source = join(directory, 'src/index.ts')
|
||||
writeFileSync(source, `${readFileSync(source, 'utf8')}\n// source changed after building`)
|
||||
assert.throws(() => checkBuilds(directory), /Stale artifact/)
|
||||
})
|
||||
|
|
@ -9,7 +9,7 @@ const read = (path) => readFile(resolve(root, path), 'utf8')
|
|||
|
||||
test('package declares Desktop 0.2 client contract', async () => {
|
||||
const pkg = JSON.parse(await read('package.json'))
|
||||
assert.equal(pkg.version, '0.1.69')
|
||||
assert.match(pkg.version, /^\d+\.\d+\.\d+$/)
|
||||
assert.equal(pkg.engines.node, '>=20')
|
||||
assert.equal(pkg.dsh.client.immediately, false)
|
||||
assert.deepEqual(pkg.dsh.client.inject, [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue