@meru454545/nexus-modscript-composer
v2.0.13
Published
Compose multi-layer deployment scripts for NexusEPM agents (L0 → L1 → L2 pipeline)
Maintainers
Readme
@meru2802/nexus-modscript-composer
Compose multi-layer deployment scripts for NexusEPM agents. Takes individual L0 monitoring/collection scripts and produces a composed L1 collection script + L2 deployment wrapper (systemd service on Linux, Scheduled Task on Windows).
Install
npm install @meru2802/nexus-modscript-composerUsage
import { generateFinalScript } from "@meru2802/nexus-modscript-composer";
const l0Bodies = [tomcatScript, postgresqlScript]; // raw L0 script strings with metadata headers
const result = await generateFinalScript(l0Bodies, {
platform: "windows",
templateUrls: {
l1: "https://nexus-endpoint-desktop-app.s3.ap-south-1.amazonaws.com/l1-l2-templates/l1-windows.ps1.tmpl",
l2: "https://nexus-endpoint-desktop-app.s3.ap-south-1.amazonaws.com/l1-l2-templates/l2-windows.ps1.tmpl",
},
orgId: "acme-corp",
icebergEndpoint: "https://iceberg.example.com/api/v1/collect",
agentId: "agent-001",
authToken: "bearer-token",
bufferTime: "5m",
timeout: "10m",
});
console.log(result.l1Script); // composed L1 collection script
console.log(result.l2Script); // L2 deployment wrapper
console.log(result.modules); // ["tomcat", "postgresql"]The L0 output contract (OUTPUT_SCHEMA_VERSION: 2)
An L0 collector writes a stream of single-line JSON records to stdout — nothing else. There are exactly two shapes:
{"log":"probing for tomcat via ps"}
{"log":"found 2 catalina processes"}
{"result":{"module":"tomcat","category":"app_monitoring","status":"ok","timestamp":"...","error":null,"data":{}}}Any number of {"log":...} records, then exactly one {"result":...} as the last line.
L1 captures each module's stream, folds it into one envelope, and sends one POST per module. Your records travel byte-for-byte — L1 never unwraps or reshapes them:
{
"agent_id": "agent-001",
"org_id": "acme-corp",
"module": "tomcat",
"timestamp": "2026-07-29T10:00:00Z",
"logs": [ {"log":"probing for tomcat via ps"}, {"log":"found 2 catalina processes"} ],
"result": { "result": { "module": "tomcat", "status": "ok", "data": {} } }
}Because nothing is unwrapped, the collector payload sits at envelope.result.result.
If a collector errors or reaches the L1 timeout, L1 preserves every valid
{"log":...} record emitted before the failure, appends a synthetic error
result when needed, and still attempts the module POST before exiting.
Collectors get two helpers, defined inside the collector function:
_nexus_log "method 3 (process): running=true score=5" # → {"log":"..."}
cat <<EOF | _nexus_result # → {"result":{...}} on one line
{ "module": "tomcat", ... }
EOFVerify any collector against the contract with:
./scripts/verify-l0-contract.sh # all scripts in l0-scripts/
./scripts/verify-l0-contract.sh l0-scripts/mine.sh # one scriptSee AIX-L0-AUTHORING-GUIDE.md §4 for the full contract, including the PowerShell equivalents.
Template URLs
| Platform | Layer | URL |
|----------|-------|-----|
| Linux | L1 | https://nexus-endpoint-desktop-app.s3.ap-south-1.amazonaws.com/l1-l2-templates/l1-linux.sh.tmpl |
| Linux | L2 | https://nexus-endpoint-desktop-app.s3.ap-south-1.amazonaws.com/l1-l2-templates/l2-linux.sh.tmpl |
| Windows | L1 | https://nexus-endpoint-desktop-app.s3.ap-south-1.amazonaws.com/l1-l2-templates/l1-windows.ps1.tmpl |
| Windows | L2 | https://nexus-endpoint-desktop-app.s3.ap-south-1.amazonaws.com/l1-l2-templates/l2-windows.ps1.tmpl |
generateFinalScript(l0ScriptBodies, options)
Parameters
l0ScriptBodies string[] -- Array of raw L0 script contents. Each string must include the full metadata header (# ==== NEXUS MODSCRIPT L0 ==== ... # ==== END METADATA ====) followed by the function body. All L0 scripts must target the same OS_FAMILY as the platform option.
options GenerateOptions:
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| platform | "linux" \| "windows" | Yes | -- | Target OS. Must match the OS_FAMILY in all L0 scripts. |
| templateUrls | { l1: string, l2: string } | Yes | -- | URLs to fetch the L1 and L2 template files. See table above. |
| orgId | string | No | "test-client" | Organization ID baked into L1 for Iceberg payloads. |
| icebergEndpoint | string | No | "https://iceberg.example.com/api/v1/collect" | URL where L1 POSTs collected data. Written to the L2 env file. |
| agentId | string | No | "test-agent-001" | Agent identifier included in Iceberg payloads. |
| authToken | string | No | "test-bearer-token" | Bearer token for Iceberg API authentication. |
| bufferTime | string | No | "5m" | Wait time after L1 completes before next run. Accepts "30s", "5m", "1h". Linux: OnUnitInactiveSec. Windows: Start-Sleep in wrapper loop. |
| timeout | string | No | "10m" | L0 collection deadline. At the deadline L1 stops unfinished collectors and posts their partial logs; L2 retains a bounded hard-kill grace period. Same format as bufferTime. |
| maxParallel | number | No | 5 | Max parallel L0 modules. Linux: background jobs. Windows: runspace pool size. |
| previousServiceId | string | No | "" | Service ID of a previous deployment to tear down before installing the new one. |
| writeToDisk | boolean | No | false | Write the composed scripts to timestamped files on disk. |
| outputDir | string | No | "./final-scripts" | Directory for writeToDisk output. Resolved relative to process.cwd(). |
Return Value
{
l1Script: string; // Composed L1 collection script (all L0 modules merged)
l2Script: string; // L2 deployment wrapper (installs L1 as systemd service or Scheduled Task)
platform: "linux" | "windows";
modules: string[]; // Names of L0 modules included, e.g. ["tomcat", "postgresql"]
l1Path?: string; // File path if writeToDisk was true
l2Path?: string; // File path if writeToDisk was true
}The function validates the composed scripts (structural checks, syntax via bash -n/shellcheck/pwsh, and a dry-run extraction test) before returning. It throws if validation fails.
Windows deletion verifier lifecycle
Use the deletion-verifier lifecycle only for a Windows Nexus Endpoint removal job. It requires exactly one L0: l0-scripts/nexus_endpoint_deletion.ps1. The collector is read-only and reports the remaining owned artifacts; it never deletes them.
const result = await generateFinalScript([nexusDeletionL0], {
platform: "windows",
templateUrls,
orgId: "org-1",
icebergEndpoint: "https://iceberg.example.com/v1/nexus-deletion/events",
agentId: "agent-1",
bufferTime: "30s",
timeout: "60s",
lifecycle: {
kind: "deletion-verifier",
jobId: "delete-2026-09-25-agent-1",
callbackToken: "one-job-scoped-token",
expectedWindowsUpdateAuOptions: null, // value recorded before installation; null = absent
},
});This produces the isolated task \NexusDeletionVerifier\NexusDeletionVerifier-<jobId> and only uses C:\ProgramData\NexusDeletionVerifier\<jobId>. It does not share the normal NexusModScripts root. The post-RMM finisher is embedded in this verifier; no second finisher script or scheduled task is created. It remains dormant until Iceberg creates delete.intent in the job root. L1 writes completion.ack only when all three conditions hold: the L0 result has data.complete: true, its data.job_id matches the configured job, and Iceberg returned a 2xx response for that exact envelope. L2 then unregisters the verifier task and deletes only its isolated root.
For a deletion job, run scripts/prepare-nexus-rmm-deletion.ps1. It combines ordinary ModScript purging with the silent Nexus Endpoint desktop-app uninstall while deliberately preserving nexusrmm, Nexus Mesh, and the isolated verifier. scripts/purge-nexus-modscripts.ps1 remains available as the backwards-compatible purge-only helper.
Iceberg controller contract
The controller must create a unique job ID and callback token, deploy the verifier before issuing the RMM uninstall command, and accept idempotent callback events. Treat completion as valid only when the authenticated callback has:
{
"module": "nexus_endpoint_deletion",
"result": {
"result": {
"data": {
"job_id": "the-controller-job-id",
"manifest_version": 1,
"complete": true,
"remaining": []
}
}
}
}Require the scoped token plus X-Nexus-Deletion-Job to match the job. Deduplicate X-Nexus-Event-Id; persist every non-complete scan as progress; and mark the job complete only after receiving this complete event. Record AUOptions before installation and pass that original value (or null when absent) as expectedWindowsUpdateAuOptions; restore it before accepting completion. For existing machines that lack this installation-time journal, require an explicit administrator-selected baseline and keep the job pending until one is supplied. Do not rely on the TacticalRMM DELETE HTTP response as proof of endpoint cleanup: it removes the management record before the agent-side uninstall has necessarily finished.
Use this controller order:
Create and durably persist the deletion job, scoped callback token, RMM agent ID, and recorded
AUOptionsbaseline.Generate and run the deletion-verifier L2. L2 registers and immediately starts
\NexusDeletionVerifier\NexusDeletionVerifier-<jobId>. Wait for at least one authenticated progress callback so the verifier is known to be alive.While
nexusrmmis still connected, runprepare-nexus-rmm-deletion.ps1as Administrator. Require its JSON result to bepreparedor explicitly acceptprepared_with_warningsafter recording the warnings. Do not advance onpreparation_incomplete; record itsremaininglist and retry or investigate.Commit the irreversible deletion intent/outbox entry in Iceberg, then create the verifier gate on the machine:
New-Item -ItemType File -Path "C:\ProgramData\NexusDeletionVerifier\<jobId>\delete.intent" -Force | Out-NullImmediately call TacticalRMM
DELETE /agents/<rmm-agent-id>/. The next verifier run sees the intent, waits up to five minutes for the normal agent uninstall, invokes the local RMM uninstaller only as a fallback, performs allowlisted residual cleanup, restoresAUOptions, and then runs the read-only L0 scan.Keep the job pending until the authenticated
complete: trueevent is accepted. That 2xx acknowledgement createscompletion.ack; the existing verifier wrapper then deletes its own scheduled task and isolated job directory.
Steps 4 and 5 must be retryable from the durable outbox state. Creating delete.intent, calling TacticalRMM DELETE, running the embedded finisher, processing callback event IDs, and acknowledging an already-complete job are all designed to be idempotent. Never purge C:\ProgramData\NexusDeletionVerifier from the controller; the acknowledged verifier removes itself.
