Initial commit

This commit is contained in:
mike 2026-06-25 15:28:43 -04:00
commit 5ed52c279d
14 changed files with 1288 additions and 0 deletions

View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Skill(deep-research)",
"Skill(deep-research:*)"
]
}
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
*.vsix

8
.vscodeignore Normal file
View File

@ -0,0 +1,8 @@
src/**
node_modules/**
.vscode/**
tsconfig.json
esbuild.js
**/*.map
**/*.ts
!dist/**

47
esbuild.js Normal file
View File

@ -0,0 +1,47 @@
const esbuild = require("esbuild");
const production = process.argv.includes("--production");
const watch = process.argv.includes("--watch");
const ctx = esbuild.context({
entryPoints: ["src/extension.ts"],
bundle: true,
format: "cjs",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "node",
outfile: "dist/extension.js",
external: ["vscode"],
logLevel: "silent",
plugins: [
{
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[watch] build started");
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
if (location) {
console.error(
` ${location.file}:${location.line}:${location.column}:`
);
}
});
console.log("[watch] build finished");
});
},
},
],
});
ctx.then(async (c) => {
if (watch) {
await c.watch();
} else {
await c.rebuild();
await c.dispose();
}
});

@ -0,0 +1 @@
Subproject commit c1df3e5802601a5dbba0cc2d30889ebd41b4bceb

7
info.env.example Normal file
View File

@ -0,0 +1,7 @@
# GenieACS connection settings
GENIEACS_URL=http://localhost:7557
GENIEACS_USER=admin
GENIEACS_PASS=yourpassword
# Secret pushed into provisioning scripts by push-secret.js
TPLG_SECRET=your-secret-here

10
media/genieacs.svg Normal file
View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<!-- Router/device icon representing ACS -->
<rect x="1" y="5" width="14" height="7" rx="1.5" stroke="currentColor" stroke-width="1.2" fill="none"/>
<circle cx="4" cy="8.5" r="1" fill="currentColor"/>
<circle cx="7" cy="8.5" r="1" fill="currentColor"/>
<circle cx="10" cy="8.5" r="1" fill="currentColor"/>
<line x1="8" y1="5" x2="8" y2="3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
<line x1="5.5" y1="4" x2="8" y2="3" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
<line x1="10.5" y1="4" x2="8" y2="3" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 703 B

489
package-lock.json generated Normal file
View File

@ -0,0 +1,489 @@
{
"name": "genieacs-editor",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "genieacs-editor",
"version": "0.1.0",
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
"esbuild": "^0.20.0",
"typescript": "^5.3.0"
},
"engines": {
"vscode": "^1.85.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.125.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz",
"integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

153
package.json Normal file
View File

@ -0,0 +1,153 @@
{
"name": "genieacs-editor",
"displayName": "GenieACS Script Editor",
"description": "Connect to GenieACS, browse and edit provisioning scripts with full JavaScript support",
"version": "0.1.0",
"publisher": "targo",
"engines": {
"vscode": "^1.85.0"
},
"categories": ["Other"],
"repository": {
"type": "git",
"url": "https://github.com/targo/genieacs-editor"
},
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"configuration": {
"title": "GenieACS",
"properties": {
"genieacs.serverUrl": {
"type": "string",
"default": "http://localhost:7557",
"description": "GenieACS NBI base URL (e.g. http://localhost:7557)"
},
"genieacs.username": {
"type": "string",
"default": "",
"description": "GenieACS username (leave empty if no auth)"
},
"genieacs.password": {
"type": "string",
"default": "",
"description": "GenieACS password"
}
}
},
"viewsContainers": {
"activitybar": [
{
"id": "genieacs-explorer",
"title": "GenieACS",
"icon": "media/genieacs.svg"
}
]
},
"views": {
"genieacs-explorer": [
{
"id": "genieacsScripts",
"name": "Provisioning Scripts"
}
]
},
"commands": [
{
"command": "genieacs.refresh",
"title": "Refresh",
"icon": "$(refresh)",
"category": "GenieACS"
},
{
"command": "genieacs.openScript",
"title": "Open Script",
"category": "GenieACS"
},
{
"command": "genieacs.newScript",
"title": "New Script",
"icon": "$(add)",
"category": "GenieACS"
},
{
"command": "genieacs.deleteScript",
"title": "Delete Script",
"icon": "$(trash)",
"category": "GenieACS"
},
{
"command": "genieacs.renameScript",
"title": "Rename Script",
"category": "GenieACS"
},
{
"command": "genieacs.configure",
"title": "Configure Server",
"icon": "$(settings-gear)",
"category": "GenieACS"
},
{
"command": "genieacs.testConnection",
"title": "Test Connection (Diagnostics)",
"category": "GenieACS"
}
],
"menus": {
"view/title": [
{
"command": "genieacs.refresh",
"when": "view == genieacsScripts",
"group": "navigation"
},
{
"command": "genieacs.newScript",
"when": "view == genieacsScripts",
"group": "navigation"
},
{
"command": "genieacs.configure",
"when": "view == genieacsScripts",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "genieacs.deleteScript",
"when": "view == genieacsScripts && viewItem == script",
"group": "inline"
},
{
"command": "genieacs.renameScript",
"when": "view == genieacsScripts && viewItem == script",
"group": "1_modification"
},
{
"command": "genieacs.deleteScript",
"when": "view == genieacsScripts && viewItem == script",
"group": "1_modification"
}
]
},
"languages": [
{
"id": "genieacs-script",
"aliases": ["GenieACS Script"],
"extensions": []
}
]
},
"scripts": {
"vscode:prepublish": "npm run build",
"build": "node esbuild.js --production",
"watch": "node esbuild.js --watch",
"compile": "tsc --noEmit",
"package": "vsce package"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
"esbuild": "^0.20.0",
"typescript": "^5.3.0"
}
}

256
src/extension.ts Normal file
View File

@ -0,0 +1,256 @@
import * as vscode from "vscode";
import { ScriptsProvider, ScriptItem } from "./scriptsProvider";
import { ScriptFileSystemProvider } from "./scriptFileSystem";
import { createScript, deleteScript, testConnection } from "./genieacsApi";
export function activate(context: vscode.ExtensionContext) {
// --- File system provider ---
const fsProvider = new ScriptFileSystemProvider();
context.subscriptions.push(
vscode.workspace.registerFileSystemProvider("genieacs", fsProvider, {
isCaseSensitive: true,
isReadonly: false,
})
);
// --- Tree view ---
const scriptsProvider = new ScriptsProvider();
const treeView = vscode.window.createTreeView("genieacsScripts", {
treeDataProvider: scriptsProvider,
showCollapseAll: false,
});
context.subscriptions.push(treeView);
// Trigger initial load
scriptsProvider.reload();
// --- Commands ---
context.subscriptions.push(
vscode.commands.registerCommand("genieacs.refresh", () => {
scriptsProvider.reload();
})
);
context.subscriptions.push(
vscode.commands.registerCommand(
"genieacs.openScript",
async (item: ScriptItem) => {
const uri = ScriptFileSystemProvider.uriForScript(item.script._id);
try {
const doc = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(doc, {
preview: false,
});
// Force JavaScript language mode for syntax highlighting + IntelliSense
await vscode.languages.setTextDocumentLanguage(doc, "javascript");
// Scroll to top
editor.revealRange(new vscode.Range(0, 0, 0, 0));
} catch (err) {
vscode.window.showErrorMessage(
`Failed to open script: ${err instanceof Error ? err.message : String(err)}`
);
}
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand("genieacs.newScript", async () => {
const id = await vscode.window.showInputBox({
prompt: "Enter a name for the new script",
placeHolder: "e.g. provision-model-xyz",
validateInput: (v) =>
v.trim() ? undefined : "Script name cannot be empty",
});
if (!id) return;
const trimmed = id.trim();
const initial = [
`"use strict";`,
``,
`// GenieACS provisioning script: ${trimmed}`,
`// Available globals: declare, commit, log`,
``,
].join("\n");
try {
await createScript(trimmed, initial);
scriptsProvider.reload();
// Open the newly created script
const uri = ScriptFileSystemProvider.uriForScript(trimmed);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await vscode.languages.setTextDocumentLanguage(doc, "javascript");
} catch (err) {
vscode.window.showErrorMessage(
`Failed to create script: ${err instanceof Error ? err.message : String(err)}`
);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand(
"genieacs.deleteScript",
async (item: ScriptItem) => {
const answer = await vscode.window.showWarningMessage(
`Delete script "${item.script._id}"? This cannot be undone.`,
{ modal: true },
"Delete"
);
if (answer !== "Delete") return;
try {
await deleteScript(item.script._id);
// Close the open editor for this script if it exists
const uri = ScriptFileSystemProvider.uriForScript(item.script._id);
fsProvider.invalidate(item.script._id);
for (const editor of vscode.window.visibleTextEditors) {
if (editor.document.uri.toString() === uri.toString()) {
await vscode.window.showTextDocument(editor.document, {
viewColumn: editor.viewColumn,
});
await vscode.commands.executeCommand(
"workbench.action.closeActiveEditor"
);
}
}
scriptsProvider.reload();
} catch (err) {
vscode.window.showErrorMessage(
`Failed to delete script: ${err instanceof Error ? err.message : String(err)}`
);
}
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand(
"genieacs.renameScript",
async (item: ScriptItem) => {
const newId = await vscode.window.showInputBox({
prompt: "Enter new name for the script",
value: item.script._id,
validateInput: (v) =>
v.trim() ? undefined : "Script name cannot be empty",
});
if (!newId || newId.trim() === item.script._id) return;
try {
// GenieACS has no rename endpoint — copy then delete
await createScript(newId.trim(), item.script.script);
await deleteScript(item.script._id);
fsProvider.invalidate(item.script._id);
const oldUri = ScriptFileSystemProvider.uriForScript(item.script._id);
for (const editor of vscode.window.visibleTextEditors) {
if (editor.document.uri.toString() === oldUri.toString()) {
await vscode.window.showTextDocument(editor.document, {
viewColumn: editor.viewColumn,
});
await vscode.commands.executeCommand(
"workbench.action.closeActiveEditor"
);
}
}
scriptsProvider.reload();
// Open renamed script
const uri = ScriptFileSystemProvider.uriForScript(newId.trim());
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await vscode.languages.setTextDocumentLanguage(doc, "javascript");
} catch (err) {
vscode.window.showErrorMessage(
`Failed to rename script: ${err instanceof Error ? err.message : String(err)}`
);
}
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand("genieacs.testConnection", async () => {
const result = await testConnection();
const panel = vscode.window.createOutputChannel("GenieACS Diagnostics");
panel.clear();
panel.appendLine("=== GenieACS Connection Diagnostics ===");
panel.appendLine(`URL: ${result.url}`);
panel.appendLine(`Status: ${result.status ?? "N/A"}`);
panel.appendLine(`Error: ${result.error ?? "none"}`);
panel.appendLine(`Body: ${result.body ?? "(empty)"}`);
panel.show();
})
);
context.subscriptions.push(
vscode.commands.registerCommand("genieacs.configure", async () => {
const url = await vscode.window.showInputBox({
prompt: "GenieACS NBI URL",
value:
vscode.workspace
.getConfiguration("genieacs")
.get<string>("serverUrl") || "http://localhost:7557",
placeHolder: "http://localhost:7557",
});
if (url === undefined) return;
const username = await vscode.window.showInputBox({
prompt: "Username (leave empty if no authentication)",
value:
vscode.workspace
.getConfiguration("genieacs")
.get<string>("username") || "",
});
if (username === undefined) return;
let password = "";
if (username) {
const pw = await vscode.window.showInputBox({
prompt: "Password",
password: true,
value:
vscode.workspace
.getConfiguration("genieacs")
.get<string>("password") || "",
});
if (pw === undefined) return;
password = pw;
}
const cfg = vscode.workspace.getConfiguration("genieacs");
await cfg.update("serverUrl", url, vscode.ConfigurationTarget.Global);
await cfg.update("username", username, vscode.ConfigurationTarget.Global);
await cfg.update("password", password, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage("GenieACS configuration saved.");
scriptsProvider.reload();
})
);
// Auto-save: when a genieacs: document is saved, write back to server
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument(async (doc) => {
if (doc.uri.scheme !== "genieacs") return;
// writeFile is triggered by VS Code automatically through the filesystem provider
// This handler is just for user feedback
vscode.window.setStatusBarMessage("$(check) Saved to GenieACS", 3000);
})
);
// When a genieacs: document is opened, ensure JS language mode
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(async (doc) => {
if (doc.uri.scheme !== "genieacs") return;
if (doc.languageId !== "javascript") {
await vscode.languages.setTextDocumentLanguage(doc, "javascript");
}
})
);
}
export function deactivate() {}

122
src/genieacsApi.ts Normal file
View File

@ -0,0 +1,122 @@
import * as https from "https";
import * as http from "http";
import * as vscode from "vscode";
export interface GenieScript {
_id: string;
script: string;
}
function getConfig() {
const cfg = vscode.workspace.getConfiguration("genieacs");
return {
serverUrl: (cfg.get<string>("serverUrl") || "http://localhost:7557").replace(/\/$/, ""),
username: cfg.get<string>("username") || "",
password: cfg.get<string>("password") || "",
};
}
function request(
method: string,
path: string,
body?: string,
contentType = "application/json"
): Promise<{ status: number; data: string }> {
return new Promise((resolve, reject) => {
const { serverUrl, username, password } = getConfig();
const url = new URL(serverUrl + path);
const isHttps = url.protocol === "https:";
const lib = isHttps ? https : http;
const headers: Record<string, string> = {
"Content-Type": contentType,
};
if (username) {
const encoded = Buffer.from(`${username}:${password}`).toString("base64");
headers["Authorization"] = `Basic ${encoded}`;
}
const options: http.RequestOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method,
headers,
};
const req = lib.request(options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve({ status: res.statusCode ?? 0, data }));
});
req.on("error", reject);
if (body) req.write(body);
req.end();
});
}
export async function listScripts(): Promise<GenieScript[]> {
const res = await request("GET", "/provisions?limit=1000");
if (res.status !== 200) {
throw new Error(`Failed to list provisions (HTTP ${res.status}): ${res.data}`);
}
return JSON.parse(res.data) as GenieScript[];
}
export async function getScript(id: string): Promise<GenieScript> {
// GenieACS NBI does not have a single-item GET for provisions;
// filter the list by _id instead.
const res = await request(
"GET",
`/provisions?query=${encodeURIComponent(JSON.stringify({ _id: id }))}`
);
if (res.status !== 200) {
throw new Error(`Failed to get provision "${id}" (HTTP ${res.status}): ${res.data}`);
}
const list = JSON.parse(res.data) as GenieScript[];
if (list.length === 0) throw new Error(`Provision "${id}" not found`);
return list[0];
}
export async function saveScript(id: string, content: string): Promise<void> {
// GenieACS expects the raw script text as the body, not JSON-wrapped
const res = await request(
"PUT",
`/provisions/${encodeURIComponent(id)}`,
content,
"application/javascript"
);
if (res.status !== 200 && res.status !== 201 && res.status !== 204) {
throw new Error(`Failed to save provision "${id}" (HTTP ${res.status}): ${res.data}`);
}
}
export async function createScript(id: string, content: string): Promise<void> {
await saveScript(id, content);
}
export async function deleteScript(id: string): Promise<void> {
const res = await request("DELETE", `/provisions/${encodeURIComponent(id)}`);
if (res.status !== 200 && res.status !== 204) {
throw new Error(`Failed to delete provision "${id}" (HTTP ${res.status}): ${res.data}`);
}
}
export async function testConnection(): Promise<{
url: string;
status?: number;
body?: string;
error?: string;
}> {
const { serverUrl } = getConfig();
const url = `${serverUrl}/provisions?limit=5`;
try {
const res = await request("GET", "/provisions?limit=5");
return { url, status: res.status, body: res.data.slice(0, 500) };
} catch (err) {
return { url, error: err instanceof Error ? err.message : String(err) };
}
}

89
src/scriptFileSystem.ts Normal file
View File

@ -0,0 +1,89 @@
import * as vscode from "vscode";
import { getScript, saveScript } from "./genieacsApi";
/**
* Virtual filesystem for GenieACS scripts.
* URIs look like: genieacs:/scripts/<scriptId>.js
*
* Opening a URI fetches the script from the API and presents it as a JS file.
* Saving writes back to the API.
*/
export class ScriptFileSystemProvider implements vscode.FileSystemProvider {
private _onDidChangeFile = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile = this._onDidChangeFile.event;
// In-memory cache: scriptId → { content, mtime }
private cache = new Map<string, { content: Uint8Array; mtime: number }>();
static scriptIdFromUri(uri: vscode.Uri): string {
// path is /scripts/<id>.js → strip prefix and .js suffix
const match = uri.path.match(/^\/scripts\/(.+)\.js$/);
if (!match) throw new Error(`Unexpected URI path: ${uri.path}`);
return decodeURIComponent(match[1]);
}
static uriForScript(id: string): vscode.Uri {
return vscode.Uri.parse(`genieacs:/scripts/${encodeURIComponent(id)}.js`);
}
watch(): vscode.Disposable {
return { dispose: () => {} };
}
stat(uri: vscode.Uri): vscode.FileStat {
const id = ScriptFileSystemProvider.scriptIdFromUri(uri);
const cached = this.cache.get(id);
return {
type: vscode.FileType.File,
ctime: 0,
mtime: cached?.mtime ?? 0,
size: cached?.content.byteLength ?? 0,
};
}
readDirectory(): never {
throw vscode.FileSystemError.NoPermissions("readDirectory not supported");
}
createDirectory(): never {
throw vscode.FileSystemError.NoPermissions("createDirectory not supported");
}
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const id = ScriptFileSystemProvider.scriptIdFromUri(uri);
const script = await getScript(id);
const content = Buffer.from(script.script ?? "", "utf8");
this.cache.set(id, { content, mtime: Date.now() });
return content;
}
async writeFile(
uri: vscode.Uri,
content: Uint8Array,
_options: { create: boolean; overwrite: boolean }
): Promise<void> {
const id = ScriptFileSystemProvider.scriptIdFromUri(uri);
const text = Buffer.from(content).toString("utf8");
await saveScript(id, text);
const mtime = Date.now();
this.cache.set(id, { content, mtime });
this._onDidChangeFile.fire([{ type: vscode.FileChangeType.Changed, uri }]);
}
delete(uri: vscode.Uri): void {
const id = ScriptFileSystemProvider.scriptIdFromUri(uri);
this.cache.delete(id);
}
rename(): never {
throw vscode.FileSystemError.NoPermissions(
"Rename via filesystem not supported — use the tree view context menu"
);
}
/** Evict a cached entry so the next open re-fetches from the server. */
invalidate(id: string): void {
this.cache.delete(id);
}
}

79
src/scriptsProvider.ts Normal file
View File

@ -0,0 +1,79 @@
import * as vscode from "vscode";
import { GenieScript, listScripts } from "./genieacsApi";
export class ScriptItem extends vscode.TreeItem {
constructor(
public readonly script: GenieScript,
public readonly collapsibleState = vscode.TreeItemCollapsibleState.None
) {
super(script._id, collapsibleState);
this.tooltip = `GenieACS provisioning script: ${script._id}`;
this.description = "";
this.contextValue = "script";
this.command = {
command: "genieacs.openScript",
title: "Open Script",
arguments: [this],
};
this.iconPath = new vscode.ThemeIcon("symbol-file");
}
}
export class ScriptsProvider implements vscode.TreeDataProvider<ScriptItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<ScriptItem | undefined | void>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private scripts: GenieScript[] = [];
private loading = false;
private lastError: string | null = null;
refresh(): void {
this.scripts = [];
this._onDidChangeTreeData.fire();
}
async reload(): Promise<void> {
this.loading = true;
this.lastError = null;
this._onDidChangeTreeData.fire();
try {
this.scripts = await listScripts();
this.scripts.sort((a, b) => a._id.localeCompare(b._id));
} catch (err) {
this.lastError = err instanceof Error ? err.message : String(err);
this.scripts = [];
} finally {
this.loading = false;
this._onDidChangeTreeData.fire();
}
}
getTreeItem(element: ScriptItem): vscode.TreeItem {
return element;
}
async getChildren(): Promise<ScriptItem[]> {
if (this.loading) {
const item = new vscode.TreeItem("Loading scripts...");
item.iconPath = new vscode.ThemeIcon("loading~spin");
return [item as unknown as ScriptItem];
}
if (this.lastError) {
const item = new vscode.TreeItem("Connection failed — click to configure");
item.iconPath = new vscode.ThemeIcon("error");
item.command = { command: "genieacs.configure", title: "Configure" };
item.tooltip = this.lastError;
return [item as unknown as ScriptItem];
}
if (this.scripts.length === 0) {
const item = new vscode.TreeItem("No scripts found");
item.iconPath = new vscode.ThemeIcon("info");
return [item as unknown as ScriptItem];
}
return this.scripts.map((s) => new ScriptItem(s));
}
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}