Author SHA1 Message Date
Renovate 519d79275e chore(deps): update dependency glob to v13.0.1
Lint / pre-commit Linting (push) Successful in 1m18s
Test / Run Tests (push) Successful in 1m40s
2026-02-08 17:07:25 +00:00
22 changed files with 2459 additions and 2976 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
# .gitea/CODEOWNERS
# Gitea's CODEOWNERS uses Go-style Regular Expressions.
# Patterns are evaluated from top to bottom; the last matching rule takes precedence.
# Global owner: Assign @AutoReview to all files
.* @AutoReview
+18
View File
@@ -0,0 +1,18 @@
name: Lint
on:
push:
workflow_dispatch:
jobs:
pre-commit-lint:
name: pre-commit Linting
runs-on: pi
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- run: pip install pre-commit
shell: bash
- name: Pre Commit
run: SKIP=no-commit-to-branch pre-commit run -a
shell: bash
-55
View File
@@ -1,55 +0,0 @@
name: Recompile dist/ on Renovate PR
on:
pull_request:
types: [opened, synchronize]
paths:
- package.json
- pnpm-lock.yaml
- index.js
- monitor.js
permissions:
contents: write
jobs:
recompile:
runs-on: pi
if: |
(github.actor == 'renovate[bot]' || contains(github.head_ref, 'renovate/')) &&
!contains(github.event.head_commit.message, '[skip compile]')
steps:
- name: Checkout PR Branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.head_ref }}
fetch-depth: 0
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Clean Install
run: pnpm install --frozen-lockfile --ignore-scripts
shell: bash
- name: Build
run: pnpm run build
shell: bash
- name: Commit dist/
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add dist/
if git diff --staged --quiet; then
echo "No changes to dist/"
else
git commit -m "chore: compile action bundle [skip compile]"
git push origin HEAD:${{ github.head_ref }}
fi
shell: bash
+4 -7
View File
@@ -2,7 +2,6 @@ name: Test
on: on:
push: push:
pull_request:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -10,13 +9,11 @@ jobs:
name: Run Tests name: Run Tests
runs-on: pi runs-on: pi
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@v6
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - uses: actions/setup-node@v6
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: '24' node-version: '24'
cache: pnpm - run: npm install
- run: pnpm install --frozen-lockfile
shell: bash shell: bash
- run: pnpm test - run: npm test
shell: bash shell: bash
-3
View File
@@ -1078,6 +1078,3 @@ FodyWeavers.xsd
/node_modules/.cache/ /node_modules/.cache/
/coverage/ /coverage/
!/dist/ !/dist/
# Local test credentials — never commit
.env.test
+1 -2
View File
@@ -21,7 +21,6 @@ repos:
hooks: hooks:
- id: pretty-format-yaml - id: pretty-format-yaml
args: [--autofix] args: [--autofix]
exclude: ^pnpm-lock\.yaml$
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8 rev: v4.0.0-alpha.8
@@ -30,7 +29,7 @@ repos:
types_or: [css, javascript] types_or: [css, javascript]
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.38.0 rev: 0.36.0
hooks: hooks:
- id: check-renovate - id: check-renovate
- id: check-github-actions - id: check-github-actions
+4 -11
View File
@@ -6,8 +6,6 @@ This repository is maintained by Gemini.
* **Test-Driven Development (TDD):** Wherever possible, Test-Driven Development principles should be followed. Write tests before writing the code they are intended to validate. * **Test-Driven Development (TDD):** Wherever possible, Test-Driven Development principles should be followed. Write tests before writing the code they are intended to validate.
* **Pre-commit Hooks:** Ensure that `pre-commit` hooks are installed and active before making any commits. This can be done by running `pre-commit install` in your local repository. * **Pre-commit Hooks:** Ensure that `pre-commit` hooks are installed and active before making any commits. This can be done by running `pre-commit install` in your local repository.
* **Note for Gemini:** Git commits trigger pre-commit hooks, which can take several seconds (or minutes) to complete. Checking the command status for git commit is only appropriate every 120s.
* **Package Manager Preferences:** Always use `pnpm` and `pnpx`. Never use `npm` or `npx` under any circumstances. All package management operations, script runs, and tool invocations must use `pnpm` / `pnpx`.
## Repository Comparison ## Repository Comparison
@@ -20,7 +18,7 @@ This project uses [Vitest](https://vitest.dev/) for testing. The tests are locat
To run the tests locally, use the following command: To run the tests locally, use the following command:
```bash ```bash
pnpm test npm test
``` ```
### Testing Pipeline ### Testing Pipeline
@@ -30,11 +28,6 @@ The tests are automatically run on every push and workflow dispatch using a Gite
The Gitea workflow does the following: The Gitea workflow does the following:
1. Checks out the code. 1. Checks out the code.
2. Sets up Node.js and pnpm. 2. Sets up Node.js.
3. Installs the dependencies using `pnpm install`. 3. Installs the dependencies using `npm install`.
4. Runs the tests using `pnpm test`. 4. Runs the tests using `npm test`.
### Automated Recompilation & Permissions
* The automated recompilation workflow (`.gitea/workflows/recompile-dist.yaml`) recompiles `dist/index.js` on Renovate PRs and requires `contents: write` permissions to push back to the branch.
* Ensure that the Gitea repository workflow settings have write permissions enabled (**Settings** ➔ **Actions** ➔ **General** ➔ **Workflow permissions** ➔ **Read and write permissions**).
+3 -16
View File
@@ -26,19 +26,6 @@ To use this action, you need to set it up in your workflow .yml file located in
- `pattern`: Glob pattern to match files (default: *.js). - `pattern`: Glob pattern to match files (default: *.js).
- `branch`: Branch in Screeps to which the code will be uploaded (default: default). - `branch`: Branch in Screeps to which the code will be uploaded (default: default).
- `git-replace`: Overwrite "{{gitRef}}", "{{gitHash}}" and "{{deployTime}}" values in files matching the pattern. - `git-replace`: Overwrite "{{gitRef}}", "{{gitHash}}" and "{{deployTime}}" values in files matching the pattern.
- `shard`: The Screeps shard to monitor (e.g. `shard0`, `shard1`). Defaults to `shard0` on the official server.
- `monitor`: Number of game ticks to monitor the Screeps console after deploying (0 = disabled, default: 0).
- `log_to_file`: If `true`, buffers stdout to an artifact file instead of streaming live (default: false). Note: Errors and warnings always stream live.
- `on_traceback`: Action on JS traceback detection: `ignore`, `warn`, or `fail` (default: `fail`).
- `on_error_log`: Action on Screeps error-console output: `ignore`, `warn`, or `fail` (default: `warn`).
- `on_warning_log`: Action on `console.warn` output: `ignore`, `warn`, or `fail` (default: `ignore`).
- `monitor_interval`: Print a progress update every N ticks (default: 10).
## Outputs
- `saw_traceback`: `true` if a JS traceback was detected during monitoring.
- `saw_error_log`: `true` if the Screeps error console had output during monitoring.
- `saw_warning_log`: `true` if `console.warn` output was detected during monitoring.
Example Workflow Example Workflow
@@ -84,7 +71,7 @@ This project includes a build step to prepare the action for use.
### Why a Build Step? ### Why a Build Step?
We use a build step to compile the JavaScript code and its dependencies into a single, self-contained file (`dist/index.js`). This approach is chosen for the following reasons: We use a build step to compile the JavaScript code and its dependencies into a single, self-contained file (`dist/index.js`). This approach is chosen for the following reasons:
1. **Zero-configuration at Runtime:** By bundling everything, the action runner doesn't need to run `pnpm install` or `npm install`, which speeds up the action execution. 1. **Zero-configuration at Runtime:** By bundling everything, the action runner doesn't need to run `npm install`, which speeds up the action execution.
2. **Reliability:** It guarantees that the exact versions of dependencies tested during development are used in the action, avoiding potential issues with version updates or missing packages. 2. **Reliability:** It guarantees that the exact versions of dependencies tested during development are used in the action, avoiding potential issues with version updates or missing packages.
3. **Standard Practice:** This is a recommended practice for JavaScript-based GitHub/Gitea Actions. 3. **Standard Practice:** This is a recommended practice for JavaScript-based GitHub/Gitea Actions.
@@ -100,8 +87,8 @@ We chose [`@vercel/ncc`](https://github.com/vercel/ncc) as the compiler because:
If you modify `index.js` or change dependencies, you must rebuild the project before committing: If you modify `index.js` or change dependencies, you must rebuild the project before committing:
```bash ```bash
pnpm install npm install
pnpm run build npm run build
``` ```
This will update the `dist/index.js` file, which should be committed to the repository. This will update the `dist/index.js` file, which should be committed to the repository.
+6 -155
View File
@@ -1,58 +1,13 @@
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; const {
// Mock @actions/core for all tests in this file
vi.mock("@actions/core", () => ({
getInput: vi.fn(),
getBooleanInput: vi.fn(),
info: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
}));
// Mock monitor.js so postCode() integration tests don't open a real socket
vi.mock("../monitor.js", () => ({
monitorConsole: vi.fn().mockResolvedValue({
sawTraceback: false,
sawErrorLog: false,
sawWarningLog: false,
}),
}));
// Mock screeps-api
vi.mock("screeps-api", () => {
const mockApi = {
auth: vi.fn().mockResolvedValue(),
code: {
get: vi.fn().mockResolvedValue({ ok: 1, modules: { main: "old_code" } }),
set: vi.fn().mockResolvedValue({ ok: 1 }),
},
};
// Use a regular function so it can be called with `new`
return {
ScreepsAPI: vi.fn(function () {
return mockApi;
}),
};
});
import * as core from "@actions/core";
import { monitorConsole } from "../monitor.js";
import {
validateAuthentication, validateAuthentication,
replacePlaceholders, replacePlaceholders,
readReplaceAndWriteFiles, readReplaceAndWriteFiles,
readFilesIntoDict, readFilesIntoDict,
applyOnAction, } = require("../index");
postCode, const fs = require("fs");
} from "../index.js"; const path = require("path");
import { ScreepsAPI } from "screeps-api"; const os = require("os");
import fs from "fs"; const { glob } = require("glob");
import path from "path";
import os from "os";
import { glob } from "glob";
describe("validateAuthentication", () => { describe("validateAuthentication", () => {
it("should return null when only token is provided", () => { it("should return null when only token is provided", () => {
@@ -242,107 +197,3 @@ describe("glob functionality", () => {
expect(normalizedFiles.sort()).toEqual(expectedFiles); expect(normalizedFiles.sort()).toEqual(expectedFiles);
}); });
}); });
// ────────────────────────────────────────────────────────────────────────────
// applyOnAction
// ────────────────────────────────────────────────────────────────────────────
describe("applyOnAction", () => {
beforeEach(() => vi.clearAllMocks());
it("'ignore' + true → no core call, returns false", () => {
expect(applyOnAction("ignore", true, "msg")).toBe(false);
expect(core.warning).not.toHaveBeenCalled();
expect(core.setFailed).not.toHaveBeenCalled();
});
it("'warn' + true → core.warning() called with message, returns false", () => {
expect(applyOnAction("warn", true, "boom")).toBe(false);
expect(core.warning).toHaveBeenCalledWith("boom");
expect(core.setFailed).not.toHaveBeenCalled();
});
it("'fail' + true → core.setFailed() called with message, returns true", () => {
expect(applyOnAction("fail", true, "boom")).toBe(true);
expect(core.setFailed).toHaveBeenCalledWith("boom");
expect(core.warning).not.toHaveBeenCalled();
});
it("'fail' + false → no core call, returns false", () => {
expect(applyOnAction("fail", false, "boom")).toBe(false);
expect(core.setFailed).not.toHaveBeenCalled();
});
it("'warn' + false → no core call, returns false", () => {
expect(applyOnAction("warn", false, "msg")).toBe(false);
expect(core.warning).not.toHaveBeenCalled();
});
});
// ────────────────────────────────────────────────────────────────────────────
// postCode — monitor wiring
// ────────────────────────────────────────────────────────────────────────────
describe("postCode — monitor wiring", () => {
beforeEach(() => {
vi.clearAllMocks();
// Default core mocks
core.getInput.mockImplementation((name) => {
if (name === "monitor") return "0";
if (name === "token") return "test-token";
if (name === "branch") return "default";
if (name === "on_traceback") return "fail";
return "";
});
core.getBooleanInput.mockImplementation((name) => {
if (name === "rollback_on_failure") return false;
return false;
});
});
it("does not call monitorConsole when monitor=0 (default)", async () => {
// We just run postCode with monitor=0 and verify monitorConsole is not called.
await postCode();
expect(monitorConsole).not.toHaveBeenCalled();
});
it("rolls back to previous code when monitor detects a failure and rollback_on_failure is true", async () => {
// Setup inputs for monitor and rollback
core.getInput.mockImplementation((name) => {
if (name === "monitor") return "10";
if (name === "token") return "test-token";
if (name === "branch") return "default";
if (name === "on_traceback") return "fail";
return "";
});
core.getBooleanInput.mockImplementation((name) => {
if (name === "rollback_on_failure") return true;
return false;
});
// Simulate a failure in monitorConsole
monitorConsole.mockResolvedValueOnce({
sawTraceback: true, // Should trigger "fail" due to on_traceback=fail
sawErrorLog: false,
sawWarningLog: false,
});
await postCode();
// Verify rollback was performed
const mockApiInstance = new ScreepsAPI();
// `code.set` should be called twice:
// 1st time: uploading the new files
// 2nd time: rolling back to oldCode
expect(mockApiInstance.code.set).toHaveBeenCalledTimes(2);
expect(mockApiInstance.code.set).toHaveBeenNthCalledWith(2, "default", {
main: "old_code",
});
// Verify it called core.setFailed due to traceback
expect(core.setFailed).toHaveBeenCalledWith(
"Screeps console: traceback detected",
);
});
});
-633
View File
@@ -1,633 +0,0 @@
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
// ── mock @actions/core so tests never touch real CI outputs ─────────────────
vi.mock("@actions/core", () => ({
info: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
startGroup: vi.fn(),
endGroup: vi.fn(),
}));
import * as artifact from "@actions/artifact";
vi.mock("@actions/artifact", () => {
const mockClient = {
uploadArtifact: vi.fn().mockResolvedValue({}),
};
return {
create: vi.fn(() => mockClient),
};
});
import * as core from "@actions/core";
import fs from "fs";
import os from "os";
import path from "path";
import {
isOfficialServer,
buildSubscribePath,
detectTraceback,
detectWarning,
outputMultiline,
buildProgressMessage,
writeLogFile,
uploadLogArtifacts,
handleConsoleEvent,
monitorConsole,
} from "../monitor.js";
// ────────────────────────────────────────────────────────────────────────────
// Pure helpers
// ────────────────────────────────────────────────────────────────────────────
describe("isOfficialServer", () => {
it("returns true for screeps.com", () => {
expect(isOfficialServer("screeps.com")).toBe(true);
});
it("returns false for a private hostname", () => {
expect(isOfficialServer("builder64")).toBe(false);
});
it("returns false for an IP address", () => {
expect(isOfficialServer("192.168.1.10")).toBe(false);
});
});
describe("buildSubscribePath", () => {
it("returns console for official server (no shard provided)", () => {
expect(buildSubscribePath("screeps.com")).toBe("console");
});
it("returns console for private server (no shard provided)", () => {
expect(buildSubscribePath("builder64")).toBe("console");
});
it("returns console when shard is provided (official)", () => {
expect(buildSubscribePath("screeps.com", "shard3")).toBe("console");
});
it("returns console when shard is provided (private)", () => {
expect(buildSubscribePath("builder64", "myshard")).toBe("console");
});
});
describe("detectTraceback", () => {
it("returns true when error contains a stack frame line", () => {
const error =
"TypeError: Cannot read properties of undefined\n at Object.<anonymous> (main:1:42)";
expect(detectTraceback(error)).toBe(true);
});
it("returns false for a plain error message without stack frames", () => {
expect(detectTraceback("Something went wrong")).toBe(false);
});
it("returns false for null", () => {
expect(detectTraceback(null)).toBe(false);
});
it("returns false for undefined", () => {
expect(detectTraceback(undefined)).toBe(false);
});
it("returns false for an empty string", () => {
expect(detectTraceback("")).toBe(false);
});
});
describe("detectWarning", () => {
it("returns true for a line with orange font tag", () => {
const lines = ["<font color='orange'>WARN: low energy</font>"];
expect(detectWarning(lines)).toBe(true);
});
it("returns true for a line with yellow font tag", () => {
const lines = ['<font color="yellow">WARN: something</font>'];
expect(detectWarning(lines)).toBe(true);
});
it("returns false for a plain log line", () => {
expect(detectWarning(["Tick 123: harvesting"])).toBe(false);
});
it("returns false for an empty array", () => {
expect(detectWarning([])).toBe(false);
});
it("returns false for null", () => {
expect(detectWarning(null)).toBe(false);
});
it("returns true when only one line in a mixed array is a warning", () => {
const lines = ["normal line", "<font color='orange'>warn</font>"];
expect(detectWarning(lines)).toBe(true);
});
});
describe("outputMultiline", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("prefixes lines with shard when provided", () => {
outputMultiline("line1\nline2", "info", "shard0");
expect(core.info).toHaveBeenCalledWith("[shard0] line1");
expect(core.info).toHaveBeenCalledWith("[shard0] line2");
});
it("does not prefix when shard is missing", () => {
outputMultiline("line1", "info");
expect(core.info).toHaveBeenCalledWith("line1");
});
it("uses core.warning for level=warning", () => {
outputMultiline("warn", "warning", "s0");
expect(core.warning).toHaveBeenCalledWith("[s0] warn");
});
it("uses core.error for level=error", () => {
outputMultiline("err", "error", "s0");
expect(core.error).toHaveBeenCalledWith("[s0] err");
});
});
describe("buildProgressMessage", () => {
it("formats correctly at 0 elapsed", () => {
expect(buildProgressMessage(0, 50)).toBe("[Monitor] 0/50 ticks elapsed...");
});
it("formats correctly midway", () => {
expect(buildProgressMessage(25, 50)).toBe(
"[Monitor] 25/50 ticks elapsed...",
);
});
it("formats correctly at 100%", () => {
expect(buildProgressMessage(50, 50)).toBe(
"[Monitor] 50/50 ticks elapsed...",
);
});
});
describe("uploadLogArtifacts", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("instantiates client and calls uploadArtifact", async () => {
await uploadLogArtifacts(
["/path/to/shard0_console_log.txt"],
"custom-name",
);
expect(artifact.create().uploadArtifact).toHaveBeenCalledWith(
"custom-name",
["/path/to/shard0_console_log.txt"],
"/path/to",
{ continueOnError: true },
);
});
it("does nothing if filePaths is empty", async () => {
await uploadLogArtifacts([]);
expect(artifact.create().uploadArtifact).not.toHaveBeenCalled();
});
});
// ────────────────────────────────────────────────────────────────────────────
// handleConsoleEvent
// ────────────────────────────────────────────────────────────────────────────
describe("handleConsoleEvent", () => {
let state;
let shardBuffers;
beforeEach(() => {
vi.clearAllMocks();
state = { sawTraceback: false, sawErrorLog: false, sawWarningLog: false };
shardBuffers = {};
});
const makeEvent = (log = [], results = [], error = null) => ({
data: { messages: { log, results }, error },
});
it("calls core.info for each stdout line with shard prefix when logToFile=false", () => {
const event = {
data: {
shard: "shard0",
messages: { log: ["line1"], results: ["line2"] },
},
};
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
expect(core.info).toHaveBeenCalledWith("[shard0] line1");
expect(core.info).toHaveBeenCalledWith("[shard0] line2");
expect(Object.keys(shardBuffers)).toHaveLength(0);
});
it("buffers stdout when logToFile=true; does not call core.info", () => {
const event = {
data: { shard: "shard0", messages: { log: ["line1", "line2"] } },
};
handleConsoleEvent(event, { logToFile: true }, shardBuffers, state);
expect(core.info).not.toHaveBeenCalled();
expect(shardBuffers["shard0"]).toEqual(["line1", "line2"]);
});
it("includes results lines in output", () => {
const event = makeEvent([], ["result1"]);
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
expect(core.info).toHaveBeenCalledWith("result1");
});
it("calls core.error when error field is non-empty", () => {
const event = makeEvent([], [], "Script crashed");
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
expect(core.error).toHaveBeenCalledWith("Script crashed");
expect(state.sawErrorLog).toBe(true);
});
it("sets state.sawTraceback when error contains stack frames", () => {
const error = "TypeError: boom\n at Object.<anonymous> (main:1:1)";
const event = makeEvent([], [], error);
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
expect(state.sawTraceback).toBe(true);
expect(state.sawErrorLog).toBe(true);
});
it("does not set sawTraceback for plain error without stack frames", () => {
const event = makeEvent([], [], "Script error: low CPU");
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
expect(state.sawTraceback).toBe(false);
expect(state.sawErrorLog).toBe(true);
});
it("sets state.sawWarningLog and calls core.warning for warn lines", () => {
const event = makeEvent(["<font color='orange'>low energy</font>"]);
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
expect(state.sawWarningLog).toBe(true);
expect(core.warning).toHaveBeenCalled();
});
it("calls core.warning regardless of logToFile setting", () => {
const event = makeEvent(["<font color='orange'>warn line</font>"]);
handleConsoleEvent(event, { logToFile: true }, shardBuffers, state);
expect(core.warning).toHaveBeenCalled();
});
it("handles missing messages gracefully (no crash on empty event)", () => {
const event = { data: {} };
expect(() =>
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state),
).not.toThrow();
});
it("handles completely empty event gracefully", () => {
const event = {};
expect(() =>
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state),
).not.toThrow();
});
it("filters messages by shard when targetShard is provided", () => {
const event = { data: { shard: "shard0", messages: { log: ["msg0"] } } };
handleConsoleEvent(event, { shard: "shard1" }, shardBuffers, state);
expect(core.info).not.toHaveBeenCalled();
handleConsoleEvent(event, { shard: "shard0" }, shardBuffers, state);
expect(core.info).toHaveBeenCalledWith("[shard0] msg0");
});
it("does not filter when targetShard is undefined", () => {
const event = { data: { shard: "shard0", messages: { log: ["msg0"] } } };
handleConsoleEvent(event, {}, shardBuffers, state);
expect(core.info).toHaveBeenCalledWith("[shard0] msg0");
});
it("buffers messages separately for different shards", () => {
const event0 = { data: { shard: "shard0", messages: { log: ["msg0"] } } };
const event1 = { data: { shard: "shard1", messages: { log: ["msg1"] } } };
handleConsoleEvent(event0, { logToFile: true }, shardBuffers, state);
handleConsoleEvent(event1, { logToFile: true }, shardBuffers, state);
expect(shardBuffers["shard0"]).toEqual(["msg0"]);
expect(shardBuffers["shard1"]).toEqual(["msg1"]);
});
it("uses 'default' key when shard is missing in event", () => {
const event = { data: { messages: { log: ["msg"] } } };
handleConsoleEvent(event, { logToFile: true }, shardBuffers, state);
expect(shardBuffers["default"]).toEqual(["msg"]);
});
});
// ────────────────────────────────────────────────────────────────────────────
// writeLogFile
// ────────────────────────────────────────────────────────────────────────────
describe("writeLogFile", () => {
let tempDir;
let tempFile;
beforeEach(async () => {
tempDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "monitor-test-"),
);
tempFile = path.join(tempDir, "log.txt");
});
afterEach(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
it("writes all lines to file joined by newlines", async () => {
await writeLogFile(["line1", "line2", "line3"], tempFile);
const content = await fs.promises.readFile(tempFile, "utf8");
expect(content).toBe("line1\nline2\nline3");
});
it("writes an empty file when given an empty array", async () => {
await writeLogFile([], tempFile);
const content = await fs.promises.readFile(tempFile, "utf8");
expect(content).toBe("");
});
});
// ────────────────────────────────────────────────────────────────────────────
// monitorConsole — integration with mocked API
// ────────────────────────────────────────────────────────────────────────────
/**
* Builds a mock ScreepsAPI that:
* - api.time(shard) returns ticks from a list on each call
* - api.socket.connect() → resolves immediately
* - api.socket.subscribe() → resolves immediately (stores the callback)
* - api.socket.disconnect()→ no-op
*/
function buildMockApi({
ticks = [100, 101, 102, 103, 104, 105],
hostname = "builder64",
} = {}) {
let tickIndex = 0;
let consoleCallback = null;
const socket = {
connect: vi.fn().mockResolvedValue(undefined),
subscribe: vi.fn().mockImplementation((_path, cb) => {
consoleCallback = cb;
return Promise.resolve();
}),
disconnect: vi.fn(),
};
const api = {
opts: { hostname },
time: vi.fn().mockImplementation(() => {
const t = ticks[Math.min(tickIndex, ticks.length - 1)];
tickIndex++;
return Promise.resolve({ time: t });
}),
socket,
// Expose so tests can fire console events
_fireConsole: (eventData) => {
if (consoleCallback) {
consoleCallback({ data: eventData });
}
},
};
return api;
}
const BASE_OPTS = {
monitor: 3,
logToFile: false,
onTraceback: "fail",
onErrorLog: "warn",
onWarningLog: "ignore",
monitorInterval: 2,
hostname: "builder64",
};
describe("monitorConsole", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns all three flags false on a clean run", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
const result = await monitorConsole(api, BASE_OPTS);
expect(result).toEqual({
sawTraceback: false,
sawErrorLog: false,
sawWarningLog: false,
});
});
it("calls api.socket.connect() exactly once", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
await monitorConsole(api, BASE_OPTS);
expect(api.socket.connect).toHaveBeenCalledTimes(1);
});
it("calls api.socket.disconnect() after completion", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
await monitorConsole(api, BASE_OPTS);
expect(api.socket.disconnect).toHaveBeenCalledTimes(1);
});
it("subscribes to 'console' for a private server", async () => {
const api = buildMockApi({
hostname: "builder64",
ticks: [100, 101, 102, 103],
});
await monitorConsole(api, { ...BASE_OPTS, hostname: "builder64" });
expect(api.socket.subscribe).toHaveBeenCalledWith(
"console",
expect.any(Function),
);
});
it("subscribes to 'console' for the official server (default)", async () => {
const api = buildMockApi({
hostname: "screeps.com",
ticks: [100, 101, 102, 103],
});
await monitorConsole(api, { ...BASE_OPTS, hostname: "screeps.com" });
expect(api.socket.subscribe).toHaveBeenCalledWith(
"console",
expect.any(Function),
);
});
it("subscribes to 'console' even when custom shard is provided", async () => {
const api = buildMockApi({
hostname: "screeps.com",
ticks: [100, 101, 102, 103],
});
await monitorConsole(api, {
...BASE_OPTS,
hostname: "screeps.com",
shard: "shard3",
});
expect(api.socket.subscribe).toHaveBeenCalledWith(
"console",
expect.any(Function),
);
// Verify polling still uses shard3 for timing
expect(api.time).toHaveBeenCalledWith("shard3");
});
it("returns sawTraceback=true when a traceback event arrives", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
// Schedule console event before poll advances
setTimeout(() => {
api._fireConsole({
messages: { log: [], results: [] },
error: "TypeError: boom\n at Object.<anonymous> (main:1:1)",
});
}, 0);
const result = await monitorConsole(api, BASE_OPTS);
expect(result.sawTraceback).toBe(true);
});
it("returns sawErrorLog=true when an error (no traceback) event arrives", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
setTimeout(() => {
api._fireConsole({
messages: { log: [], results: [] },
error: "Script error: CPU limit exceeded",
});
}, 0);
const result = await monitorConsole(api, BASE_OPTS);
expect(result.sawErrorLog).toBe(true);
expect(result.sawTraceback).toBe(false);
});
it("returns sawWarningLog=true when a warn log line arrives", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
setTimeout(() => {
api._fireConsole({
messages: {
log: ["<font color='orange'>low energy</font>"],
results: [],
},
error: null,
});
}, 0);
const result = await monitorConsole(api, BASE_OPTS);
expect(result.sawWarningLog).toBe(true);
});
it("logs progress via core.info at monitorInterval boundaries when logToFile=true", async () => {
// ticks: start=100, then 101,102(interval),103(done at delta=3)
const api = buildMockApi({ ticks: [100, 100, 101, 102, 103] });
await monitorConsole(api, {
...BASE_OPTS,
logToFile: true,
monitorInterval: 2,
});
// Progress should be logged when elapsed reaches 2
const infoCalls = core.info.mock.calls.map((c) => c[0]);
expect(infoCalls.some((m) => m.includes("2/3"))).toBe(true);
});
it("calls api.socket.disconnect() even if an error occurs during polling", async () => {
const api = buildMockApi({ ticks: [100] });
// Make time() reject after first call
let calls = 0;
api.time = vi.fn().mockImplementation(() => {
calls++;
if (calls > 1) return Promise.reject(new Error("network error"));
return Promise.resolve({ time: 100 });
});
await expect(monitorConsole(api, BASE_OPTS)).rejects.toThrow(
"network error",
);
expect(api.socket.disconnect).toHaveBeenCalledTimes(1);
});
it("exits early if a traceback occurs and onTraceback='fail'", async () => {
// startTick=100, monitor=10 ticks.
// If it didn't exit early, it would call api.time() many times.
const api = buildMockApi({
ticks: [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110],
});
// Fire a traceback event after the first poll
setTimeout(() => {
api._fireConsole({
messages: { log: [], results: [] },
error:
"TypeError: fail-fast test\n at Object.<anonymous> (main:1:1)",
});
}, 100);
const result = await monitorConsole(api, {
...BASE_OPTS,
monitor: 10,
onTraceback: "fail",
});
expect(result.sawTraceback).toBe(true);
// We expect it to have called api.time fewer than 10 times (excluding the startTick call)
expect(api.time.mock.calls.length).toBeLessThan(10);
});
it("detects encoded tracebacks in log lines", async () => {
const api = buildMockApi({ ticks: [100, 101, 102] });
setTimeout(() => {
api._fireConsole({
messages: {
log: [
"Error: ReferenceError: a is not defined%0A at eval (eval at <anonymous> (_console1778948572008_0:1:46), <anonymous>:1:1)%0A at _console1778948572008_0:1:46",
],
results: [],
},
error: null,
});
}, 50);
const result = await monitorConsole(api, {
...BASE_OPTS,
onTraceback: "fail",
});
expect(result.sawTraceback).toBe(true);
expect(result.sawErrorLog).toBe(true);
});
it("creates separate log files for different shards when logToFile=true", async () => {
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
setTimeout(() => {
api._fireConsole({
shard: "shard0",
messages: { log: ["msg0"], results: [] },
error: null,
});
api._fireConsole({
shard: "shard1",
messages: { log: ["msg1"], results: [] },
error: null,
});
}, 50);
// Verify uploadArtifact was called with two files
await monitorConsole(api, {
...BASE_OPTS,
logToFile: true,
});
expect(artifact.create().uploadArtifact).toHaveBeenCalledWith(
"screeps-console-log",
expect.arrayContaining([
expect.stringContaining("shard0_console_log.txt"),
expect.stringContaining("shard1_console_log.txt"),
]),
expect.any(String),
{ continueOnError: true },
);
});
});
-38
View File
@@ -40,44 +40,6 @@ inputs:
git-replace: git-replace:
description: Allows for the overwrite of the "{{gitRef}}", "{{gitHash}}" and "{{deployTime}}" values in the file matching the file pattern. The file pattern will be combined with the prefix. description: Allows for the overwrite of the "{{gitRef}}", "{{gitHash}}" and "{{deployTime}}" values in the file matching the file pattern. The file pattern will be combined with the prefix.
required: false required: false
shard:
description: The Screeps shard to monitor (e.g. shard0, shard1). Defaults to shard0 on the official server.
required: false
monitor:
description: Number of game ticks to monitor the Screeps console after deploying (0 = disabled).
required: false
default: '0'
log_to_file:
description: 'Buffer stdout to an artifact file instead of streaming live (default: false). Errors/warnings always stream live.'
required: false
default: 'false'
on_traceback:
description: 'Action on JS traceback detection: ignore, warn, or fail (default: fail).'
required: false
default: fail
on_error_log:
description: 'Action on Screeps error-console output: ignore, warn, or fail (default: warn).'
required: false
default: warn
on_warning_log:
description: 'Action on console.warn output: ignore, warn, or fail (default: ignore).'
required: false
default: ignore
monitor_interval:
description: 'Print a progress update every N ticks when log_to_file=true (default: 10).'
required: false
default: '10'
rollback_on_failure:
description: 'Automatically rollback to previous code if the monitor detects failures. Requires monitor > 0. (default: false)'
required: false
default: 'false'
outputs:
saw_traceback:
description: true if a JS traceback was detected during monitoring.
saw_error_log:
description: true if the Screeps error console had output during monitoring.
saw_warning_log:
description: true if console.warn output was detected during monitoring.
runs: runs:
using: node20 using: node20
main: dist/index.js main: dist/index.js
+5 -16
View File
@@ -1,27 +1,16 @@
import{createRequire as A}from"module";var i={7038:(A,i,g)=>{var E;E={value:true};i.v=void 0;const p=g(918);function create(){return p.DefaultArtifactClient.create()}i.v=create},918:function(A,i,g){var E=this&&this.__createBinding||(Object.create?function(A,i,g,E){if(E===undefined)E=g;var p=Object.getOwnPropertyDescriptor(i,g);if(!p||("get"in p?!i.__esModule:p.writable||p.configurable)){p={enumerable:true,get:function(){return i[g]}}}Object.defineProperty(A,E,p)}:function(A,i,g,E){if(E===undefined)E=g;A[E]=i[g]});var p=this&&this.__setModuleDefault||(Object.create?function(A,i){Object.defineProperty(A,"default",{enumerable:true,value:i})}:function(A,i){A["default"]=i});var C=this&&this.__importStar||function(A){if(A&&A.__esModule)return A;var i={};if(A!=null)for(var g in A)if(g!=="default"&&Object.prototype.hasOwnProperty.call(A,g))E(i,A,g);p(i,A);return i};var f=this&&this.__awaiter||function(A,i,g,E){function adopt(A){return A instanceof g?A:new g(function(i){i(A)})}return new(g||(g=Promise))(function(g,p){function fulfilled(A){try{step(E.next(A))}catch(A){p(A)}}function rejected(A){try{step(E["throw"](A))}catch(A){p(A)}}function step(A){A.done?g(A.value):adopt(A.value).then(fulfilled,rejected)}step((E=E.apply(A,i||[])).next())})};Object.defineProperty(i,"__esModule",{value:true});i.DefaultArtifactClient=void 0;const Q=C(g(9910));const B=g(9367);const I=g(8950);const y=g(2669);const b=g(9400);const D=g(3439);const R=g(3886);const k=g(1124);const S=g(6928);class DefaultArtifactClient{static create(){return new DefaultArtifactClient}uploadArtifact(A,i,g,E){return f(this,void 0,void 0,function*(){Q.info(`Starting artifact upload\nFor more detailed logs during the artifact upload process, enable step-debugging: https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging`);(0,b.checkArtifactName)(A);const p=(0,B.getUploadSpecification)(A,g,i);const C={artifactName:A,artifactItems:[],size:0,failedItems:[]};const f=new I.UploadHttpClient;if(p.length===0){Q.warning(`No files found that can be uploaded`)}else{const i=yield f.createArtifactInFileContainer(A,E);if(!i.fileContainerResourceUrl){Q.debug(i.toString());throw new Error("No URL provided by the Artifact Service to upload an artifact to")}Q.debug(`Upload Resource URL: ${i.fileContainerResourceUrl}`);Q.info(`Container for artifact "${A}" successfully created. Starting upload of file(s)`);const g=yield f.uploadArtifactToFileContainer(i.fileContainerResourceUrl,p,E);Q.info(`File upload process has finished. Finalizing the artifact upload`);yield f.patchArtifactSize(g.totalSize,A);if(g.failedItems.length>0){Q.info(`Upload finished. There were ${g.failedItems.length} items that failed to upload`)}else{Q.info(`Artifact has been finalized. All files have been successfully uploaded!`)}Q.info(`\nThe raw size of all the files that were specified for upload is ${g.totalSize} bytes\nThe size of all the files that were uploaded is ${g.uploadSize} bytes. This takes into account any gzip compression used to reduce the upload size, time and storage\n\nNote: The size of downloaded zips can differ significantly from the reported size. For more information see: https://github.com/actions/upload-artifact#zipped-artifact-downloads \r\n`);C.artifactItems=p.map(A=>A.absoluteFilePath);C.size=g.uploadSize;C.failedItems=g.failedItems}return C})}downloadArtifact(A,i,g){return f(this,void 0,void 0,function*(){const E=new D.DownloadHttpClient;const p=yield E.listArtifacts();if(p.count===0){throw new Error(`Unable to find any artifacts for the associated workflow`)}const C=p.value.find(i=>i.name===A);if(!C){throw new Error(`Unable to find an artifact with the name: ${A}`)}const f=yield E.getContainerItems(C.name,C.fileContainerResourceUrl);if(!i){i=(0,k.getWorkSpaceDirectory)()}i=(0,S.normalize)(i);i=(0,S.resolve)(i);const B=(0,R.getDownloadSpecification)(A,f.value,i,(g===null||g===void 0?void 0:g.createArtifactFolder)||false);if(B.filesToDownload.length===0){Q.info(`No downloadable files were found for the artifact: ${C.name}`)}else{yield(0,y.createDirectoriesForArtifact)(B.directoryStructure);Q.info("Directory structure has been set up for the artifact");yield(0,y.createEmptyFilesForArtifact)(B.emptyFilesToCreate);yield E.downloadSingleArtifact(B.filesToDownload)}return{artifactName:A,downloadPath:B.rootDownloadLocation}})}downloadAllArtifacts(A){return f(this,void 0,void 0,function*(){const i=new D.DownloadHttpClient;const g=[];const E=yield i.listArtifacts();if(E.count===0){Q.info("Unable to find any artifacts for the associated workflow");return g}if(!A){A=(0,k.getWorkSpaceDirectory)()}A=(0,S.normalize)(A);A=(0,S.resolve)(A);let p=0;while(p<E.count){const C=E.value[p];p+=1;Q.info(`starting download of artifact ${C.name} : ${p}/${E.count}`);const f=yield i.getContainerItems(C.name,C.fileContainerResourceUrl);const B=(0,R.getDownloadSpecification)(C.name,f.value,A,true);if(B.filesToDownload.length===0){Q.info(`No downloa Line truncated (()=>{var __webpack_modules__={6136:(module,__unused_webpack_exports,__nccwpck_require__)=>{const{ScreepsAPI:ScreepsAPI}=__nccwpck_require__(9546);const core=__nccwpck_require__(7484);const fs=__nccwpck_require__(9896);const{glob:glob}=__nccwpck_require__(1363);const path=__nccwpck_require__(6928);function replacePlaceholders(e,t){const s=(new Date).toISOString();return e.replace(/{{gitHash}}/g,process.env.GITHUB_SHA).replace(/{{gitRef}}/g,process.env.GITHUB_REF).replace(/{{deployTime}}/g,s).replace(/{{hostname}}/g,t)}async function readReplaceAndWriteFiles(e,t,s){const i=t?path.join(t,e):e;const n=await glob(i);let r=n.map((e=>fs.promises.readFile(e,"utf8").then((t=>{t=replacePlaceholders(t,s);return fs.promises.writeFile(e,t)}))));await Promise.all(r);return n}async function readFilesIntoDict(e,t){const s=t?path.join(t,e):e;const i=await glob(s);let n={};let r=i.map((e=>fs.promises.readFile(e,"utf8").then((s=>{let i=e;if(t&&e.startsWith(t)){i=i.slice(t.length)}i=path.basename(i,path.extname(i));n[i]=s}))));await Promise.all(r);return n}function validateAuthentication(e,t,s){if(e){if(t||s){return"Token is defined along with username and/or password."}}else{if(!t&&!s){return"Neither token nor password and username are defined."}if(t&&!s){return"Username is defined but no password is provided."}if(!t&&s){return"Password is defined but no username is provided."}}return null}async function postCode(){const e=core.getInput("protocol")||"https";const t=core.getInput("hostname")||"screeps.com";const s=core.getInput("port")||"443";const i=core.getInput("path")||"/";const n=core.getInput("token")||undefined;const r=core.getInput("username")||undefined;const o=core.getInput("password")||undefined;const a=core.getInput("source-prefix");const A=core.getInput("pattern")||"*.js";const c=core.getInput("branch")||"default";const l=core.getInput("git-replace")||null;if(l){await readReplaceAndWriteFiles(l,a,t)}const u=await readFilesIntoDict(A,a);core.info(`Trying to upload the following files to ${c}:`);Object.keys(u).forEach((e=>{core.info(`Key: ${e}`)}));const p={token:n,username:r,password:o,protocol:e,hostname:t,port:s,path:i};core.info("login_arguments:");core.info(JSON.stringify(p,null,2));const h=validateAuthentication(n,r,o);if(h){core.error(h);return}let d=new ScreepsAPI(p);if(n){const e=await d.code.set(c,u);core.info(JSON.stringify(e,null,2));console.log(`Code set successfully to ${c}`)}else{core.info(`Logging in as user ${r}`);await Promise.resolve().then((()=>d.auth(r,o,p))).then((()=>{d.code.set(c,u)})).then((()=>{console.log(`Code set successfully to ${c}`)})).catch((e=>{console.error("Error:",e)}))}}if(require.main===require.cache[eval("__filename")]){postCode()}module.exports={validateAuthentication:validateAuthentication,replacePlaceholders:replacePlaceholders,postCode:postCode,readReplaceAndWriteFiles:readReplaceAndWriteFiles,readFilesIntoDict:readFilesIntoDict}},4914:function(e,t,s){"use strict";var i=this&&this.__createBinding||(Object.create?function(e,t,s,i){if(i===undefined)i=s;var n=Object.getOwnPropertyDescriptor(t,s);if(!n||("get"in n?!t.__esModule:n.writable||n.configurable)){n={enumerable:true,get:function(){return t[s]}}}Object.defineProperty(e,i,n)}:function(e,t,s,i){if(i===undefined)i=s;e[i]=t[s]});var n=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var r=this&&this.__importStar||function(){var ownKeys=function(e){ownKeys=Object.getOwnPropertyNames||function(e){var t=[];for(var s in e)if(Object.prototype.hasOwnProperty.call(e,s))t[t.length]=s;return t};return ownKeys(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var s=ownKeys(e),r=0;r<s.length;r++)if(s[r]!=="default")i(t,e,s[r]);n(t,e);return t}}();Object.defineProperty(t,"__esModule",{value:true});t.issueCommand=issueCommand;t.issue=issue;const o=r(s(857));const a=s(302);function issueCommand(e,t,s){const i=new Command(e,t,s);process.stdout.write(i.toString()+o.EOL)}function issue(e,t=""){issueCommand(e,{},t)}const A="::";class Command{constructor(e,t,s){if(!e){e="missing.command"}this.command=e;this.properties=t;this.message=s}toString(){let e=A+this.command;if(this.properties&&Object.keys(this.properties).length>0){e+=" ";let t=true;for(const s in this.properties){if(this.properties.hasOwnProperty(s)){const i=this.properties[s];if(i){if(t){t=false}else{e+=","}e+=`${s}=${escapeProperty(i)}`}}}}e+=`${A}${escapeData(this.message)}`;return e}}function escapeData(e){return(0,a.toCommandValue)(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return(0,a.toCommandValue)(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A").replace(/:/g,"%3A").replace(/,/g,"%2C")}},7484:function(e,t,s){"use strict";var i=this&&this.__createBinding||(Object.create?function(e,t,s,i){if(i===undefined)i=s;var n=Object.getOwnPropertyDescriptor(t,s);if(!n||("get"in n?!t.__esModule:nLine truncated
/*! /*!
* mime-db * mime-db
* Copyright(c) 2014 Jonathan Ong * Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015-2022 Douglas Christopher Wilson * Copyright(c) 2015-2022 Douglas Christopher Wilson
* MIT Licensed * MIT Licensed
*/ */
A.exports=g(7080)},3674:(A,i,g)=>{ e.exports=s(1813)},4096:(e,t,s)=>{"use strict";
/*! /*!
* mime-types * mime-types
* Copyright(c) 2014 Jonathan Ong * Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015 Douglas Christopher Wilson * Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed * MIT Licensed
*/ */var i=s(9829);var n=s(6928).extname;var r=/^\s*([^;\s]*)(?:;|\s|$)/;var o=/^text\//i;t.charset=charset;t.charsets={lookup:charset};t.contentType=contentType;t.extension=extension;t.extensions=Object.create(null);t.lookup=lookup;t.types=Object.create(null);populateMaps(t.extensions,t.types);function charset(e){if(!e||typeof e!=="string"){return false}var t=r.exec(e);var s=t&&i[t[1].toLowerCase()];if(s&&s.charset){return s.charset}if(t&&o.test(t[1])){return"UTF-8"}return false}function contentType(e){if(!e||typeof e!=="string"){return false}var s=e.indexOf("/")===-1?t.lookup(e):e;if(!s){return false}if(s.indexOf("charset")===-1){var i=t.charset(s);if(i)s+="; charset="+i.toLowerCase()}return s}function extension(e){if(!e||typeof e!=="string"){return false}var s=r.exec(e);var i=s&&t.extensions[s[1].toLowerCase()];if(!i||!i.length){return false}return i[0]}function lookup(e){if(!e||typeof e!=="string"){return false}var s=n("x."+e).toLowerCase().substr(1);if(!s){return false}return t.types[s]||false}function populateMaps(e,t){var s=["nginx","apache",undefined,"iana"];Object.keys(i).forEach((function forEachMimeType(n){var r=i[n];var o=r.extensions;if(!o||!o.length){return}e[n]=o;for(var a=0;a<o.length;a++){var A=o[a];if(t[A]){var c=s.indexOf(i[t[A]].source);var l=s.indexOf(r.source);if(t[A]!=="application/octet-stream"&&(c>l||c===l&&t[A].substr(0,12)==="application/")){continue}}t[A]=n}}))}},744:e=>{var t=1e3;var s=t*60;var i=s*60;var n=i*24;var r=n*7;var o=n*365.25;e.exports=function(e,t){t=t||{};var s=typeof e;if(s==="string"&&e.length>0){return parse(e)}else if(s==="number"&&isFinite(e)){return t.long?fmtLong(e):fmtShort(e)}throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))};function parse(e){e=String(e);if(e.length>100){return}var a=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(!a){return}var A=parseFloat(a[1]);var c=(a[2]||"ms").toLowerCase();switch(c){case"years":case"year":case"yrs":case"yr":case"y":return A*o;case"weeks":case"week":case"w":return A*r;case"days":case"day":case"d":return A*n;case"hours":case"hour":case"hrs":case"hr":case"h":return A*i;case"minutes":case"minute":case"mins":case"min":case"m":return A*s;case"seconds":case"second":case"secs":case"sec":case"s":return A*t;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return A;default:return undefined}}function fmtShort(e){var r=Math.abs(e);if(r>=n){return Math.round(e/n)+"d"}if(r>=i){return Math.round(e/i)+"h"}if(r>=s){return Math.round(e/s)+"m"}if(r>=t){return Math.round(e/t)+"s"}return e+"ms"}function fmtLong(e){var r=Math.abs(e);if(r>=n){return plural(e,r,n,"day")}if(r>=i){return plural(e,r,i,"hour")}if(r>=s){return plural(e,r,s,"minute")}if(r>=t){return plural(e,r,t,"second")}return e+" ms"}function plural(e,t,s,i){var n=t>=s*1.5;return Math.round(e/s)+" "+i+(n?"s":"")}},7777:(e,t,s)=>{"use strict";var i=s(7016).parse;var n={ftp:21,gopher:70,http:80,https:443,ws:80,wss:443};var r=String.prototype.endsWith||function(e){return e.length<=this.length&&this.indexOf(e,this.length-e.length)!==-1};function getProxyForUrl(e){var t=typeof e==="string"?i(e):e||{};var s=t.protocol;var r=t.host;var o=t.port;if(typeof r!=="string"||!r||typeof s!=="string"){return""}s=s.split(":",1)[0];r=r.replace(/:\d*$/,"");o=parseInt(o)||n[s]||0;if(!shouldProxy(r,o)){return""}var a=getEnv("npm_config_"+s+"_proxy")||getEnv(s+"_proxy")||getEnv("npm_config_proxy")||getEnv("all_proxy");if(a&&a.indexOf("://")===-1){a=s+"://"+a}return a}function shouldProxy(e,t){var s=(getEnv("npm_config_no_proxy")||getEnv("no_proxy")).toLowerCase();if(!s){return true}if(s==="*"){return false}return s.split(/[,\s]/).every((function(s){if(!s){return true}var i=s.match(/^(.+):(\d+)$/);var n=i?i[1]:s;var o=i?parseInt(i[2]):0;if(o&&o!==t){return true}if(!/^[.*]/.test(n)){return e!==n}if(n.charAt(0)==="*"){n=n.slice(1)}return!r.call(e,n)}))}function getEnv(e){return process.env[e.toLowerCase()]||process.env[e.toUpperCase()]||""}t.T=getProxyForUrl},9546:(e,t,s)=>{"use strict";Object.defineProperty(t,"__esModule",{value:true});const i=s(1354);const n=s(7016);const r=s(4434);const o=s(2830);const a=s(3106);const A=s(4922);const c=s(9023);const l=s(9896);const u=s(9557);const p=s(6928);function _interopDefaultLegacy(e){return e&&typeof e==="object"&&"default"in e?e:{default:e}}const h=_interopDefaultLegacy(i);const d=_interopDefaultLegacy(n);const f=_interopDefaultLegacy(o);const g=_interopDefaultLegacy(a);const E=_interopDefaultLegacy(A);const C=_interopDefaultLegacy(c);const Q=_interopDefaultLegacy(l);const B=_interopDefaultLegacy(u);const I=_interopDefaultLegacy(p);const y=f["default"]("screepsapi:socket");const b={reconnect:true,resubscribe:true,keepAlive:true,maxRetries:10,maxRetryDelay:60*1e3};class Socket extends r.EventEmitter{constructor(e){super();this.api=e;this.opts=Object.assign({},b);this.on("error",(()=>{}));this.reset();this.oLine truncated
var E=g(8975);var p=g(6928).extname;var C=/^\s*([^;\s]*)(?:;|\s|$)/;var f=/^text\//i;i.charset=charset;i.charsets={lookup:charset};i.contentType=contentType;i.extension=extension;i.extensions=Object.create(null);i.lookup=lookup;i.types=Object.create(null);populateMaps(i.extensions,i.types);function charset(A){if(!A||typeof A!=="string"){return false}var i=C.exec(A);var g=i&&E[i[1].toLowerCase()];if(g&&g.charset){return g.charset}if(i&&f.test(i[1])){return"UTF-8"}return false}function contentType(A){if(!A||typeof A!=="string"){return false}var g=A.indexOf("/")===-1?i.lookup(A):A;if(!g){return false}if(g.indexOf("charset")===-1){var E=i.charset(g);if(E)g+="; charset="+E.toLowerCase()}return g}function extension(A){if(!A||typeof A!=="string"){return false}var g=C.exec(A);var E=g&&i.extensions[g[1].toLowerCase()];if(!E||!E.length){return false}return E[0]}function lookup(A){if(!A||typeof A!=="string"){return false}var g=p("x."+A).toLowerCase().substr(1);if(!g){return false}return i.types[g]||false}function populateMaps(A,i){var g=["nginx","apache",undefined,"iana"];Object.keys(E).forEach(function forEachMimeType(p){var C=E[p];var f=C.extensions;if(!f||!f.length){return}A[p]=f;for(var Q=0;Q<f.length;Q++){var B=f[Q];if(i[B]){var I=g.indexOf(E[i[B]].source);var y=g.indexOf(C.source);if(i[B]!=="application/octet-stream"&&(I>y||I===y&&i[B].substr(0,12)==="application/")){continue}}i[B]=p}})}},4023:A=>{var i=1e3;var g=i*60;var E=g*60;var p=E*24;var C=p*7;var f=p*365.25;A.exports=function(A,i){i=i||{};var g=typeof A;if(g==="string"&&A.length>0){return parse(A)}else if(g==="number"&&isFinite(A)){return i.long?fmtLong(A):fmtShort(A)}throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(A))};function parse(A){A=String(A);if(A.length>100){return}var Q=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(A);if(!Q){return}var B=parseFloat(Q[1]);var I=(Q[2]||"ms").toLowerCase();switch(I){case"years":case"year":case"yrs":case"yr":case"y":return B*f;case"weeks":case"week":case"w":return B*C;case"days":case"day":case"d":return B*p;case"hours":case"hour":case"hrs":case"hr":case"h":return B*E;case"minutes":case"minute":case"mins":case"min":case"m":return B*g;case"seconds":case"second":case"secs":case"sec":case"s":return B*i;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return B;default:return undefined}}function fmtShort(A){var C=Math.abs(A);if(C>=p){return Math.round(A/p)+"d"}if(C>=E){return Math.round(A/E)+"h"}if(C>=g){return Math.round(A/g)+"m"}if(C>=i){return Math.round(A/i)+"s"}return A+"ms"}function fmtLong(A){var C=Math.abs(A);if(C>=p){return plural(A,C,p,"day")}if(C>=E){return plural(A,C,E,"hour")}if(C>=g){return plural(A,C,g,"minute")}if(C>=i){return plural(A,C,i,"second")}return A+" ms"}function plural(A,i,g,E){var p=i>=g*1.5;return Math.round(A/g)+" "+E+(p?"s":"")}},4147:(A,i,g)=>{var E=g(7016).parse;var p={ftp:21,gopher:70,http:80,https:443,ws:80,wss:443};var C=String.prototype.endsWith||function(A){return A.length<=this.length&&this.indexOf(A,this.length-A.length)!==-1};function getProxyForUrl(A){var i=typeof A==="string"?E(A):A||{};var g=i.protocol;var C=i.host;var f=i.port;if(typeof C!=="string"||!C||typeof g!=="string"){return""}g=g.split(":",1)[0];C=C.replace(/:\d*$/,"");f=parseInt(f)||p[g]||0;if(!shouldProxy(C,f)){return""}var Q=getEnv("npm_config_"+g+"_proxy")||getEnv(g+"_proxy")||getEnv("npm_config_proxy")||getEnv("all_proxy");if(Q&&Q.indexOf("://")===-1){Q=g+"://"+Q}return Q}function shouldProxy(A,i){var g=(getEnv("npm_config_no_proxy")||getEnv("no_proxy")).toLowerCase();if(!g){return true}if(g==="*"){return false}return g.split(/[,\s]/).every(function(g){if(!g){return true}var E=g.match(/^(.+):(\d+)$/);var p=E?E[1]:g;var f=E?parseInt(E[2]):0;if(f&&f!==i){return true}if(!/^[.*]/.test(p)){return A!==p}if(p.charAt(0)==="*"){p=p.slice(1)}return!C.call(A,p)})}function getEnv(A){return process.env[A.toLowerCase()]||process.env[A.toUpperCase()]||""}i.T=getProxyForUrl},4398:(A,i,g)=>{var E;E={value:true};const p=g(9105);const C=g(7016);const f=g(4434);const Q=g(1141);const B=g(3106);const I=g(6550);const y=g(9023);const b=g(9896);const D=g(7967);const R=g(6928);function _interopDefaultLegacy(A){return A&&typeof A==="object"&&"default"in A?A:{default:A}}const k=_interopDefaultLegacy(p);const S=_interopDefaultLegacy(C);const v=_interopDefaultLegacy(Q);const F=_interopDefaultLegacy(B);const x=_interopDefaultLegacy(I);const U=_interopDefaultLegacy(y);const L=_interopDefaultLegacy(b);const M=_interopDefaultLegacy(D);const T=_interopDefaultLegacy(R);const H=v["default"]("screepsapi:socket");const G={reconnect:true,resubscribe:true,keepAlive:true,maxRetries:10,maxRetryDelay:60*1e3};class Socket extends f.EventEmitter{constructor(A){super();this.api=A;this.opts=Object.assign({},G);this.on("error",()=>{});this.reset();this.on("auth",A=>{if(A.data.status==="ok"){while(this.__queue.length)Line truncated /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */;const escape=e=>e.replace(/\n/g,"%0A").replace(/\r/g,"%0D").replace(/"/g,"%22");const normalizeLinefeeds=e=>e.replace(/\r?\n|\r/g,"\r\n");const i=[];const n=new Uint8Array([13,10]);l=0;let r=false;for(const[t,o]of e){if(typeof o==="string"){const e=b.encode(s+`; name="${escape(normalizeLinefeeds(t))}"`+`\r\n\r\n${normalizeLinefeeds(o)}\r\n`);i.push(e);l+=e.byteLength}else{const e=b.encode(`${s}; name="${escape(normalizeLinefeeds(t))}"`+(o.name?`; filename="${escape(o.name)}"`:"")+"\r\n"+`Content-Type: ${o.type||"application/octet-stream"}\r\n\r\n`);i.push(e,o,n);if(typeof o.size==="number"){l+=e.byteLength+o.size+n.byteLength}else{r=true}}}const o=b.encode(`--${t}--\r\n`);i.push(o);l+=o.byteLength;if(r){l=null}c=e;A=async function*(){for(const e of i){if(e.stream){yield*e.stream()}else{yield e}}};u=`multipart/form-data; boundary=${t}`}else if(r(e)){c=e;l=e.size;if(e.type){u=e.type}}else if(typeof e[Symbol.asyncIterator]==="function"){if(t){throw new TypeError("keepalive")}if(i.isDisturbed(e)||e.locked){throw new TypeError("Response body object should not be disturbed or locked")}s=e instanceof ReadableStream?e:n(e)}if(typeof c==="string"||i.isBuffer(c)){l=Buffer.byteLength(c)}if(A!=null){let t;s=new ReadableStream({async start(){t=A(e)[Symbol.asyncIterator]()},async pull(e){const{value:i,done:n}=await t.next();if(n){queueMicrotask((()=>{e.close();e.byobRequest?.respond(0)}))}else{if(!E(s)){const t=new Uint8Array(i);if(t.byteLength){e.enqueue(t)}}}return e.desiredSize>0},async cancel(e){await t.return()},type:"bytes"})}const p={stream:s,source:c,length:l};return[p,u]}function safelyExtractBody(e,t=false){if(e instanceof ReadableStream){g(!i.isDisturbed(e),"The body has already been consumed.");g(!e.locked,"The stream is locked.")}return extractBody(e,t)}function cloneBody(e,t){const[s,i]=t.stream.tee();t.stream=s;return{stream:i,length:t.length,source:t.source}}function throwIfAborted(e){if(e.aborted){throw new DOMException("The operation was aborted.","AbortError")}}function bodyMixinMethods(e){const t={blob(){return consumeBody(this,(e=>{let t=bodyMimeType(this);if(t===null){t=""}else if(t){t=B(t)}return new f([e],{type:t})}),e)},arrayBuffer(){return consumeBody(this,(e=>new Uint8Array(e).buffer),e)},text(){return consumeBody(this,u,e)},json(){return consumeBody(this,parseJSONFromBytes,e)},formData(){return consumeBody(this,(e=>{const t=bodyMimeType(this);if(t!==null){switch(t.essence){case"multipart/form-data":{const s=I(e,t);if(s==="failure"){throw new TypeError("Failed to parse body as FormData.")}const i=new p;i[h]=s;return i}case"application/x-www-form-urlencoded":{const t=new URLSearchParams(e.toString());const s=new p;for(const[e,i]of t){s.append(e,i)}return s}}}throw new TypeError('Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".')}),e)},bytes(){return consumeBody(this,(e=>new Uint8Array(e)),e)}};return t}function mixinBody(e){Object.assign(e.prototype,bodyMixinMethods(e))}async function consumeBody(e,t,s){d.brandCheck(e,s);if(bodyUnusable(e)){throw new TypeError("Body is unusable: Body has already been read")}throwIfAborted(e[h]);const i=A();const errorSteps=e=>i.reject(e);const successSteps=e=>{try{i.resolve(t(e))}catch(e){errorSteps(e)}};if(e[h].body==null){successSteps(Buffer.allocUnsafe(0));return i.promise}await c(e[h].body,successSteps,errorSteps);return i.promise}function bodyUnusable(e){const t=e[h].body;return t!=null&&(t.stream.locked||i.isDisturbed(t.stream))}function parseJSONFromBytes(e){return JSON.parse(u(e))}function bodyMimeType(e){const t=e[h].headersList;const s=l(t);if(s==="failure"){return null}return s}e.exports={extractBody:extractBody,safelyExtractBody:safelyExtractBody,cloneBody:cloneBody,mixinBody:mixinBody,streamRegistry:v,hasFinalizationRegistry:w,bodyUnusable:bodyUnusable}},4495:e=>{"use strict";const t=["GET","HEAD","POST"];const s=new Set(t);const i=[101,204,205,304];const n=[301,302,303,307,308];const r=new Set(n);const o=["1","7","9","11","13","15","17","19","20","21","22","23","25","37","42","43","53","69","77","79","87","95","101","102","103","104","109","110","111","113","115","117","119","123","135","137","139","143","161","179","389","427","465","512","513","514","515","526","530","531","532","540","548","554","556","563","587","601","636","989","990","993","995","1719","1720","1723","2049","3659","4045","4190","5060","5061","6000","6566","6665","6666","6667","6668","6669","6679","6697","10080"];const a=new Set(o);const A=["","no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"];const c=new Set(A);const l=["follow","manual","error"];const u=["GET","HEAD","OPTIONS","TRACE"];const p=new Set(u);const h=["navigate","same-origin","no-cors","cors"];const d=["omit","same-origin","include"];const f=["default","no-store","reload","no-caLine truncated
/*! /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */a[o-4]=s[0];a[o-3]=s[1];a[o-2]=s[2];a[o-1]=s[3];a[1]=r;if(r===126){a.writeUInt16BE(n,2)}else if(r===127){a[2]=a[3]=0;a.writeUIntBE(n,4,6)}a[1]|=128;for(let e=0;e<n;++e){a[o+e]=t[e]^s[e&3]}return a}}e.exports={WebsocketFrameSend:WebsocketFrameSend}},9469:(e,t,s)=>{"use strict";const{createInflateRaw:i,Z_DEFAULT_WINDOWBITS:n}=s(8522);const{isValidClientWindowBits:r}=s(8625);const o=Buffer.from([0,0,255,255]);const a=Symbol("kBuffer");const A=Symbol("kLength");class PerMessageDeflate{#Z;#h={};constructor(e){this.#h.serverNoContextTakeover=e.has("server_no_context_takeover");this.#h.serverMaxWindowBits=e.get("server_max_window_bits")}decompress(e,t,s){if(!this.#Z){let e=n;if(this.#h.serverMaxWindowBits){if(!r(this.#h.serverMaxWindowBits)){s(new Error("Invalid server_max_window_bits"));return}e=Number.parseInt(this.#h.serverMaxWindowBits)}this.#Z=i({windowBits:e});this.#Z[a]=[];this.#Z[A]=0;this.#Z.on("data",(e=>{this.#Z[a].push(e);this.#Z[A]+=e.length}));this.#Z.on("error",(e=>{this.#Z=null;s(e)}))}this.#Z.write(e);if(t){this.#Z.write(o)}this.#Z.flush((()=>{const e=Buffer.concat(this.#Z[a],this.#Z[A]);this.#Z[a].length=0;this.#Z[A]=0;s(null,e)}))}}e.exports={PerMessageDeflate:PerMessageDeflate}},1652:(e,t,s)=>{"use strict";const{Writable:i}=s(7075);const n=s(4589);const{parserStates:r,opcodes:o,states:a,emptyBuffer:A,sentCloseFrameState:c}=s(736);const{kReadyState:l,kSentClose:u,kResponse:p,kReceivedClose:h}=s(1216);const{channels:d}=s(2414);const{isValidStatusCode:f,isValidOpcode:g,failWebsocketConnection:E,websocketMessageReceived:C,utf8Decode:Q,isControlFrame:B,isTextBinaryFrame:I,isContinuationFrame:y}=s(8625);const{WebsocketFrameSend:b}=s(3264);const{closeWebSocketConnection:w}=s(6897);const{PerMessageDeflate:v}=s(9469);class ByteParser extends i{#K=[];#X=0;#$=false;#Q=r.INFO;#ee={};#te=[];#se;constructor(e,t){super();this.ws=e;this.#se=t==null?new Map:t;if(this.#se.has("permessage-deflate")){this.#se.set("permessage-deflate",new v(t))}}_write(e,t,s){this.#K.push(e);this.#X+=e.length;this.#$=true;this.run(s)}run(e){while(this.#$){if(this.#Q===r.INFO){if(this.#X<2){return e()}const t=this.consume(2);const s=(t[0]&128)!==0;const i=t[0]&15;const n=(t[1]&128)===128;const a=!s&&i!==o.CONTINUATION;const A=t[1]&127;const c=t[0]&64;const l=t[0]&32;const u=t[0]&16;if(!g(i)){E(this.ws,"Invalid opcode received");return e()}if(n){E(this.ws,"Frame cannot be masked");return e()}if(c!==0&&!this.#se.has("permessage-deflate")){E(this.ws,"Expected RSV1 to be clear.");return}if(l!==0||u!==0){E(this.ws,"RSV1, RSV2, RSV3 must be clear");return}if(a&&!I(i)){E(this.ws,"Invalid frame type was fragmented.");return}if(I(i)&&this.#te.length>0){E(this.ws,"Expected continuation frame");return}if(this.#ee.fragmented&&a){E(this.ws,"Fragmented frame exceeded 125 bytes.");return}if((A>125||a)&&B(i)){E(this.ws,"Control frame either too large or fragmented");return}if(y(i)&&this.#te.length===0&&!this.#ee.compressed){E(this.ws,"Unexpected continuation frame");return}if(A<=125){this.#ee.payloadLength=A;this.#Q=r.READ_DATA}else if(A===126){this.#Q=r.PAYLOADLENGTH_16}else if(A===127){this.#Q=r.PAYLOADLENGTH_64}if(I(i)){this.#ee.binaryType=i;this.#ee.compressed=c!==0}this.#ee.opcode=i;this.#ee.masked=n;this.#ee.fin=s;this.#ee.fragmented=a}else if(this.#Q===r.PAYLOADLENGTH_16){if(this.#X<2){return e()}const t=this.consume(2);this.#ee.payloadLength=t.readUInt16BE(0);this.#Q=r.READ_DATA}else if(this.#Q===r.PAYLOADLENGTH_64){if(this.#X<8){return e()}const t=this.consume(8);const s=t.readUInt32BE(0);if(s>2**31-1){E(this.ws,"Received payload length > 2^31 bytes.");return}const i=t.readUInt32BE(4);this.#ee.payloadLength=(s<<8)+i;this.#Q=r.READ_DATA}else if(this.#Q===r.READ_DATA){if(this.#X<this.#ee.payloadLength){return e()}const t=this.consume(this.#ee.payloadLength);if(B(this.#ee.opcode)){this.#$=this.parseControlFrame(t);this.#Q=r.INFO}else{if(!this.#ee.compressed){this.#te.push(t);if(!this.#ee.fragmented&&this.#ee.fin){const e=Buffer.concat(this.#te);C(this.ws,this.#ee.binaryType,e);this.#te.length=0}this.#Q=r.INFO}else{this.#se.get("permessage-deflate").decompress(t,this.#ee.fin,((t,s)=>{if(t){w(this.ws,1007,t.message,t.message.length);return}this.#te.push(s);if(!this.#ee.fin){this.#Q=r.INFO;this.#$=true;this.run(e);return}C(this.ws,this.#ee.binaryType,Buffer.concat(this.#te));this.#$=true;this.#Q=r.INFO;this.#te.length=0;this.run(e)}));this.#$=false;break}}}}}consume(e){if(e>this.#X){throw new Error("Called consume() before buffers satiated.")}else if(e===0){return A}if(this.#K[0].length===e){this.#X-=this.#K[0].length;return this.#K.shift()}const t=Buffer.allocUnsafe(e);let s=0;while(s!==e){const i=this.#K[0];const{length:n}=i;if(n+s===e){t.set(this.#K.shift(),s);break}else if(n+s>e){t.set(i.subarray(0,e-s),s);this.#K[0]=i.subarray(e-s);break}else{t.set(this.#K.shift(),s);s+=i.length}}this.#X-=e;return t}parseCloseBody(e){n(e.length!==1);let t;if(e.length>=2){t=e.readUInt16BE(0)}if(Line truncated
* Tmp
*
* Copyright (c) 2011-2017 KARASZI Istvan <github@spam.raszi.hu>
*
* MIT Licensed
*/
const E=g(9896);const p=g(857);const C=g(6928);const f=g(6982);const Q={fs:E.constants,os:p.constants};const B="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",I=/XXXXXX/,y=3,b=(Q.O_CREAT||Q.fs.O_CREAT)|(Q.O_EXCL||Q.fs.O_EXCL)|(Q.O_RDWR||Q.fs.O_RDWR),D=p.platform()==="win32",R=Q.EBADF||Q.os.errno.EBADF,k=Q.ENOENT||Q.os.errno.ENOENT,S=448,v=384,F="exit",x=[],U=E.rmdirSync.bind(E);let L=false;function rimraf(A,i){return E.rm(A,{recursive:true},i)}function FN_RIMRAF_SYNC(A){return E.rmSync(A,{recursive:true})}function tmpName(A,i){const g=_parseArguments(A,i),p=g[0],C=g[1];_assertAndSanitizeOptions(p,function(A,i){if(A)return C(A);let g=i.tries;(function _getUniqueName(){try{const A=_generateTmpName(i);E.stat(A,function(i){if(!i){if(g-- >0)return _getUniqueName();return C(new Error("Could not get a unique tmp filename, max tries reached "+A))}C(null,A)})}catch(A){C(A)}})()})}function tmpNameSync(A){const i=_parseArguments(A),g=i[0];const p=_assertAndSanitizeOptionsSync(g);let C=p.tries;do{const A=_generateTmpName(p);try{E.statSync(A)}catch(i){return A}}while(C-- >0);throw new Error("Could not get a unique tmp filename, max tries reached")}function file(A,i){const g=_parseArguments(A,i),p=g[0],C=g[1];tmpName(p,function _tmpNameCreated(A,i){if(A)return C(A);E.open(i,b,p.mode||v,function _fileCreated(A,g){if(A)return C(A);if(p.discardDescriptor){return E.close(g,function _discardCallback(A){return C(A,i,undefined,_prepareTmpFileRemoveCallback(i,-1,p,false))})}else{const A=p.discardDescriptor||p.detachDescriptor;C(null,i,g,_prepareTmpFileRemoveCallback(i,A?-1:g,p,false))}})})}function fileSync(A){const i=_parseArguments(A),g=i[0];const p=g.discardDescriptor||g.detachDescriptor;const C=tmpNameSync(g);let f=E.openSync(C,b,g.mode||v);if(g.discardDescriptor){E.closeSync(f);f=undefined}return{name:C,fd:f,removeCallback:_prepareTmpFileRemoveCallback(C,p?-1:f,g,true)}}function dir(A,i){const g=_parseArguments(A,i),p=g[0],C=g[1];tmpName(p,function _tmpNameCreated(A,i){if(A)return C(A);E.mkdir(i,p.mode||S,function _dirCreated(A){if(A)return C(A);C(null,i,_prepareTmpDirRemoveCallback(i,p,false))})})}function dirSync(A){const i=_parseArguments(A),g=i[0];const p=tmpNameSync(g);E.mkdirSync(p,g.mode||S);return{name:p,removeCallback:_prepareTmpDirRemoveCallback(p,g,true)}}function _removeFileAsync(A,i){const _handler=function(A){if(A&&!_isENOENT(A)){return i(A)}i()};if(0<=A[0])E.close(A[0],function(){E.unlink(A[1],_handler)});else E.unlink(A[1],_handler)}function _removeFileSync(A){let i=null;try{if(0<=A[0])E.closeSync(A[0])}catch(A){if(!_isEBADF(A)&&!_isENOENT(A))throw A}finally{try{E.unlinkSync(A[1])}catch(A){if(!_isENOENT(A))i=A}}if(i!==null){throw i}}function _prepareTmpFileRemoveCallback(A,i,g,E){const p=_prepareRemoveCallback(_removeFileSync,[i,A],E);const C=_prepareRemoveCallback(_removeFileAsync,[i,A],E,p);if(!g.keep)x.unshift(p);return E?p:C}function _prepareTmpDirRemoveCallback(A,i,g){const p=i.unsafeCleanup?rimraf:E.rmdir.bind(E);const C=i.unsafeCleanup?FN_RIMRAF_SYNC:U;const f=_prepareRemoveCallback(C,A,g);const Q=_prepareRemoveCallback(p,A,g,f);if(!i.keep)x.unshift(f);return g?f:Q}function _prepareRemoveCallback(A,i,g,E){let p=false;return function _cleanupCallback(C){if(!p){const f=E||_cleanupCallback;const Q=x.indexOf(f);if(Q>=0)x.splice(Q,1);p=true;if(g||A===U||A===FN_RIMRAF_SYNC){return A(i)}else{return A(i,C||function(){})}}}}function _garbageCollector(){if(!L)return;while(x.length){try{x[0]()}catch(A){}}}function _randomChars(A){let i=[],g=null;try{g=f.randomBytes(A)}catch(i){g=f.pseudoRandomBytes(A)}for(let E=0;E<A;E++){i.push(B[g[E]%B.length])}return i.join("")}function _isUndefined(A){return typeof A==="undefined"}function _parseArguments(A,i){if(typeof A==="function"){return[{},A]}if(_isUndefined(A)){return[{},i]}const g={};for(const i of Object.getOwnPropertyNames(A)){g[i]=A[i]}return[g,i]}function _resolvePath(A,i,g){const p=C.isAbsolute(A)?A:C.join(i,A);E.stat(p,function(A){if(A){E.realpath(C.dirname(p),function(A,i){if(A)return g(A);g(null,C.join(i,C.basename(p)))})}else{E.realpath(p,g)}})}function _resolvePathSync(A,i){const g=C.isAbsolute(A)?A:C.join(i,A);try{E.statSync(g);return E.realpathSync(g)}catch(A){const i=E.realpathSync(C.dirname(g));return C.join(i,C.basename(g))}}function _generateTmpName(A){const i=A.tmpdir;if(!_isUndefined(A.name)){return C.join(i,A.dir,A.name)}if(!_isUndefined(A.template)){return C.join(i,A.dir,A.template).replace(I,_randomChars(6))}const g=[A.prefix?A.prefix:"tmp","-",process.pid,"-",_randomChars(12),A.postfix?"-"+A.postfix:""].join("");return C.join(i,A.dir,g)}function _assertPath(A,i){if(typeof i!=="string"){throw new Error(`${A} option must be a string, got "${typeof i}".`)}if(i.includes("..")){throw new Error("Relative value not allowed")}return i}function _assertOptionsBase(A){if(!_isUndefined(A.name)){const i=A.name;if(C.isAbsolute(i))throw new Error(`name option must not contain an absolute path, found "${i}".`);const g=C.basename(i);if(g===".."Line truncated
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */;const escape=A=>A.replace(/\n/g,"%0A").replace(/\r/g,"%0D").replace(/"/g,"%22");const normalizeLinefeeds=A=>A.replace(/\r?\n|\r/g,"\r\n");const E=[];const p=new Uint8Array([13,10]);b=0;let C=false;for(const[i,f]of A){if(typeof f==="string"){const A=P.encode(g+`; name="${escape(normalizeLinefeeds(i))}"`+`\r\n\r\n${normalizeLinefeeds(f)}\r\n`);E.push(A);b+=A.byteLength}else{const A=P.encode(`${g}; name="${escape(normalizeLinefeeds(i))}"`+(f.name?`; filename="${escape(f.name)}"`:"")+"\r\n"+`Content-Type: ${f.type||"application/octet-stream"}\r\n\r\n`);E.push(A,f,p);if(typeof f.size==="number"){b+=A.byteLength+f.size+p.byteLength}else{C=true}}}const f=P.encode(`--${i}--`);E.push(f);b+=f.byteLength;if(C){b=null}y=A;I=async function*(){for(const A of E){if(A.stream){yield*A.stream()}else{yield A}}};D="multipart/form-data; boundary="+i}else if(f(A)){y=A;b=A.size;if(A.type){D=A.type}}else if(typeof A[Symbol.asyncIterator]==="function"){if(i){throw new TypeError("keepalive")}if(p.isDisturbed(A)||A.locked){throw new TypeError("Response body object should not be disturbed or locked")}E=A instanceof _?A:C(A)}if(typeof y==="string"||p.isBuffer(y)){b=Buffer.byteLength(y)}if(I!=null){let i;E=new _({async start(){i=I(A)[Symbol.asyncIterator]()},async pull(A){const{value:g,done:p}=await i.next();if(p){queueMicrotask(()=>{A.close()})}else{if(!L(E)){A.enqueue(new Uint8Array(g))}}return A.desiredSize>0},async cancel(A){await i.return()},type:undefined})}const R={stream:E,source:y,length:b};return[R,D]}function safelyExtractBody(A,i=false){if(!_){_=g(3774).ReadableStream}if(A instanceof _){U(!p.isDisturbed(A),"The body has already been consumed.");U(!A.locked,"The stream is locked.")}return extractBody(A,i)}function cloneBody(A){const[i,g]=A.stream.tee();const E=S(g,{transfer:[g]});const[,p]=E.tee();A.stream=i;return{stream:p,length:A.length,source:A.source}}async function*consumeBody(A){if(A){if(M(A)){yield A}else{const i=A.stream;if(p.isDisturbed(i)){throw new TypeError("The body has already been consumed.")}if(i.locked){throw new TypeError("The stream is locked.")}i[x]=true;yield*i}}}function throwIfAborted(A){if(A.aborted){throw new k("The operation was aborted.","AbortError")}}function bodyMixinMethods(A){const i={blob(){return specConsumeBody(this,A=>{let i=bodyMimeType(this);if(i==="failure"){i=""}else if(i){i=Y(i)}return new v([A],{type:i})},A)},arrayBuffer(){return specConsumeBody(this,A=>new Uint8Array(A).buffer,A)},text(){return specConsumeBody(this,utf8DecodeBytes,A)},json(){return specConsumeBody(this,parseJSONFromBytes,A)},async formData(){R.brandCheck(this,A);throwIfAborted(this[D]);const i=this.headers.get("Content-Type");if(/multipart\/form-data/.test(i)){const A={};for(const[i,g]of this.headers)A[i.toLowerCase()]=g;const i=new b;let g;try{g=new E({headers:A,preservePath:true})}catch(A){throw new k(`${A}`,"AbortError")}g.on("field",(A,g)=>{i.append(A,g)});g.on("file",(A,g,E,p,C)=>{const f=[];if(p==="base64"||p.toLowerCase()==="base64"){let p="";g.on("data",A=>{p+=A.toString().replace(/[\r\n]/gm,"");const i=p.length-p.length%4;f.push(Buffer.from(p.slice(0,i),"base64"));p=p.slice(i)});g.on("end",()=>{f.push(Buffer.from(p,"base64"));i.append(A,new V(f,E,{type:C}))})}else{g.on("data",A=>{f.push(A)});g.on("end",()=>{i.append(A,new V(f,E,{type:C}))})}});const p=new Promise((A,i)=>{g.on("finish",A);g.on("error",A=>i(new TypeError(A)))});if(this.body!==null)for await(const A of consumeBody(this[D].body))g.write(A);g.end();await p;return i}else if(/application\/x-www-form-urlencoded/.test(i)){let A;try{let i="";const g=new TextDecoder("utf-8",{ignoreBOM:true});for await(const A of consumeBody(this[D].body)){if(!M(A)){throw new TypeError("Expected Uint8Array chunk")}i+=g.decode(A,{stream:true})}i+=g.decode();A=new URLSearchParams(i)}catch(A){throw Object.assign(new TypeError,{cause:A})}const i=new b;for(const[g,E]of A){i.append(g,E)}return i}else{await Promise.resolve();throwIfAborted(this[D]);throw R.errors.exception({header:`${A.name}.formData`,message:"Could not parse content as FormData."})}}};return i}function mixinBody(A){Object.assign(A.prototype,bodyMixinMethods(A))}async function specConsumeBody(A,i,g){R.brandCheck(A,g);throwIfAborted(A[D]);if(bodyUnusable(A[D].body)){throw new TypeError("Body is unusable")}const E=I();const errorSteps=A=>E.reject(A);const successSteps=A=>{try{E.resolve(i(A))}catch(A){errorSteps(A)}};if(A[D].body==null){successSteps(new Uint8Array);return E.promise}await y(A[D].body,successSteps,errorSteps);return E.promise}function bodyUnusable(A){return A!=null&&(A.stream.locked||p.isDisturbed(A.stream))}function utf8DecodeBytes(A){if(A.length===0){return""}if(A[0]===239&&A[1]===187&&A[2]===191){A=A.subarray(3)}const i=j.decode(A);return i}function parseJSONFromBytes(A){return JSON.parse(utf8DecodeBytes(A))}function bodyMimeType(A){const{headersList:i}=A[D];const g=i.get("content-type");if(g===null){reLine truncated
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */C[p-4]=this.maskKey[0];C[p-3]=this.maskKey[1];C[p-2]=this.maskKey[2];C[p-1]=this.maskKey[3];C[1]=g;if(g===126){C.writeUInt16BE(i,2)}else if(g===127){C[2]=C[3]=0;C.writeUIntBE(i,4,6)}C[1]|=128;for(let A=0;A<i;A++){C[p+A]=this.frameData[A]^this.maskKey[A%4]}return C}}A.exports={WebsocketFrameSend:WebsocketFrameSend}},5080:(A,i,g)=>{const{Writable:E}=g(2203);const p=g(1637);const{parserStates:C,opcodes:f,states:Q,emptyBuffer:B}=g(932);const{kReadyState:I,kSentClose:y,kResponse:b,kReceivedClose:D}=g(327);const{isValidStatusCode:R,failWebsocketConnection:k,websocketMessageReceived:S}=g(1501);const{WebsocketFrameSend:v}=g(9140);const F={};F.ping=p.channel("undici:websocket:ping");F.pong=p.channel("undici:websocket:pong");class ByteParser extends E{#n=[];#o=0;#a=C.INFO;#c={};#l=[];constructor(A){super();this.ws=A}_write(A,i,g){this.#n.push(A);this.#o+=A.length;this.run(g)}run(A){while(true){if(this.#a===C.INFO){if(this.#o<2){return A()}const i=this.consume(2);this.#c.fin=(i[0]&128)!==0;this.#c.opcode=i[0]&15;this.#c.originalOpcode??=this.#c.opcode;this.#c.fragmented=!this.#c.fin&&this.#c.opcode!==f.CONTINUATION;if(this.#c.fragmented&&this.#c.opcode!==f.BINARY&&this.#c.opcode!==f.TEXT){k(this.ws,"Invalid frame type was fragmented.");return}const g=i[1]&127;if(g<=125){this.#c.payloadLength=g;this.#a=C.READ_DATA}else if(g===126){this.#a=C.PAYLOADLENGTH_16}else if(g===127){this.#a=C.PAYLOADLENGTH_64}if(this.#c.fragmented&&g>125){k(this.ws,"Fragmented frame exceeded 125 bytes.");return}else if((this.#c.opcode===f.PING||this.#c.opcode===f.PONG||this.#c.opcode===f.CLOSE)&&g>125){k(this.ws,"Payload length for control frame exceeded 125 bytes.");return}else if(this.#c.opcode===f.CLOSE){if(g===1){k(this.ws,"Received close frame with a 1-byte body.");return}const A=this.consume(g);this.#c.closeInfo=this.parseCloseBody(false,A);if(!this.ws[y]){const A=Buffer.allocUnsafe(2);A.writeUInt16BE(this.#c.closeInfo.code,0);const i=new v(A);this.ws[b].socket.write(i.createFrame(f.CLOSE),A=>{if(!A){this.ws[y]=true}})}this.ws[I]=Q.CLOSING;this.ws[D]=true;this.end();return}else if(this.#c.opcode===f.PING){const i=this.consume(g);if(!this.ws[D]){const A=new v(i);this.ws[b].socket.write(A.createFrame(f.PONG));if(F.ping.hasSubscribers){F.ping.publish({payload:i})}}this.#a=C.INFO;if(this.#o>0){continue}else{A();return}}else if(this.#c.opcode===f.PONG){const i=this.consume(g);if(F.pong.hasSubscribers){F.pong.publish({payload:i})}if(this.#o>0){continue}else{A();return}}}else if(this.#a===C.PAYLOADLENGTH_16){if(this.#o<2){return A()}const i=this.consume(2);this.#c.payloadLength=i.readUInt16BE(0);this.#a=C.READ_DATA}else if(this.#a===C.PAYLOADLENGTH_64){if(this.#o<8){return A()}const i=this.consume(8);const g=i.readUInt32BE(0);if(g>2**31-1){k(this.ws,"Received payload length > 2^31 bytes.");return}const E=i.readUInt32BE(4);this.#c.payloadLength=(g<<8)+E;this.#a=C.READ_DATA}else if(this.#a===C.READ_DATA){if(this.#o<this.#c.payloadLength){return A()}else if(this.#o>=this.#c.payloadLength){const A=this.consume(this.#c.payloadLength);this.#l.push(A);if(!this.#c.fragmented||this.#c.fin&&this.#c.opcode===f.CONTINUATION){const A=Buffer.concat(this.#l);S(this.ws,this.#c.originalOpcode,A);this.#c={};this.#l.length=0}this.#a=C.INFO}}if(this.#o>0){continue}else{A();break}}}consume(A){if(A>this.#o){return null}else if(A===0){return B}if(this.#n[0].length===A){this.#o-=this.#n[0].length;return this.#n.shift()}const i=Buffer.allocUnsafe(A);let g=0;while(g!==A){const E=this.#n[0];const{length:p}=E;if(p+g===A){i.set(this.#n.shift(),g);break}else if(p+g>A){i.set(E.subarray(0,A-g),g);this.#n[0]=E.subarray(A-g);break}else{i.set(this.#n.shift(),g);g+=E.length}}this.#o-=A;return i}parseCloseBody(A,i){let g;if(i.length>=2){g=i.readUInt16BE(0)}if(A){if(!R(g)){return null}return{code:g}}let E=i.subarray(2);if(E[0]===239&&E[1]===187&&E[2]===191){E=E.subarray(3)}if(g!==undefined&&!R(g)){return null}try{E=new TextDecoder("utf-8",{fatal:true}).decode(E)}catch{return null}return{code:g,reason:E}}get closingInfo(){return this.#c.closeInfo}}A.exports={ByteParser:ByteParser}},327:A=>{A.exports={kWebSocketURL:Symbol("url"),kReadyState:Symbol("ready state"),kController:Symbol("controller"),kResponse:Symbol("response"),kBinaryType:Symbol("binary type"),kSentClose:Symbol("sent close"),kReceivedClose:Symbol("received close"),kByteParser:Symbol("byte parser")}},1501:(A,i,g)=>{const{kReadyState:E,kController:p,kResponse:C,kBinaryType:f,kWebSocketURL:Q}=g(327);const{states:B,opcodes:I}=g(932);const{MessageEvent:y,ErrorEvent:b}=g(7952);function isEstablished(A){return A[E]===B.OPEN}function isClosing(A){return A[E]===B.CLOSING}function isClosed(A){return A[E]===B.CLOSED}function fireEvent(A,i,g=Event,E){const p=new g(A,E);i.dispatchEvent(p)}function websocketMessageReceived(A,i,g){if(A[E]!==B.OPEN){return}let p;if(i===I.TEXT){try{p=new TextDecoder("utf-8",{fatal:true}).decode(g)}catch{failWebsocketConnection(A,"Received invLine truncated
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */;const escape=A=>A.replace(/\n/g,"%0A").replace(/\r/g,"%0D").replace(/"/g,"%22");const normalizeLinefeeds=A=>A.replace(/\r?\n|\r/g,"\r\n");const E=[];const p=new Uint8Array([13,10]);y=0;let C=false;for(const[i,f]of A){if(typeof f==="string"){const A=H.encode(g+`; name="${escape(normalizeLinefeeds(i))}"`+`\r\n\r\n${normalizeLinefeeds(f)}\r\n`);E.push(A);y+=A.byteLength}else{const A=H.encode(`${g}; name="${escape(normalizeLinefeeds(i))}"`+(f.name?`; filename="${escape(f.name)}"`:"")+"\r\n"+`Content-Type: ${f.type||"application/octet-stream"}\r\n\r\n`);E.push(A,f,p);if(typeof f.size==="number"){y+=A.byteLength+f.size+p.byteLength}else{C=true}}}const f=H.encode(`--${i}--\r\n`);E.push(f);y+=f.byteLength;if(C){y=null}I=A;B=async function*(){for(const A of E){if(A.stream){yield*A.stream()}else{yield A}}};b=`multipart/form-data; boundary=${i}`}else if(C(A)){I=A;y=A.size;if(A.type){b=A.type}}else if(typeof A[Symbol.asyncIterator]==="function"){if(i){throw new TypeError("keepalive")}if(E.isDisturbed(A)||A.locked){throw new TypeError("Response body object should not be disturbed or locked")}g=A instanceof ReadableStream?A:p(A)}if(typeof I==="string"||E.isBuffer(I)){y=Buffer.byteLength(I)}if(B!=null){let i;g=new ReadableStream({async start(){i=B(A)[Symbol.asyncIterator]()},async pull(A){const{value:E,done:p}=await i.next();if(p){queueMicrotask(()=>{A.close();A.byobRequest?.respond(0)})}else{if(!F(g)){const i=new Uint8Array(E);if(i.byteLength){A.enqueue(i)}}}return A.desiredSize>0},async cancel(A){await i.return()},type:"bytes"})}const D={stream:g,source:I,length:y};return[D,b]}function safelyExtractBody(A,i=false){if(A instanceof ReadableStream){v(!E.isDisturbed(A),"The body has already been consumed.");v(!A.locked,"The stream is locked.")}return extractBody(A,i)}function cloneBody(A,i){const[g,E]=i.stream.tee();i.stream=g;return{stream:E,length:i.length,source:i.source}}function throwIfAborted(A){if(A.aborted){throw new DOMException("The operation was aborted.","AbortError")}}function bodyMixinMethods(A){const i={blob(){return consumeBody(this,A=>{let i=bodyMimeType(this);if(i===null){i=""}else if(i){i=L(i)}return new S([A],{type:i})},A)},arrayBuffer(){return consumeBody(this,A=>new Uint8Array(A).buffer,A)},text(){return consumeBody(this,b,A)},json(){return consumeBody(this,parseJSONFromBytes,A)},formData(){return consumeBody(this,A=>{const i=bodyMimeType(this);if(i!==null){switch(i.essence){case"multipart/form-data":{const g=M(A,i);if(g==="failure"){throw new TypeError("Failed to parse body as FormData.")}const E=new D;E[R]=g;return E}case"application/x-www-form-urlencoded":{const i=new URLSearchParams(A.toString());const g=new D;for(const[A,E]of i){g.append(A,E)}return g}}}throw new TypeError('Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".')},A)},bytes(){return consumeBody(this,A=>new Uint8Array(A),A)}};return i}function mixinBody(A){Object.assign(A.prototype,bodyMixinMethods(A))}async function consumeBody(A,i,g){k.brandCheck(A,g);if(bodyUnusable(A)){throw new TypeError("Body is unusable: Body has already been read")}throwIfAborted(A[R]);const E=B();const errorSteps=A=>E.reject(A);const successSteps=A=>{try{E.resolve(i(A))}catch(A){errorSteps(A)}};if(A[R].body==null){successSteps(Buffer.allocUnsafe(0));return E.promise}await I(A[R].body,successSteps,errorSteps);return E.promise}function bodyUnusable(A){const i=A[R].body;return i!=null&&(i.stream.locked||E.isDisturbed(i.stream))}function parseJSONFromBytes(A){return JSON.parse(b(A))}function bodyMimeType(A){const i=A[R].headersList;const g=y(i);if(g==="failure"){return null}return g}A.exports={extractBody:extractBody,safelyExtractBody:safelyExtractBody,cloneBody:cloneBody,mixinBody:mixinBody,streamRegistry:Y,hasFinalizationRegistry:G,bodyUnusable:bodyUnusable}},4578:A=>{const i=["GET","HEAD","POST"];const g=new Set(i);const E=[101,204,205,304];const p=[301,302,303,307,308];const C=new Set(p);const f=["1","7","9","11","13","15","17","19","20","21","22","23","25","37","42","43","53","69","77","79","87","95","101","102","103","104","109","110","111","113","115","117","119","123","135","137","139","143","161","179","389","427","465","512","513","514","515","526","530","531","532","540","548","554","556","563","587","601","636","989","990","993","995","1719","1720","1723","2049","3659","4045","4190","5060","5061","6000","6566","6665","6666","6667","6668","6669","6679","6697","10080"];const Q=new Set(f);const B=["","no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"];const I=new Set(B);const y=["follow","manual","error"];const b=["GET","HEAD","OPTIONS","TRACE"];const D=new Set(b);const R=["navigate","same-origin","no-cors","cors"];const k=["omit","same-origin","include"];const S=["default","no-store","reload","no-cache","force-cache","onlLine truncated
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */Q[f-4]=g[0];Q[f-3]=g[1];Q[f-2]=g[2];Q[f-1]=g[3];Q[1]=C;if(C===126){Q.writeUInt16BE(p,2)}else if(C===127){Q[2]=Q[3]=0;Q.writeUIntBE(p,4,6)}Q[1]|=128;for(let A=0;A<p;++A){Q[f+A]=i[A]^g[A&3]}return Q}}A.exports={WebsocketFrameSend:WebsocketFrameSend}},6286:(A,i,g)=>{const{createInflateRaw:E,Z_DEFAULT_WINDOWBITS:p}=g(8522);const{isValidClientWindowBits:C}=g(8098);const{MessageSizeExceededError:f}=g(6246);const Q=Buffer.from([0,0,255,255]);const B=Symbol("kBuffer");const I=Symbol("kLength");class PerMessageDeflate{#re;#S={};#ie=0;constructor(A,i){this.#S.serverNoContextTakeover=A.has("server_no_context_takeover");this.#S.serverMaxWindowBits=A.get("server_max_window_bits");this.#ie=i.maxPayloadSize}decompress(A,i,g){if(!this.#re){let A=p;if(this.#S.serverMaxWindowBits){if(!C(this.#S.serverMaxWindowBits)){g(new Error("Invalid server_max_window_bits"));return}A=Number.parseInt(this.#S.serverMaxWindowBits)}try{this.#re=E({windowBits:A})}catch(A){g(A);return}this.#re[B]=[];this.#re[I]=0;this.#re.on("data",A=>{this.#re[I]+=A.length;if(this.#ie>0&&this.#re[I]>this.#ie){g(new f);this.#re.removeAllListeners();this.#re=null;return}this.#re[B].push(A)});this.#re.on("error",A=>{this.#re=null;g(A)})}this.#re.write(A);if(i){this.#re.write(Q)}this.#re.flush(()=>{if(!this.#re){return}const A=Buffer.concat(this.#re[B],this.#re[I]);this.#re[B].length=0;this.#re[I]=0;g(null,A)})}}A.exports={PerMessageDeflate:PerMessageDeflate}},5975:(A,i,g)=>{const{Writable:E}=g(7075);const p=g(4589);const{parserStates:C,opcodes:f,states:Q,emptyBuffer:B,sentCloseFrameState:I}=g(8397);const{kReadyState:y,kSentClose:b,kResponse:D,kReceivedClose:R}=g(7609);const{channels:k}=g(9978);const{isValidStatusCode:S,isValidOpcode:v,failWebsocketConnection:F,websocketMessageReceived:x,utf8Decode:U,isControlFrame:L,isTextBinaryFrame:M,isContinuationFrame:T}=g(8098);const{WebsocketFrameSend:H}=g(9321);const{closeWebSocketConnection:G}=g(1874);const{PerMessageDeflate:Y}=g(6286);const{MessageSizeExceededError:J}=g(6246);function failWebsocketConnectionWithCode(A,i,g){G(A,i,g,Buffer.byteLength(g));F(A,g)}class ByteParser extends E{#n=[];#ne=0;#o=0;#oe=false;#a=C.INFO;#c={};#l=[];#d;#ae;#ie;constructor(A,i,g={}){super();this.ws=A;this.#d=i==null?new Map:i;this.#ae=g.maxFragments??0;this.#ie=g.maxPayloadSize??0;if(this.#d.has("permessage-deflate")){this.#d.set("permessage-deflate",new Y(i,g))}}_write(A,i,g){this.#n.push(A);this.#o+=A.length;this.#oe=true;this.run(g)}#ce(){if(this.#ie>0&&!L(this.#c.opcode)&&this.#c.payloadLength+this.#ne>this.#ie){failWebsocketConnectionWithCode(this.ws,1009,"Payload size exceeds maximum allowed size");return false}return true}run(A){while(this.#oe){if(this.#a===C.INFO){if(this.#o<2){return A()}const i=this.consume(2);const g=(i[0]&128)!==0;const E=i[0]&15;const p=(i[1]&128)===128;const Q=!g&&E!==f.CONTINUATION;const B=i[1]&127;const I=i[0]&64;const y=i[0]&32;const b=i[0]&16;if(!v(E)){F(this.ws,"Invalid opcode received");return A()}if(p){F(this.ws,"Frame cannot be masked");return A()}if(I!==0&&!this.#d.has("permessage-deflate")){F(this.ws,"Expected RSV1 to be clear.");return}if(y!==0||b!==0){F(this.ws,"RSV1, RSV2, RSV3 must be clear");return}if(Q&&!M(E)){F(this.ws,"Invalid frame type was fragmented.");return}if(M(E)&&this.#l.length>0){F(this.ws,"Expected continuation frame");return}if(this.#c.fragmented&&Q){F(this.ws,"Fragmented frame exceeded 125 bytes.");return}if((B>125||Q)&&L(E)){F(this.ws,"Control frame either too large or fragmented");return}if(T(E)&&this.#l.length===0&&!this.#c.compressed){F(this.ws,"Unexpected continuation frame");return}if(B<=125){this.#c.payloadLength=B;this.#a=C.READ_DATA;if(!this.#ce()){return}}else if(B===126){this.#a=C.PAYLOADLENGTH_16}else if(B===127){this.#a=C.PAYLOADLENGTH_64}if(M(E)){this.#c.binaryType=E;this.#c.compressed=I!==0}this.#c.opcode=E;this.#c.masked=p;this.#c.fin=g;this.#c.fragmented=Q}else if(this.#a===C.PAYLOADLENGTH_16){if(this.#o<2){return A()}const i=this.consume(2);this.#c.payloadLength=i.readUInt16BE(0);this.#a=C.READ_DATA;if(!this.#ce()){return}}else if(this.#a===C.PAYLOADLENGTH_64){if(this.#o<8){return A()}const i=this.consume(8);const g=i.readUInt32BE(0);const E=i.readUInt32BE(4);if(g!==0||E>2**31-1){F(this.ws,"Received payload length > 2^31 bytes.");return}this.#c.payloadLength=E;this.#a=C.READ_DATA;if(!this.#ce()){return}}else if(this.#a===C.READ_DATA){if(this.#o<this.#c.payloadLength){return A()}const i=this.consume(this.#c.payloadLength);if(L(this.#c.opcode)){this.#oe=this.parseControlFrame(i);this.#a=C.INFO}else{if(!this.#c.compressed){if(!this.writeFragments(i)){return}if(this.#ie>0&&this.#ne>this.#ie){failWebsocketConnectionWithCode(this.ws,1009,(new J).message);return}if(!this.#c.fragmented&&this.#c.fin){x(this.ws,this.#c.binaryType,this.consumeFragments())}this.#a=C.INFO}else{this.#d.get("permessage-deflate").decompress(i,this.#c.fin,(i,g)=>{if(i){const A=i instanceof J?1009:1007;failWebsocketConnectionWithCLine truncated
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
+36 -144
View File
@@ -1,10 +1,8 @@
import { ScreepsAPI } from "screeps-api"; const { ScreepsAPI } = require("screeps-api");
import * as core from "@actions/core"; const core = require("@actions/core");
import fs from "fs"; const fs = require("fs");
import { glob } from "glob"; const { glob } = require("glob");
import path from "path"; const path = require("path");
import { fileURLToPath } from "url";
import { monitorConsole } from "./monitor.js";
/** /**
* Replaces specific placeholder strings within the provided content with corresponding dynamic values. * Replaces specific placeholder strings within the provided content with corresponding dynamic values.
@@ -19,7 +17,7 @@ import { monitorConsole } from "./monitor.js";
* @param {string} content - The string content in which placeholders are to be replaced. * @param {string} content - The string content in which placeholders are to be replaced.
* @returns {string} The content with placeholders replaced by their respective dynamic values. * @returns {string} The content with placeholders replaced by their respective dynamic values.
*/ */
export function replacePlaceholders(content, hostname) { function replacePlaceholders(content, hostname) {
const deployTime = new Date().toISOString(); const deployTime = new Date().toISOString();
return content return content
.replace(/{{gitHash}}/g, process.env.GITHUB_SHA) .replace(/{{gitHash}}/g, process.env.GITHUB_SHA)
@@ -39,9 +37,8 @@ export function replacePlaceholders(content, hostname) {
* @param {string} [prefix] - An optional directory prefix to prepend to the glob pattern. This allows searching within a specific directory. * @param {string} [prefix] - An optional directory prefix to prepend to the glob pattern. This allows searching within a specific directory.
* @returns {Promise<string[]>} A promise that resolves with an array of file paths that were processed, or rejects with an error if the process fails. * @returns {Promise<string[]>} A promise that resolves with an array of file paths that were processed, or rejects with an error if the process fails.
*/ */
export async function readReplaceAndWriteFiles(pattern, prefix, hostname) { async function readReplaceAndWriteFiles(pattern, prefix, hostname) {
let globPattern = prefix ? path.join(prefix, pattern) : pattern; const globPattern = prefix ? path.join(prefix, pattern) : pattern;
globPattern = globPattern.replace(/\\/g, "/");
const files = await glob(globPattern); const files = await glob(globPattern);
let processPromises = files.map((file) => { let processPromises = files.map((file) => {
@@ -61,10 +58,9 @@ export async function readReplaceAndWriteFiles(pattern, prefix, hostname) {
* @param {string} prefix - Directory prefix for file paths. * @param {string} prefix - Directory prefix for file paths.
* @returns {Promise<Object>} - Promise resolving to a dictionary of file contents keyed by filenames. * @returns {Promise<Object>} - Promise resolving to a dictionary of file contents keyed by filenames.
*/ */
export async function readFilesIntoDict(pattern, prefix) { async function readFilesIntoDict(pattern, prefix) {
// Prepend the prefix to the glob pattern // Prepend the prefix to the glob pattern
let globPattern = prefix ? path.join(prefix, pattern) : pattern; const globPattern = prefix ? path.join(prefix, pattern) : pattern;
globPattern = globPattern.replace(/\\/g, "/");
const files = await glob(globPattern); const files = await glob(globPattern);
let fileDict = {}; let fileDict = {};
@@ -92,7 +88,7 @@ export async function readFilesIntoDict(pattern, prefix) {
* @param {string} password - The password. * @param {string} password - The password.
* @returns {string|null} - Returns an error message if validation fails, otherwise null. * @returns {string|null} - Returns an error message if validation fails, otherwise null.
*/ */
export function validateAuthentication(token, username, password) { function validateAuthentication(token, username, password) {
if (token) { if (token) {
if (username || password) { if (username || password) {
return "Token is defined along with username and/or password."; return "Token is defined along with username and/or password.";
@@ -111,32 +107,10 @@ export function validateAuthentication(token, username, password) {
return null; // No errors found return null; // No errors found
} }
/**
* Applies the 'ignore' | 'warn' | 'fail' enum action when the given flag is true.
* Exported so it can be unit-tested independently.
*
* @param {'ignore'|'warn'|'fail'} action
* @param {boolean} flag - Only acts when true
* @param {string} message - Passed to core.warning / core.setFailed
* @returns {boolean} - Returns true if the action was 'fail' and the flag was true.
*/
export function applyOnAction(action, flag, message) {
if (!flag) return false;
if (action === "warn") {
core.warning(message);
return false;
}
if (action === "fail") {
core.setFailed(message);
return true;
}
return false;
}
/** /**
* Posts code to Screeps server. * Posts code to Screeps server.
*/ */
export async function postCode() { async function postCode() {
const protocol = core.getInput("protocol") || "https"; const protocol = core.getInput("protocol") || "https";
const hostname = core.getInput("hostname") || "screeps.com"; const hostname = core.getInput("hostname") || "screeps.com";
const port = core.getInput("port") || "443"; const port = core.getInput("port") || "443";
@@ -181,116 +155,34 @@ export async function postCode() {
return; return;
} }
let api = new ScreepsAPI(login_arguments); let api = new ScreepsAPI(login_arguments);
if (token) {
if (!token) {
core.info(`Logging in as user ${username}`);
try {
await api.auth(username, password, login_arguments);
} catch (err) {
core.error(`Authentication error: ${err}`);
throw err;
}
}
let oldCode = null;
let rollbackOnFailure = false;
try {
rollbackOnFailure = core.getBooleanInput("rollback_on_failure");
} catch (e) {
// getBooleanInput throws if not 'true' or 'false', ignore
}
if (rollbackOnFailure) {
core.info(
`Downloading existing code from branch ${branch} for potential rollback...`,
);
try {
const getResponse = await api.code.get(branch);
if (getResponse && getResponse.ok && getResponse.modules) {
oldCode = getResponse.modules;
core.info(
`Successfully downloaded existing code (modules: ${Object.keys(oldCode).join(", ")})`,
);
} else {
core.setFailed(
`Failed to download existing code, but rollback_on_failure is enabled. Aborting deployment.`,
);
return;
}
} catch (err) {
core.setFailed(
`Error downloading existing code: ${err.message}. Aborting deployment.`,
);
return;
}
}
try {
const response = await api.code.set(branch, files_to_push); const response = await api.code.set(branch, files_to_push);
core.info(JSON.stringify(response, null, 2)); core.info(JSON.stringify(response, null, 2));
core.info(`Code set successfully to ${branch}`); console.log(`Code set successfully to ${branch}`);
} catch (err) { } else {
core.error(`Upload error: ${err}`); core.info(`Logging in as user ${username}`);
throw err; await Promise.resolve()
} .then(() => api.auth(username, password, login_arguments))
.then(() => {
// Console monitoring (optional) api.code.set(branch, files_to_push);
const monitorTicks = parseInt(core.getInput("monitor") || "0", 10); })
if (monitorTicks > 0) { .then(() => {
const onTraceback = core.getInput("on_traceback") || "fail"; console.log(`Code set successfully to ${branch}`);
const onErrorLog = core.getInput("on_error_log") || "warn"; })
const onWarningLog = core.getInput("on_warning_log") || "ignore"; .catch((err) => {
console.error("Error:", err);
const result = await monitorConsole(api, { });
monitor: monitorTicks,
logToFile: core.getBooleanInput("log_to_file"),
onTraceback,
onErrorLog,
onWarningLog,
monitorInterval: parseInt(core.getInput("monitor_interval") || "10", 10),
hostname,
shard: core.getInput("shard") || undefined,
});
core.setOutput("saw_traceback", String(result.sawTraceback));
core.setOutput("saw_error_log", String(result.sawErrorLog));
core.setOutput("saw_warning_log", String(result.sawWarningLog));
const fail1 = applyOnAction(
onTraceback,
result.sawTraceback,
"Screeps console: traceback detected",
);
const fail2 = applyOnAction(
onErrorLog,
result.sawErrorLog,
"Screeps console: error log output detected",
);
const fail3 = applyOnAction(
onWarningLog,
result.sawWarningLog,
"Screeps console: warning log output detected",
);
const shouldFail = fail1 || fail2 || fail3;
if (shouldFail && rollbackOnFailure && oldCode) {
core.info(
"Action failed based on monitor configuration. Rolling back to previous code...",
);
try {
await api.code.set(branch, oldCode);
core.info(
`Successfully rolled back to previous code on branch ${branch}.`,
);
} catch (err) {
core.error(`Rollback failed: ${err}`);
}
}
} }
} }
const __filename = fileURLToPath(import.meta.url); if (require.main === module) {
if (process.argv[1] === __filename) {
postCode(); postCode();
} }
module.exports = {
validateAuthentication,
replacePlaceholders,
postCode,
readReplaceAndWriteFiles,
readFilesIntoDict,
};
-420
View File
@@ -1,420 +0,0 @@
import * as core from "@actions/core";
import { create } from "@actions/artifact";
import fs from "fs";
import path from "path";
import os from "os";
/**
* Returns true if the hostname is the official Screeps server.
* Used to decide whether to prefix the subscribe path with a shard name.
*
* @param {string} hostname - e.g. "screeps.com" or "builder64"
* @returns {boolean}
*/
export function isOfficialServer(hostname) {
return hostname === "screeps.com";
}
/**
* Builds the channel path argument passed to socket.subscribe().
*
* The screeps-api socket automatically prefixes `user:<id>/` when the path
* does not match the `type:id` pattern, so we only supply the channel part:
* If shard is provided → "<shard>/console"
* Official server → "shard0/console" (if no shard provided)
* Private server → "console" (if no shard provided)
*
* @param {string} hostname
* @param {string} [shard]
* @returns {string}
*/
export function buildSubscribePath(hostname, shard) {
// The console channel on Screeps official and most private servers is 'console'.
// We subscribe to the aggregate feed and filter by shard in handleConsoleEvent.
return "console";
}
/**
* Returns true when errorText contains JavaScript stack-frame lines.
* Screeps places runtime errors (including stack traces) in event.data.error.
* A traceback is identified by lines beginning with four spaces followed by "at ".
*
* @param {string|null|undefined} errorText - Contents of event.data.error
* @returns {boolean}
*/
export function detectTraceback(errorText) {
if (!errorText) return false;
const text = safeDecode(errorText);
return /^\s{4}at /m.test(text);
}
/**
* Returns true when any log line contains Screeps console.warn markup.
* Screeps wraps console.warn() output in orange or yellow <font> HTML tags.
*
* @param {string[]|null|undefined} logLines - Contents of event.data.messages.log
* @returns {boolean}
*/
export function detectWarning(logLines) {
if (!logLines || logLines.length === 0) return false;
const warnPattern = /<font\s+color=['"](?:orange|yellow)['"]/i;
return logLines.some((line) => warnPattern.test(line));
}
/**
* Safely decodes a URI-encoded string from the Screeps console.
* Returns the original string if decoding fails or if no '%' is present.
*
* @param {string} text
* @returns {string}
*/
export function safeDecode(text) {
if (typeof text !== "string") return text;
let result = text;
if (result.includes("%")) {
try {
result = decodeURIComponent(result);
} catch (err) {
// Ignore decoding errors
}
}
// Screeps console often contains HTML entities
return result
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&amp;/g, "&");
}
/**
* Outputs text to the action log, splitting by newline to ensure proper display.
* Optionally prepends a [shard] prefix.
*
* @param {string} text
* @param {"info"|"warning"|"error"} level
* @param {string} [shard]
*/
export function outputMultiline(text, level = "info", shard = null) {
if (!text) return;
const prefix = shard ? `[${shard}] ` : "";
const lines = text.split(/\r?\n/);
lines.forEach((line) => {
const formattedLine = `${prefix}${line}`;
if (level === "error") core.error(formattedLine);
else if (level === "warning") core.warning(formattedLine);
else core.info(formattedLine);
});
}
/**
* Builds a CI-friendly progress string for core.info().
*
* @param {number} elapsed - Ticks elapsed since monitoring started
* @param {number} total - Total ticks to monitor
* @returns {string} - e.g. "[Monitor] 10/50 ticks elapsed..."
*/
export function buildProgressMessage(elapsed, total) {
return `[Monitor] ${elapsed}/${total} ticks elapsed...`;
}
/**
* Writes an array of log lines to a UTF-8 text file, one line per entry.
*
* @param {string[]} lines - Lines to write
* @param {string} filePath - Absolute path to write to
* @returns {Promise<void>}
*/
export async function writeLogFile(lines, filePath) {
await fs.promises.writeFile(filePath, lines.join("\n"), "utf8");
}
/**
* Uploads one or more files as a named workflow artifact using @actions/artifact.
* Degrades gracefully to core.warning() if the runner does not support the
* artifact service.
*
* @param {string[]} filePaths - Absolute paths of the files to upload
* @param {string} [artifactName="screeps-console-log"] - Artifact display name
* @returns {Promise<void>}
*/
export async function uploadLogArtifacts(
filePaths,
artifactName = "screeps-console-log",
) {
if (!filePaths || filePaths.length === 0) return;
try {
const client = create();
const rootDir = path.dirname(filePaths[0]);
await client.uploadArtifact(artifactName, filePaths, rootDir, {
continueOnError: true,
});
core.info(`[Monitor] Console logs uploaded as artifact '${artifactName}'.`);
} catch (err) {
core.warning(
`[Monitor] Could not upload console logs as artifact: ${err.message}`,
);
}
}
/**
* Processes a single 'console' WebSocket event from the Screeps socket.
* Mutates `state` and `stdoutBuffer` in place; never throws.
*
* WebSocket event.data shape:
* {
* messages: {
* log: string[], // stdout (console.warn included with HTML markup)
* results: string[], // return values of console-evaluated expressions
* },
* error: string | null, // stderr, runtime errors, tracebacks
* }
*
* Behaviour:
* - Warn lines (orange/yellow <font> tags) → always core.warning() (live),
* sets state.sawWarningLog, still included in stdoutBuffer / core.info().
* - Error field → always core.error() (live), sets state.sawErrorLog.
* If a stack frame is detected → also sets state.sawTraceback.
* - All stdout lines → core.info() when logToFile=false,
* pushed to stdoutBuffer when logToFile=true.
*
* @param {{ data?: { messages?: { log?: string[], results?: string[] }, error?: string, shard?: string } }} event
* @param {{ logToFile: boolean, shard?: string }} opts
* @param {Record<string, string[]>} shardBuffers - Mutable buffer Map used in logToFile mode
* @param {{ sawTraceback: boolean, sawErrorLog: boolean, sawWarningLog: boolean }} state
*/
export function handleConsoleEvent(event, opts, shardBuffers, state) {
const { logToFile, shard: targetShard } = opts;
const data = event?.data ?? {};
// Shard filtering: If a shard is specified in opts, only process messages from that shard.
// Official server events include a 'shard' property in event.data.
if (targetShard && data.shard && data.shard !== targetShard) {
return;
}
const logLines = data?.messages?.log ?? [];
const results = data?.messages?.results ?? [];
const errorText = data?.error ?? null;
// Warn detection (always live regardless of logToFile)
if (detectWarning(logLines)) {
state.sawWarningLog = true;
const warnPattern = /<font\s+color=['"](?:orange|yellow)['"]/i;
logLines
.filter((l) => warnPattern.test(l))
.forEach((l) => outputMultiline(safeDecode(l), "warning", data.shard));
}
// Traceback detection in log lines (Screeps sometimes sends errors here)
if (logLines.some((l) => detectTraceback(l))) {
state.sawTraceback = true;
state.sawErrorLog = true;
}
// Stdout lines
const allStdout = [...logLines, ...results].map(safeDecode);
if (allStdout.length > 0) {
if (logToFile) {
const shard = data.shard || "default";
if (!shardBuffers[shard]) shardBuffers[shard] = [];
shardBuffers[shard].push(...allStdout);
} else {
allStdout.forEach((l) => outputMultiline(l, "info", data.shard));
}
}
// Error field (always live)
if (errorText) {
state.sawErrorLog = true;
const decodedError = safeDecode(errorText);
outputMultiline(decodedError, "error", data.shard);
if (detectTraceback(decodedError)) {
state.sawTraceback = true;
}
}
}
/**
* Sleeps for the given number of milliseconds.
*
* @param {number} ms
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Polls GET /api/game/time every `intervalMs` milliseconds until the tick
* delta (currentTick - startTick) reaches or exceeds `targetTicks`.
* Calls `onProgress(elapsed, targetTicks)` on every poll so the caller can
* log progress at whatever cadence it chooses.
*
* @param {import('screeps-api').ScreepsAPI} api
* @param {number} startTick - Tick number recorded before monitoring started
* @param {number} targetTicks - Stop when (currentTick - startTick) >= this
* @param {string|undefined} shard - "shard0" for official, undefined for private
* @param {number} intervalMs - Poll interval in milliseconds
* @param {(elapsed: number, total: number) => void} onProgress
* @returns {Promise<number>} Final elapsed tick count
*/
export async function pollUntilDone(
api,
startTick,
targetTicks,
shard,
intervalMs,
onProgress,
shouldStop = () => false,
) {
let elapsed = 0;
while (elapsed < targetTicks && !shouldStop()) {
await sleep(intervalMs);
const { time } = await api.time(shard);
elapsed = time - startTick;
onProgress(elapsed, targetTicks);
}
return elapsed;
}
/**
* @typedef {Object} MonitorOptions
* @property {number} monitor - Number of game ticks to collect.
* @property {boolean} logToFile - Buffer stdout to artifact instead of streaming.
* @property {'ignore'|'warn'|'fail'} onTraceback - Action on traceback detection.
* @property {'ignore'|'warn'|'fail'} onErrorLog - Action on any error-console output.
* @property {'ignore'|'warn'|'fail'} onWarningLog - Action on console.warn output.
* @property {number} monitorInterval - Print a progress update every N ticks.
* @property {string} hostname - Screeps hostname (for shard derivation).
* @property {string} [shard] - Optional shard to monitor.
*/
/**
* @typedef {Object} MonitorResult
* @property {boolean} sawTraceback - True if a JS stack trace was detected.
* @property {boolean} sawErrorLog - True if the error console had any output.
* @property {boolean} sawWarningLog - True if console.warn output was detected.
*/
/**
* Monitors the Screeps console for a given number of game ticks after a deploy.
*
* Flow:
* 1. Fetch startTick via GET /api/game/time (REST poll).
* 2. Connect WebSocket and subscribe to the console channel.
* 3. Run the tick-poll loop (500 ms interval) concurrently with the socket
* event listener; the poll loop drives the stop condition.
* 4. On each 'console' WebSocket event, delegate to handleConsoleEvent().
* 5. When poll finishes, disconnect socket cleanly (in a finally block).
* 6. If logToFile=true: write buffered stdout to a temp file and upload artifact.
* 7. Return MonitorResult.
*
* @param {import('screeps-api').ScreepsAPI} api
* @param {MonitorOptions} opts
* @returns {Promise<MonitorResult>}
*/
export async function monitorConsole(api, opts) {
const {
monitor,
logToFile,
monitorInterval,
hostname,
shard: providedShard,
} = opts;
// Use provided shard, or fall back to shard0 for official, or undefined for private
const shard =
providedShard || (isOfficialServer(hostname) ? "shard0" : undefined);
const subscribePath = buildSubscribePath(hostname, providedShard);
// Shared mutable state — updated by handleConsoleEvent via event listener
const shardBuffers = {}; // { [shardName]: string[] }
const state = {
sawTraceback: false,
sawErrorLog: false,
sawWarningLog: false,
};
let lastProgressTick = 0;
// Step 1: record starting tick
const { time: startTick } = await api.time(shard);
// Step 2: connect socket + subscribe
await api.socket.connect();
await api.socket.subscribe(subscribePath, (event) => {
handleConsoleEvent(event, opts, shardBuffers, state);
});
core.info(
`[Monitor] Watching Screeps console for ${monitor} ticks` +
(shard ? ` on ${shard}` : "") +
"...",
);
// Step 3 & 4: tick-poll loop
try {
await pollUntilDone(
api,
startTick,
monitor,
shard,
500,
(elapsed, total) => {
// Print progress at configured interval boundaries
if (
elapsed > 0 &&
elapsed >= lastProgressTick + monitorInterval &&
elapsed <= total
) {
core.info(buildProgressMessage(elapsed, total));
lastProgressTick = elapsed;
}
},
() => {
// Fail-fast logic: stop monitoring if any 'fail' action is triggered
if (opts.onTraceback === "fail" && state.sawTraceback) return true;
if (opts.onErrorLog === "fail" && state.sawErrorLog) return true;
if (opts.onWarningLog === "fail" && state.sawWarningLog) return true;
return false;
},
);
} finally {
// Step 5: always disconnect cleanly
api.socket.disconnect();
}
// Step 6: artifact upload
const shardKeys = Object.keys(shardBuffers);
if (logToFile && shardKeys.length > 0) {
const tmpDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "screeps-monitor-"),
);
const filePaths = [];
for (const sName of shardKeys) {
const fileName =
sName === "default"
? "screeps_console_log.txt"
: `${sName}_console_log.txt`;
const tmpFile = path.join(tmpDir, fileName);
await writeLogFile(shardBuffers[sName], tmpFile);
filePaths.push(tmpFile);
}
await uploadLogArtifacts(filePaths);
} else if (logToFile) {
core.info(
"[Monitor] No stdout lines were collected; skipping artifact upload.",
);
}
core.info(
`[Monitor] Done. sawTraceback=${state.sawTraceback} sawErrorLog=${state.sawErrorLog} sawWarningLog=${state.sawWarningLog}`,
);
return {
sawTraceback: state.sawTraceback,
sawErrorLog: state.sawErrorLog,
sawWarningLog: state.sawWarningLog,
};
}
+2361
View File
File diff suppressed because it is too large. Load diff
+6 -9
View File
@@ -1,9 +1,7 @@
{ {
"name": "screeps-deploy-action", "name": "screeps-deploy-action",
"version": "0.2.1", "version": "0.1.1",
"description": "Deploys screeps code to the official game or a private server.", "description": "Deploys screeps code to the official game or an pirvate server.",
"type": "module",
"packageManager": "pnpm@11.25.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
@@ -11,14 +9,13 @@
"build": "ncc build index.js -o dist -m --external utf-8-validate --external bufferutil" "build": "ncc build index.js -o dist -m --external utf-8-validate --external bufferutil"
}, },
"dependencies": { "dependencies": {
"@actions/artifact": "^1.1.2", "@actions/core": "^2.0.0",
"@actions/core": "^3.0.0",
"glob": "^13.0.0", "glob": "^13.0.0",
"screeps-api": "^1.7.2" "screeps-api": "^1.7.2"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.45.0", "@vercel/ncc": "^0.38.4",
"@vitest/coverage-v8": "^4.1.11", "@vitest/coverage-v8": "^4.0.16",
"vitest": "^4.1.11" "vitest": "^4.0.16"
} }
} }
-1408
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -1,3 +0,0 @@
allowBuilds:
bufferutil: true
utf-8-validate: true
+15 -6
View File
@@ -5,11 +5,20 @@
"config:recommended", "config:recommended",
":automergeLinters" ":automergeLinters"
], ],
"rebaseWhen": "behind-base-branch", "packageRules": [
"branchConcurrentLimit": 2, {
"prConcurrentLimit": 2, "matchManagers": [
"gitIgnoredAuthors": [ "npm"
"github-actions[bot]@users.noreply.github.com", ],
"bot@horstenkamp.eu" "postUpgradeTasks": {
"commands": [
"npm ci",
"npm run build"
],
"fileFilters": [
"dist/index.js"
]
}
}
] ]
} }
-29
View File
@@ -1,29 +0,0 @@
import { glob } from "glob";
import path from "path";
import os from "os";
import fs from "fs";
async function test() {
const tempDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "glob-test-"),
);
const file = path.join(tempDir, "test.js");
await fs.promises.writeFile(file, "test");
const pattern = "*.js";
const globPattern = path.join(tempDir, pattern);
console.log("globPattern:", globPattern);
const files = await glob(globPattern);
console.log("found files:", files);
// Fix for windows
const fixedPattern = globPattern.replace(/\\/g, "/");
console.log("fixedPattern:", fixedPattern);
const fixedFiles = await glob(fixedPattern);
console.log("found fixed files:", fixedFiles);
await fs.promises.rm(tempDir, { recursive: true, force: true });
}
test();
-12
View File
@@ -1,12 +0,0 @@
function detectTraceback(errorText) {
if (!errorText) return false;
return /^\s{4}at /m.test(errorText);
}
const encodedTraceback =
"Error: ReferenceError: a is not defined%0A at eval (eval at <anonymous> (_console1778948572008_0:1:46), <anonymous>:1:1)%0A at _console1778948572008_0:1:46%0A at _console1778948572008_0:1:60%0A at exports.evalCode (<runtime>:15347:63)%0A at exports.run (<runtime>:20876:41)%0A";
console.log("Encoded match:", detectTraceback(encodedTraceback));
const decodedTraceback = decodeURIComponent(encodedTraceback);
console.log("Decoded match:", detectTraceback(decodedTraceback));