Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b994f1bd | ||
|
|
815d874ef7 | ||
|
|
c08128c0ef | ||
|
|
7c5cb5ffec | ||
|
|
f5c1ffde37 | ||
|
|
a2711f3f40 | ||
|
|
7e526aeef3 | ||
|
|
c116597f23 | ||
|
|
0751b1470b | ||
|
|
76a263859b | ||
|
|
2f32772d0a | ||
|
|
7a447adf45 | ||
|
|
dcdca8e753 | ||
|
|
9dcad628d4 | ||
|
|
c9b5d215f6 | ||
|
|
e8534d7fae | ||
|
|
873de41ead | ||
|
|
36e34e99c5 | ||
|
|
2292b914ad | ||
|
|
bdd0fbfd7e | ||
|
|
df5d81df5c | ||
|
|
0b930f5645 | ||
|
|
67f3f15c2b | ||
|
|
d0a08da728 | ||
|
|
076e96f3de | ||
|
|
bf52580bf3 | ||
|
|
be3c8ac7d2 | ||
|
|
1df4a4248c | ||
|
|
2a4928efe1 | ||
|
|
065bdde05d | ||
|
|
26a3dacde8 | ||
|
|
11576b5c40 | ||
|
|
7f1ea2b452 | ||
|
|
56dd6822ff | ||
|
|
6345c28950 | ||
|
|
6fc5d6253f | ||
|
|
f96bfc2f7b | ||
|
|
34cc58529b | ||
|
|
2be5b2a1bc | ||
|
|
cbbd0e64e8 | ||
|
|
dee3bff2ed | ||
|
|
2a1480e46b | ||
|
|
150f8fc15f | ||
|
|
96f131369e | ||
|
|
684cc88afb | ||
|
|
c07372bfa5 | ||
|
|
d918fd764c | ||
|
|
4a3dbb707c | ||
|
|
bfa059df07 |
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
# .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
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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
|
||||||
@@ -2,6 +2,7 @@ name: Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -9,11 +10,13 @@ jobs:
|
|||||||
name: Run Tests
|
name: Run Tests
|
||||||
runs-on: pi
|
runs-on: pi
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: actions/setup-node@v6
|
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24'
|
||||||
- run: npm install
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
shell: bash
|
shell: bash
|
||||||
- run: npm test
|
- run: pnpm test
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -1078,3 +1078,6 @@ FodyWeavers.xsd
|
|||||||
/node_modules/.cache/
|
/node_modules/.cache/
|
||||||
/coverage/
|
/coverage/
|
||||||
!/dist/
|
!/dist/
|
||||||
|
|
||||||
|
# Local test credentials — never commit
|
||||||
|
.env.test
|
||||||
@@ -17,10 +17,11 @@ repos:
|
|||||||
- id: no-commit-to-branch
|
- id: no-commit-to-branch
|
||||||
|
|
||||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||||
rev: v2.15.0
|
rev: v2.16.0
|
||||||
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
|
||||||
@@ -29,7 +30,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.36.0
|
rev: 0.38.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-renovate
|
- id: check-renovate
|
||||||
- id: check-github-actions
|
- id: check-github-actions
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ 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
|
||||||
|
|
||||||
@@ -18,7 +20,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
|
||||||
npm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing Pipeline
|
### Testing Pipeline
|
||||||
@@ -28,6 +30,11 @@ 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.
|
2. Sets up Node.js and pnpm.
|
||||||
3. Installs the dependencies using `npm install`.
|
3. Installs the dependencies using `pnpm install`.
|
||||||
4. Runs the tests using `npm test`.
|
4. Runs the tests using `pnpm 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**).
|
||||||
@@ -26,6 +26,19 @@ 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
|
||||||
|
|
||||||
@@ -71,7 +84,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 `npm install`, which speeds up the action execution.
|
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.
|
||||||
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.
|
||||||
|
|
||||||
@@ -87,8 +100,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
|
||||||
npm install
|
pnpm install
|
||||||
npm run build
|
pnpm 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.
|
||||||
|
|||||||
+155
-6
@@ -1,13 +1,58 @@
|
|||||||
const {
|
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
// 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,
|
||||||
} = require("../index");
|
applyOnAction,
|
||||||
const fs = require("fs");
|
postCode,
|
||||||
const path = require("path");
|
} from "../index.js";
|
||||||
const os = require("os");
|
import { ScreepsAPI } from "screeps-api";
|
||||||
const { glob } = require("glob");
|
import fs from "fs";
|
||||||
|
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", () => {
|
||||||
@@ -197,3 +242,107 @@ 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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,633 @@
|
|||||||
|
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
@@ -40,6 +40,44 @@ 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
|
||||||
Vendored
+16
-5
@@ -1,16 +1,27 @@
|
|||||||
(()=>{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 A=core.getInput("password")||undefined;const o=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,o,t)}const u=await readFilesIntoDict(a,o);core.info(`Trying to upload the following files to ${c}:`);Object.keys(u).forEach((e=>{core.info(`Key: ${e}`)}));const h={token:n,username:r,password:A,protocol:e,hostname:t,port:s,path:i};core.info("login_arguments:");core.info(JSON.stringify(h,null,2));const p=validateAuthentication(n,r,A);if(p){core.error(p);return}let d=new ScreepsAPI(h);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,A,h))).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 A=r(s(857));const o=s(302);function issueCommand(e,t,s){const i=new Command(e,t,s);process.stdout.write(i.toString()+A.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,o.toCommandValue)(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return(0,o.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
|
import{createRequire as t}from"module";var A={7038:(t,A,u)=>{var d;d={value:true};A.v=void 0;const E=u(918);function create(){return E.DefaultArtifactClient.create()}A.v=create},918:function(t,A,u){var d=this&&this.__createBinding||(Object.create?function(t,A,u,d){if(d===undefined)d=u;var E=Object.getOwnPropertyDescriptor(A,u);if(!E||("get"in E?!A.__esModule:E.writable||E.configurable)){E={enumerable:true,get:function(){return A[u]}}}Object.defineProperty(t,d,E)}:function(t,A,u,d){if(d===undefined)d=u;t[d]=A[u]});var E=this&&this.__setModuleDefault||(Object.create?function(t,A){Object.defineProperty(t,"default",{enumerable:true,value:A})}:function(t,A){t["default"]=A});var C=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var A={};if(t!=null)for(var u in t)if(u!=="default"&&Object.prototype.hasOwnProperty.call(t,u))d(A,t,u);E(A,t);return A};var Q=this&&this.__awaiter||function(t,A,u,d){function adopt(t){return t instanceof u?t:new u(function(A){A(t)})}return new(u||(u=Promise))(function(u,E){function fulfilled(t){try{step(d.next(t))}catch(t){E(t)}}function rejected(t){try{step(d["throw"](t))}catch(t){E(t)}}function step(t){t.done?u(t.value):adopt(t.value).then(fulfilled,rejected)}step((d=d.apply(t,A||[])).next())})};Object.defineProperty(A,"__esModule",{value:true});A.DefaultArtifactClient=void 0;const B=C(u(9910));const I=u(9367);const m=u(8950);const y=u(2669);const b=u(9400);const D=u(3439);const R=u(3886);const k=u(1124);const S=u(6928);class DefaultArtifactClient{static create(){return new DefaultArtifactClient}uploadArtifact(t,A,u,d){return Q(this,void 0,void 0,function*(){B.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)(t);const E=(0,I.getUploadSpecification)(t,u,A);const C={artifactName:t,artifactItems:[],size:0,failedItems:[]};const Q=new m.UploadHttpClient;if(E.length===0){B.warning(`No files found that can be uploaded`)}else{const A=yield Q.createArtifactInFileContainer(t,d);if(!A.fileContainerResourceUrl){B.debug(A.toString());throw new Error("No URL provided by the Artifact Service to upload an artifact to")}B.debug(`Upload Resource URL: ${A.fileContainerResourceUrl}`);B.info(`Container for artifact "${t}" successfully created. Starting upload of file(s)`);const u=yield Q.uploadArtifactToFileContainer(A.fileContainerResourceUrl,E,d);B.info(`File upload process has finished. Finalizing the artifact upload`);yield Q.patchArtifactSize(u.totalSize,t);if(u.failedItems.length>0){B.info(`Upload finished. There were ${u.failedItems.length} items that failed to upload`)}else{B.info(`Artifact has been finalized. All files have been successfully uploaded!`)}B.info(`\nThe raw size of all the files that were specified for upload is ${u.totalSize} bytes\nThe size of all the files that were uploaded is ${u.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=E.map(t=>t.absoluteFilePath);C.size=u.uploadSize;C.failedItems=u.failedItems}return C})}downloadArtifact(t,A,u){return Q(this,void 0,void 0,function*(){const d=new D.DownloadHttpClient;const E=yield d.listArtifacts();if(E.count===0){throw new Error(`Unable to find any artifacts for the associated workflow`)}const C=E.value.find(A=>A.name===t);if(!C){throw new Error(`Unable to find an artifact with the name: ${t}`)}const Q=yield d.getContainerItems(C.name,C.fileContainerResourceUrl);if(!A){A=(0,k.getWorkSpaceDirectory)()}A=(0,S.normalize)(A);A=(0,S.resolve)(A);const I=(0,R.getDownloadSpecification)(t,Q.value,A,(u===null||u===void 0?void 0:u.createArtifactFolder)||false);if(I.filesToDownload.length===0){B.info(`No downloadable files were found for the artifact: ${C.name}`)}else{yield(0,y.createDirectoriesForArtifact)(I.directoryStructure);B.info("Directory structure has been set up for the artifact");yield(0,y.createEmptyFilesForArtifact)(I.emptyFilesToCreate);yield d.downloadSingleArtifact(I.filesToDownload)}return{artifactName:t,downloadPath:I.rootDownloadLocation}})}downloadAllArtifacts(t){return Q(this,void 0,void 0,function*(){const A=new D.DownloadHttpClient;const u=[];const d=yield A.listArtifacts();if(d.count===0){B.info("Unable to find any artifacts for the associated workflow");return u}if(!t){t=(0,k.getWorkSpaceDirectory)()}t=(0,S.normalize)(t);t=(0,S.resolve)(t);let E=0;while(E<d.count){const C=d.value[E];E+=1;B.info(`starting download of artifact ${C.name} : ${E}/${d.count}`);const Q=yield A.getContainerItems(C.name,C.fileContainerResourceUrl);const I=(0,R.getDownloadSpecification)(C.name,Q.value,t,true);if(I.filesToDownload.length===0){B.info(`No downloa
Line 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
|
||||||
*/
|
*/
|
||||||
e.exports=s(1813)},4096:(e,t,s)=>{"use strict";
|
t.exports=u(7080)},3674:(t,A,u)=>{
|
||||||
/*!
|
/*!
|
||||||
* 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 A=/^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&&A.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 A=r.extensions;if(!A||!A.length){return}e[n]=A;for(var o=0;o<A.length;o++){var a=A[o];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 A=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 o=/^(-?(?:\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(!o){return}var a=parseFloat(o[1]);var c=(o[2]||"ms").toLowerCase();switch(c){case"years":case"year":case"yrs":case"yr":case"y":return a*A;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 A=t.port;if(typeof r!=="string"||!r||typeof s!=="string"){return""}s=s.split(":",1)[0];r=r.replace(/:\d*$/,"");A=parseInt(A)||n[s]||0;if(!shouldProxy(r,A)){return""}var o=getEnv("npm_config_"+s+"_proxy")||getEnv(s+"_proxy")||getEnv("npm_config_proxy")||getEnv("all_proxy");if(o&&o.indexOf("://")===-1){o=s+"://"+o}return o}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 A=i?parseInt(i[2]):0;if(A&&A!==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 A=s(2830);const o=s(3106);const a=s(4922);const c=s(9023);const l=s(9896);const u=s(9557);const h=s(6928);function _interopDefaultLegacy(e){return e&&typeof e==="object"&&"default"in e?e:{default:e}}const p=_interopDefaultLegacy(i);const d=_interopDefaultLegacy(n);const g=_interopDefaultLegacy(A);const f=_interopDefaultLegacy(o);const E=_interopDefaultLegacy(a);const C=_interopDefaultLegacy(c);const Q=_interopDefaultLegacy(l);const B=_interopDefaultLegacy(u);const I=_interopDefaultLegacy(h);const y=g["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
|
*/
|
||||||
/*! 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]);u=0;let r=false;for(const[t,A]of e){if(typeof A==="string"){const e=D.encode(s+`; name="${escape(normalizeLinefeeds(t))}"`+`\r\n\r\n${normalizeLinefeeds(A)}\r\n`);i.push(e);u+=e.byteLength}else{const e=D.encode(`${s}; name="${escape(normalizeLinefeeds(t))}"`+(A.name?`; filename="${escape(A.name)}"`:"")+"\r\n"+`Content-Type: ${A.type||"application/octet-stream"}\r\n\r\n`);i.push(e,A,n);if(typeof A.size==="number"){u+=e.byteLength+A.size+n.byteLength}else{r=true}}}const A=D.encode(`--${t}--`);i.push(A);u+=A.byteLength;if(r){u=null}l=e;c=async function*(){for(const e of i){if(e.stream){yield*e.stream()}else{yield e}}};h="multipart/form-data; boundary="+t}else if(A(e)){l=e;u=e.size;if(e.type){h=e.type}}else if(typeof e[Symbol.asyncIterator]==="function"){if(t){throw new TypeError("keepalive")}if(n.isDisturbed(e)||e.locked){throw new TypeError("Response body object should not be disturbed or locked")}i=e instanceof R?e:r(e)}if(typeof l==="string"||n.isBuffer(l)){u=Buffer.byteLength(l)}if(c!=null){let t;i=new R({async start(){t=c(e)[Symbol.asyncIterator]()},async pull(e){const{value:s,done:n}=await t.next();if(n){queueMicrotask((()=>{e.close()}))}else{if(!B(i)){e.enqueue(new Uint8Array(s))}}return e.desiredSize>0},async cancel(e){await t.return()},type:undefined})}const p={stream:i,source:l,length:u};return[p,h]}function safelyExtractBody(e,t=false){if(!R){R=s(3774).ReadableStream}if(e instanceof R){Q(!n.isDisturbed(e),"The body has already been consumed.");Q(!e.locked,"The stream is locked.")}return extractBody(e,t)}function cloneBody(e){const[t,s]=e.stream.tee();const i=g(s,{transfer:[s]});const[,n]=i.tee();e.stream=t;return{stream:n,length:e.length,source:e.source}}async function*consumeBody(e){if(e){if(I(e)){yield e}else{const t=e.stream;if(n.isDisturbed(t)){throw new TypeError("The body has already been consumed.")}if(t.locked){throw new TypeError("The stream is locked.")}t[C]=true;yield*t}}}function throwIfAborted(e){if(e.aborted){throw new d("The operation was aborted.","AbortError")}}function bodyMixinMethods(e){const t={blob(){return specConsumeBody(this,(e=>{let t=bodyMimeType(this);if(t==="failure"){t=""}else if(t){t=v(t)}return new f([e],{type:t})}),e)},arrayBuffer(){return specConsumeBody(this,(e=>new Uint8Array(e).buffer),e)},text(){return specConsumeBody(this,utf8DecodeBytes,e)},json(){return specConsumeBody(this,parseJSONFromBytes,e)},async formData(){p.brandCheck(this,e);throwIfAborted(this[h]);const t=this.headers.get("Content-Type");if(/multipart\/form-data/.test(t)){const e={};for(const[t,s]of this.headers)e[t.toLowerCase()]=s;const t=new u;let s;try{s=new i({headers:e,preservePath:true})}catch(e){throw new d(`${e}`,"AbortError")}s.on("field",((e,s)=>{t.append(e,s)}));s.on("file",((e,s,i,n,r)=>{const A=[];if(n==="base64"||n.toLowerCase()==="base64"){let n="";s.on("data",(e=>{n+=e.toString().replace(/[\r\n]/gm,"");const t=n.length-n.length%4;A.push(Buffer.from(n.slice(0,t),"base64"));n=n.slice(t)}));s.on("end",(()=>{A.push(Buffer.from(n,"base64"));t.append(e,new k(A,i,{type:r}))}))}else{s.on("data",(e=>{A.push(e)}));s.on("end",(()=>{t.append(e,new k(A,i,{type:r}))}))}}));const n=new Promise(((e,t)=>{s.on("finish",e);s.on("error",(e=>t(new TypeError(e))))}));if(this.body!==null)for await(const e of consumeBody(this[h].body))s.write(e);s.end();await n;return t}else if(/application\/x-www-form-urlencoded/.test(t)){let e;try{let t="";const s=new TextDecoder("utf-8",{ignoreBOM:true});for await(const e of consumeBody(this[h].body)){if(!I(e)){throw new TypeError("Expected Uint8Array chunk")}t+=s.decode(e,{stream:true})}t+=s.decode();e=new URLSearchParams(t)}catch(e){throw Object.assign(new TypeError,{cause:e})}const t=new u;for(const[s,i]of e){t.append(s,i)}return t}else{await Promise.resolve();throwIfAborted(this[h]);throw p.errors.exception({header:`${e.name}.formData`,message:"Could not parse content as FormData."})}}};return t}function mixinBody(e){Object.assign(e.prototype,bodyMixinMethods(e))}async function specConsumeBody(e,t,s){p.brandCheck(e,s);throwIfAborted(e[h]);if(bodyUnusable(e[h].body)){throw new TypeError("Body is unusable")}const i=c();const errorSteps=e=>i.reject(e);const successSteps=e=>{try{i.resolve(t(e))}catch(e){errorSteps(e)}};if(e[h].body==null){successSteps(new Uint8Array);return i.promise}await l(e[h].body,successSteps,errorSteps);return i.promise}function bodyUnusable(e){return e!=null&&(e.stream.locked||n.isDisturbed(e.stream))}function utf8DecodeBytes(e){if(e.length===0){return""}if(e[0]===239&&e[1]===187&&e[2]===191){e=e.subarray(3)}const t=S.decode(e);return t}function parseJSONFromBytes(e){return JSON.parse(utf8DecodeBytes(e))}function bodyMimeType(e){const{headersList:t}=e[h];const s=t.get("content-Line truncated
|
var d=u(8975);var E=u(6928).extname;var C=/^\s*([^;\s]*)(?:;|\s|$)/;var Q=/^text\//i;A.charset=charset;A.charsets={lookup:charset};A.contentType=contentType;A.extension=extension;A.extensions=Object.create(null);A.lookup=lookup;A.types=Object.create(null);populateMaps(A.extensions,A.types);function charset(t){if(!t||typeof t!=="string"){return false}var A=C.exec(t);var u=A&&d[A[1].toLowerCase()];if(u&&u.charset){return u.charset}if(A&&Q.test(A[1])){return"UTF-8"}return false}function contentType(t){if(!t||typeof t!=="string"){return false}var u=t.indexOf("/")===-1?A.lookup(t):t;if(!u){return false}if(u.indexOf("charset")===-1){var d=A.charset(u);if(d)u+="; charset="+d.toLowerCase()}return u}function extension(t){if(!t||typeof t!=="string"){return false}var u=C.exec(t);var d=u&&A.extensions[u[1].toLowerCase()];if(!d||!d.length){return false}return d[0]}function lookup(t){if(!t||typeof t!=="string"){return false}var u=E("x."+t).toLowerCase().substr(1);if(!u){return false}return A.types[u]||false}function populateMaps(t,A){var u=["nginx","apache",undefined,"iana"];Object.keys(d).forEach(function forEachMimeType(E){var C=d[E];var Q=C.extensions;if(!Q||!Q.length){return}t[E]=Q;for(var B=0;B<Q.length;B++){var I=Q[B];if(A[I]){var m=u.indexOf(d[A[I]].source);var y=u.indexOf(C.source);if(A[I]!=="application/octet-stream"&&(m>y||m===y&&A[I].substr(0,12)==="application/")){continue}}A[I]=E}})}},4023:t=>{var A=1e3;var u=A*60;var d=u*60;var E=d*24;var C=E*7;var Q=E*365.25;t.exports=function(t,A){A=A||{};var u=typeof t;if(u==="string"&&t.length>0){return parse(t)}else if(u==="number"&&isFinite(t)){return A.long?fmtLong(t):fmtShort(t)}throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))};function parse(t){t=String(t);if(t.length>100){return}var B=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(t);if(!B){return}var I=parseFloat(B[1]);var m=(B[2]||"ms").toLowerCase();switch(m){case"years":case"year":case"yrs":case"yr":case"y":return I*Q;case"weeks":case"week":case"w":return I*C;case"days":case"day":case"d":return I*E;case"hours":case"hour":case"hrs":case"hr":case"h":return I*d;case"minutes":case"minute":case"mins":case"min":case"m":return I*u;case"seconds":case"second":case"secs":case"sec":case"s":return I*A;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return I;default:return undefined}}function fmtShort(t){var C=Math.abs(t);if(C>=E){return Math.round(t/E)+"d"}if(C>=d){return Math.round(t/d)+"h"}if(C>=u){return Math.round(t/u)+"m"}if(C>=A){return Math.round(t/A)+"s"}return t+"ms"}function fmtLong(t){var C=Math.abs(t);if(C>=E){return plural(t,C,E,"day")}if(C>=d){return plural(t,C,d,"hour")}if(C>=u){return plural(t,C,u,"minute")}if(C>=A){return plural(t,C,A,"second")}return t+" ms"}function plural(t,A,u,d){var E=A>=u*1.5;return Math.round(t/u)+" "+d+(E?"s":"")}},4147:(t,A,u)=>{var d=u(7016).parse;var E={ftp:21,gopher:70,http:80,https:443,ws:80,wss:443};var C=String.prototype.endsWith||function(t){return t.length<=this.length&&this.indexOf(t,this.length-t.length)!==-1};function getProxyForUrl(t){var A=typeof t==="string"?d(t):t||{};var u=A.protocol;var C=A.host;var Q=A.port;if(typeof C!=="string"||!C||typeof u!=="string"){return""}u=u.split(":",1)[0];C=C.replace(/:\d*$/,"");Q=parseInt(Q)||E[u]||0;if(!shouldProxy(C,Q)){return""}var B=getEnv("npm_config_"+u+"_proxy")||getEnv(u+"_proxy")||getEnv("npm_config_proxy")||getEnv("all_proxy");if(B&&B.indexOf("://")===-1){B=u+"://"+B}return B}function shouldProxy(t,A){var u=(getEnv("npm_config_no_proxy")||getEnv("no_proxy")).toLowerCase();if(!u){return true}if(u==="*"){return false}return u.split(/[,\s]/).every(function(u){if(!u){return true}var d=u.match(/^(.+):(\d+)$/);var E=d?d[1]:u;var Q=d?parseInt(d[2]):0;if(Q&&Q!==A){return true}if(!/^[.*]/.test(E)){return t!==E}if(E.charAt(0)==="*"){E=E.slice(1)}return!C.call(t,E)})}function getEnv(t){return process.env[t.toLowerCase()]||process.env[t.toUpperCase()]||""}A.T=getProxyForUrl},4398:(t,A,u)=>{var d;d={value:true};const E=u(9105);const C=u(7016);const Q=u(4434);const B=u(1141);const I=u(3106);const m=u(6550);const y=u(9023);const b=u(9896);const D=u(7967);const R=u(6928);function _interopDefaultLegacy(t){return t&&typeof t==="object"&&"default"in t?t:{default:t}}const k=_interopDefaultLegacy(E);const S=_interopDefaultLegacy(C);const v=_interopDefaultLegacy(B);const x=_interopDefaultLegacy(I);const N=_interopDefaultLegacy(m);const L=_interopDefaultLegacy(y);const M=_interopDefaultLegacy(b);const T=_interopDefaultLegacy(D);const H=_interopDefaultLegacy(R);const G=v["default"]("screepsapi:socket");const Y={reconnect:true,resubscribe:true,keepAlive:true,maxRetries:10,maxRetryDelay:60*1e3};class Socket extends Q.EventEmitter{constructor(t){super();this.api=t;this.opts=Object.assign({},Y);this.on("error",()=>{});this.reset();this.on("auth",t=>{if(t.data.status==="ok"){while(this.__queue.length)Line truncated
|
||||||
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */r[n-4]=this.maskKey[0];r[n-3]=this.maskKey[1];r[n-2]=this.maskKey[2];r[n-1]=this.maskKey[3];r[1]=s;if(s===126){r.writeUInt16BE(t,2)}else if(s===127){r[2]=r[3]=0;r.writeUIntBE(t,4,6)}r[1]|=128;for(let e=0;e<t;e++){r[n+e]=this.frameData[e]^this.maskKey[e%4]}return r}}e.exports={WebsocketFrameSend:WebsocketFrameSend}},3171:(e,t,s)=>{"use strict";const{Writable:i}=s(2203);const n=s(1637);const{parserStates:r,opcodes:A,states:o,emptyBuffer:a}=s(5913);const{kReadyState:c,kSentClose:l,kResponse:u,kReceivedClose:h}=s(2933);const{isValidStatusCode:p,failWebsocketConnection:d,websocketMessageReceived:g}=s(3574);const{WebsocketFrameSend:f}=s(1237);const E={};E.ping=n.channel("undici:websocket:ping");E.pong=n.channel("undici:websocket:pong");class ByteParser extends i{#A=[];#o=0;#a=r.INFO;#c={};#l=[];constructor(e){super();this.ws=e}_write(e,t,s){this.#A.push(e);this.#o+=e.length;this.run(s)}run(e){while(true){if(this.#a===r.INFO){if(this.#o<2){return e()}const t=this.consume(2);this.#c.fin=(t[0]&128)!==0;this.#c.opcode=t[0]&15;this.#c.originalOpcode??=this.#c.opcode;this.#c.fragmented=!this.#c.fin&&this.#c.opcode!==A.CONTINUATION;if(this.#c.fragmented&&this.#c.opcode!==A.BINARY&&this.#c.opcode!==A.TEXT){d(this.ws,"Invalid frame type was fragmented.");return}const s=t[1]&127;if(s<=125){this.#c.payloadLength=s;this.#a=r.READ_DATA}else if(s===126){this.#a=r.PAYLOADLENGTH_16}else if(s===127){this.#a=r.PAYLOADLENGTH_64}if(this.#c.fragmented&&s>125){d(this.ws,"Fragmented frame exceeded 125 bytes.");return}else if((this.#c.opcode===A.PING||this.#c.opcode===A.PONG||this.#c.opcode===A.CLOSE)&&s>125){d(this.ws,"Payload length for control frame exceeded 125 bytes.");return}else if(this.#c.opcode===A.CLOSE){if(s===1){d(this.ws,"Received close frame with a 1-byte body.");return}const e=this.consume(s);this.#c.closeInfo=this.parseCloseBody(false,e);if(!this.ws[l]){const e=Buffer.allocUnsafe(2);e.writeUInt16BE(this.#c.closeInfo.code,0);const t=new f(e);this.ws[u].socket.write(t.createFrame(A.CLOSE),(e=>{if(!e){this.ws[l]=true}}))}this.ws[c]=o.CLOSING;this.ws[h]=true;this.end();return}else if(this.#c.opcode===A.PING){const t=this.consume(s);if(!this.ws[h]){const e=new f(t);this.ws[u].socket.write(e.createFrame(A.PONG));if(E.ping.hasSubscribers){E.ping.publish({payload:t})}}this.#a=r.INFO;if(this.#o>0){continue}else{e();return}}else if(this.#c.opcode===A.PONG){const t=this.consume(s);if(E.pong.hasSubscribers){E.pong.publish({payload:t})}if(this.#o>0){continue}else{e();return}}}else if(this.#a===r.PAYLOADLENGTH_16){if(this.#o<2){return e()}const t=this.consume(2);this.#c.payloadLength=t.readUInt16BE(0);this.#a=r.READ_DATA}else if(this.#a===r.PAYLOADLENGTH_64){if(this.#o<8){return e()}const t=this.consume(8);const s=t.readUInt32BE(0);if(s>2**31-1){d(this.ws,"Received payload length > 2^31 bytes.");return}const i=t.readUInt32BE(4);this.#c.payloadLength=(s<<8)+i;this.#a=r.READ_DATA}else if(this.#a===r.READ_DATA){if(this.#o<this.#c.payloadLength){return e()}else if(this.#o>=this.#c.payloadLength){const e=this.consume(this.#c.payloadLength);this.#l.push(e);if(!this.#c.fragmented||this.#c.fin&&this.#c.opcode===A.CONTINUATION){const e=Buffer.concat(this.#l);g(this.ws,this.#c.originalOpcode,e);this.#c={};this.#l.length=0}this.#a=r.INFO}}if(this.#o>0){continue}else{e();break}}}consume(e){if(e>this.#o){return null}else if(e===0){return a}if(this.#A[0].length===e){this.#o-=this.#A[0].length;return this.#A.shift()}const t=Buffer.allocUnsafe(e);let s=0;while(s!==e){const i=this.#A[0];const{length:n}=i;if(n+s===e){t.set(this.#A.shift(),s);break}else if(n+s>e){t.set(i.subarray(0,e-s),s);this.#A[0]=i.subarray(e-s);break}else{t.set(this.#A.shift(),s);s+=i.length}}this.#o-=e;return t}parseCloseBody(e,t){let s;if(t.length>=2){s=t.readUInt16BE(0)}if(e){if(!p(s)){return null}return{code:s}}let i=t.subarray(2);if(i[0]===239&&i[1]===187&&i[2]===191){i=i.subarray(3)}if(s!==undefined&&!p(s)){return null}try{i=new TextDecoder("utf-8",{fatal:true}).decode(i)}catch{return null}return{code:s,reason:i}}get closingInfo(){return this.#c.closeInfo}}e.exports={ByteParser:ByteParser}},2933:e=>{"use strict";e.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")}},3574:(e,t,s)=>{"use strict";const{kReadyState:i,kController:n,kResponse:r,kBinaryType:A,kWebSocketURL:o}=s(2933);const{states:a,opcodes:c}=s(5913);const{MessageEvent:l,ErrorEvent:u}=s(6255);function isEstablished(e){return e[i]===a.OPEN}function isClosing(e){return e[i]===a.CLOSING}function isClosed(e){return e[i]===a.CLOSED}function fireEvent(e,t,s=Event,i){const n=new s(e,i);t.dispatchEvent(n)}function websocketMessageReceived(e,t,s){if(e[i]!==a.OPEN){return}let n;if(t===c.TEXT){try{n=new TextDecoder("utf-8",{fatal:true}).decode(s)Line truncated
|
/*!
|
||||||
|
* Tmp
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011-2017 KARASZI Istvan <github@spam.raszi.hu>
|
||||||
|
*
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
const d=u(9896);const E=u(857);const C=u(6928);const Q=u(6982);const B={fs:d.constants,os:E.constants};const I="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",m=/XXXXXX/,y=3,b=(B.O_CREAT||B.fs.O_CREAT)|(B.O_EXCL||B.fs.O_EXCL)|(B.O_RDWR||B.fs.O_RDWR),D=E.platform()==="win32",R=B.EBADF||B.os.errno.EBADF,k=B.ENOENT||B.os.errno.ENOENT,S=448,v=384,x="exit",N=[],L=d.rmdirSync.bind(d);let M=false;function rimraf(t,A){return d.rm(t,{recursive:true},A)}function FN_RIMRAF_SYNC(t){return d.rmSync(t,{recursive:true})}function tmpName(t,A){const u=_parseArguments(t,A),E=u[0],C=u[1];_assertAndSanitizeOptions(E,function(t,A){if(t)return C(t);let u=A.tries;(function _getUniqueName(){try{const t=_generateTmpName(A);d.stat(t,function(A){if(!A){if(u-- >0)return _getUniqueName();return C(new Error("Could not get a unique tmp filename, max tries reached "+t))}C(null,t)})}catch(t){C(t)}})()})}function tmpNameSync(t){const A=_parseArguments(t),u=A[0];const E=_assertAndSanitizeOptionsSync(u);let C=E.tries;do{const t=_generateTmpName(E);try{d.statSync(t)}catch(A){return t}}while(C-- >0);throw new Error("Could not get a unique tmp filename, max tries reached")}function file(t,A){const u=_parseArguments(t,A),E=u[0],C=u[1];tmpName(E,function _tmpNameCreated(t,A){if(t)return C(t);d.open(A,b,E.mode||v,function _fileCreated(t,u){if(t)return C(t);if(E.discardDescriptor){return d.close(u,function _discardCallback(t){return C(t,A,undefined,_prepareTmpFileRemoveCallback(A,-1,E,false))})}else{const t=E.discardDescriptor||E.detachDescriptor;C(null,A,u,_prepareTmpFileRemoveCallback(A,t?-1:u,E,false))}})})}function fileSync(t){const A=_parseArguments(t),u=A[0];const E=u.discardDescriptor||u.detachDescriptor;const C=tmpNameSync(u);let Q=d.openSync(C,b,u.mode||v);if(u.discardDescriptor){d.closeSync(Q);Q=undefined}return{name:C,fd:Q,removeCallback:_prepareTmpFileRemoveCallback(C,E?-1:Q,u,true)}}function dir(t,A){const u=_parseArguments(t,A),E=u[0],C=u[1];tmpName(E,function _tmpNameCreated(t,A){if(t)return C(t);d.mkdir(A,E.mode||S,function _dirCreated(t){if(t)return C(t);C(null,A,_prepareTmpDirRemoveCallback(A,E,false))})})}function dirSync(t){const A=_parseArguments(t),u=A[0];const E=tmpNameSync(u);d.mkdirSync(E,u.mode||S);return{name:E,removeCallback:_prepareTmpDirRemoveCallback(E,u,true)}}function _removeFileAsync(t,A){const _handler=function(t){if(t&&!_isENOENT(t)){return A(t)}A()};if(0<=t[0])d.close(t[0],function(){d.unlink(t[1],_handler)});else d.unlink(t[1],_handler)}function _removeFileSync(t){let A=null;try{if(0<=t[0])d.closeSync(t[0])}catch(t){if(!_isEBADF(t)&&!_isENOENT(t))throw t}finally{try{d.unlinkSync(t[1])}catch(t){if(!_isENOENT(t))A=t}}if(A!==null){throw A}}function _prepareTmpFileRemoveCallback(t,A,u,d){const E=_prepareRemoveCallback(_removeFileSync,[A,t],d);const C=_prepareRemoveCallback(_removeFileAsync,[A,t],d,E);if(!u.keep)N.unshift(E);return d?E:C}function _prepareTmpDirRemoveCallback(t,A,u){const E=A.unsafeCleanup?rimraf:d.rmdir.bind(d);const C=A.unsafeCleanup?FN_RIMRAF_SYNC:L;const Q=_prepareRemoveCallback(C,t,u);const B=_prepareRemoveCallback(E,t,u,Q);if(!A.keep)N.unshift(Q);return u?Q:B}function _prepareRemoveCallback(t,A,u,d){let E=false;return function _cleanupCallback(C){if(!E){const Q=d||_cleanupCallback;const B=N.indexOf(Q);if(B>=0)N.splice(B,1);E=true;if(u||t===L||t===FN_RIMRAF_SYNC){return t(A)}else{return t(A,C||function(){})}}}}function _garbageCollector(){if(!M)return;while(N.length){try{N[0]()}catch(t){}}}function _randomChars(t){let A=[],u=null;try{u=Q.randomBytes(t)}catch(A){u=Q.pseudoRandomBytes(t)}for(let d=0;d<t;d++){A.push(I[u[d]%I.length])}return A.join("")}function _isUndefined(t){return typeof t==="undefined"}function _parseArguments(t,A){if(typeof t==="function"){return[{},t]}if(_isUndefined(t)){return[{},A]}const u={};for(const A of Object.getOwnPropertyNames(t)){u[A]=t[A]}return[u,A]}function _resolvePath(t,A,u){const E=C.isAbsolute(t)?t:C.join(A,t);d.stat(E,function(t){if(t){d.realpath(C.dirname(E),function(t,A){if(t)return u(t);u(null,C.join(A,C.basename(E)))})}else{d.realpath(E,u)}})}function _resolvePathSync(t,A){const u=C.isAbsolute(t)?t:C.join(A,t);try{d.statSync(u);return d.realpathSync(u)}catch(t){const A=d.realpathSync(C.dirname(u));return C.join(A,C.basename(u))}}function _generateTmpName(t){const A=t.tmpdir;if(!_isUndefined(t.name)){return C.join(A,t.dir,t.name)}if(!_isUndefined(t.template)){return C.join(A,t.dir,t.template).replace(m,_randomChars(6))}const u=[t.prefix?t.prefix:"tmp","-",process.pid,"-",_randomChars(12),t.postfix?"-"+t.postfix:""].join("");return C.join(A,t.dir,u)}function _assertPath(t,A){if(typeof A!=="string"){throw new Error(`${t} option must be a string, got "${typeof A}".`)}if(A.includes("..")){throw new Error("Relative value not allowed")}return A}function _assertOptionsBase(t){if(!_isUndefined(t.name)){const A=t.name;if(C.isAbsolute(A))throw new Error(`name option must not contain an absolute path, found "${A}".`);const u=C.basename(A);if(u===".."Line truncated
|
||||||
|
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */;const escape=t=>t.replace(/\n/g,"%0A").replace(/\r/g,"%0D").replace(/"/g,"%22");const normalizeLinefeeds=t=>t.replace(/\r?\n|\r/g,"\r\n");const d=[];const E=new Uint8Array([13,10]);b=0;let C=false;for(const[A,Q]of t){if(typeof Q==="string"){const t=q.encode(u+`; name="${escape(normalizeLinefeeds(A))}"`+`\r\n\r\n${normalizeLinefeeds(Q)}\r\n`);d.push(t);b+=t.byteLength}else{const t=q.encode(`${u}; name="${escape(normalizeLinefeeds(A))}"`+(Q.name?`; filename="${escape(Q.name)}"`:"")+"\r\n"+`Content-Type: ${Q.type||"application/octet-stream"}\r\n\r\n`);d.push(t,Q,E);if(typeof Q.size==="number"){b+=t.byteLength+Q.size+E.byteLength}else{C=true}}}const Q=q.encode(`--${A}--`);d.push(Q);b+=Q.byteLength;if(C){b=null}y=t;m=async function*(){for(const t of d){if(t.stream){yield*t.stream()}else{yield t}}};D="multipart/form-data; boundary="+A}else if(Q(t)){y=t;b=t.size;if(t.type){D=t.type}}else if(typeof t[Symbol.asyncIterator]==="function"){if(A){throw new TypeError("keepalive")}if(E.isDisturbed(t)||t.locked){throw new TypeError("Response body object should not be disturbed or locked")}d=t instanceof P?t:C(t)}if(typeof y==="string"||E.isBuffer(y)){b=Buffer.byteLength(y)}if(m!=null){let A;d=new P({async start(){A=m(t)[Symbol.asyncIterator]()},async pull(t){const{value:u,done:E}=await A.next();if(E){queueMicrotask(()=>{t.close()})}else{if(!M(d)){t.enqueue(new Uint8Array(u))}}return t.desiredSize>0},async cancel(t){await A.return()},type:undefined})}const R={stream:d,source:y,length:b};return[R,D]}function safelyExtractBody(t,A=false){if(!P){P=u(3774).ReadableStream}if(t instanceof P){L(!E.isDisturbed(t),"The body has already been consumed.");L(!t.locked,"The stream is locked.")}return extractBody(t,A)}function cloneBody(t){const[A,u]=t.stream.tee();const d=S(u,{transfer:[u]});const[,E]=d.tee();t.stream=A;return{stream:E,length:t.length,source:t.source}}async function*consumeBody(t){if(t){if(T(t)){yield t}else{const A=t.stream;if(E.isDisturbed(A)){throw new TypeError("The body has already been consumed.")}if(A.locked){throw new TypeError("The stream is locked.")}A[N]=true;yield*A}}}function throwIfAborted(t){if(t.aborted){throw new k("The operation was aborted.","AbortError")}}function bodyMixinMethods(t){const A={blob(){return specConsumeBody(this,t=>{let A=bodyMimeType(this);if(A==="failure"){A=""}else if(A){A=O(A)}return new v([t],{type:A})},t)},arrayBuffer(){return specConsumeBody(this,t=>new Uint8Array(t).buffer,t)},text(){return specConsumeBody(this,utf8DecodeBytes,t)},json(){return specConsumeBody(this,parseJSONFromBytes,t)},async formData(){R.brandCheck(this,t);throwIfAborted(this[D]);const A=this.headers.get("Content-Type");if(/multipart\/form-data/.test(A)){const t={};for(const[A,u]of this.headers)t[A.toLowerCase()]=u;const A=new b;let u;try{u=new d({headers:t,preservePath:true})}catch(t){throw new k(`${t}`,"AbortError")}u.on("field",(t,u)=>{A.append(t,u)});u.on("file",(t,u,d,E,C)=>{const Q=[];if(E==="base64"||E.toLowerCase()==="base64"){let E="";u.on("data",t=>{E+=t.toString().replace(/[\r\n]/gm,"");const A=E.length-E.length%4;Q.push(Buffer.from(E.slice(0,A),"base64"));E=E.slice(A)});u.on("end",()=>{Q.push(Buffer.from(E,"base64"));A.append(t,new W(Q,d,{type:C}))})}else{u.on("data",t=>{Q.push(t)});u.on("end",()=>{A.append(t,new W(Q,d,{type:C}))})}});const E=new Promise((t,A)=>{u.on("finish",t);u.on("error",t=>A(new TypeError(t)))});if(this.body!==null)for await(const t of consumeBody(this[D].body))u.write(t);u.end();await E;return A}else if(/application\/x-www-form-urlencoded/.test(A)){let t;try{let A="";const u=new TextDecoder("utf-8",{ignoreBOM:true});for await(const t of consumeBody(this[D].body)){if(!T(t)){throw new TypeError("Expected Uint8Array chunk")}A+=u.decode(t,{stream:true})}A+=u.decode();t=new URLSearchParams(A)}catch(t){throw Object.assign(new TypeError,{cause:t})}const A=new b;for(const[u,d]of t){A.append(u,d)}return A}else{await Promise.resolve();throwIfAborted(this[D]);throw R.errors.exception({header:`${t.name}.formData`,message:"Could not parse content as FormData."})}}};return A}function mixinBody(t){Object.assign(t.prototype,bodyMixinMethods(t))}async function specConsumeBody(t,A,u){R.brandCheck(t,u);throwIfAborted(t[D]);if(bodyUnusable(t[D].body)){throw new TypeError("Body is unusable")}const d=m();const errorSteps=t=>d.reject(t);const successSteps=t=>{try{d.resolve(A(t))}catch(t){errorSteps(t)}};if(t[D].body==null){successSteps(new Uint8Array);return d.promise}await y(t[D].body,successSteps,errorSteps);return d.promise}function bodyUnusable(t){return t!=null&&(t.stream.locked||E.isDisturbed(t.stream))}function utf8DecodeBytes(t){if(t.length===0){return""}if(t[0]===239&&t[1]===187&&t[2]===191){t=t.subarray(3)}const A=j.decode(t);return A}function parseJSONFromBytes(t){return JSON.parse(utf8DecodeBytes(t))}function bodyMimeType(t){const{headersList:A}=t[D];const u=A.get("content-type");if(u===null){reLine truncated
|
||||||
|
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */C[E-4]=this.maskKey[0];C[E-3]=this.maskKey[1];C[E-2]=this.maskKey[2];C[E-1]=this.maskKey[3];C[1]=u;if(u===126){C.writeUInt16BE(A,2)}else if(u===127){C[2]=C[3]=0;C.writeUIntBE(A,4,6)}C[1]|=128;for(let t=0;t<A;t++){C[E+t]=this.frameData[t]^this.maskKey[t%4]}return C}}t.exports={WebsocketFrameSend:WebsocketFrameSend}},5080:(t,A,u)=>{const{Writable:d}=u(2203);const E=u(1637);const{parserStates:C,opcodes:Q,states:B,emptyBuffer:I}=u(932);const{kReadyState:m,kSentClose:y,kResponse:b,kReceivedClose:D}=u(327);const{isValidStatusCode:R,failWebsocketConnection:k,websocketMessageReceived:S}=u(1501);const{WebsocketFrameSend:v}=u(9140);const x={};x.ping=E.channel("undici:websocket:ping");x.pong=E.channel("undici:websocket:pong");class ByteParser extends d{#n=[];#o=0;#a=C.INFO;#c={};#l=[];constructor(t){super();this.ws=t}_write(t,A,u){this.#n.push(t);this.#o+=t.length;this.run(u)}run(t){while(true){if(this.#a===C.INFO){if(this.#o<2){return t()}const A=this.consume(2);this.#c.fin=(A[0]&128)!==0;this.#c.opcode=A[0]&15;this.#c.originalOpcode??=this.#c.opcode;this.#c.fragmented=!this.#c.fin&&this.#c.opcode!==Q.CONTINUATION;if(this.#c.fragmented&&this.#c.opcode!==Q.BINARY&&this.#c.opcode!==Q.TEXT){k(this.ws,"Invalid frame type was fragmented.");return}const u=A[1]&127;if(u<=125){this.#c.payloadLength=u;this.#a=C.READ_DATA}else if(u===126){this.#a=C.PAYLOADLENGTH_16}else if(u===127){this.#a=C.PAYLOADLENGTH_64}if(this.#c.fragmented&&u>125){k(this.ws,"Fragmented frame exceeded 125 bytes.");return}else if((this.#c.opcode===Q.PING||this.#c.opcode===Q.PONG||this.#c.opcode===Q.CLOSE)&&u>125){k(this.ws,"Payload length for control frame exceeded 125 bytes.");return}else if(this.#c.opcode===Q.CLOSE){if(u===1){k(this.ws,"Received close frame with a 1-byte body.");return}const t=this.consume(u);this.#c.closeInfo=this.parseCloseBody(false,t);if(!this.ws[y]){const t=Buffer.allocUnsafe(2);t.writeUInt16BE(this.#c.closeInfo.code,0);const A=new v(t);this.ws[b].socket.write(A.createFrame(Q.CLOSE),t=>{if(!t){this.ws[y]=true}})}this.ws[m]=B.CLOSING;this.ws[D]=true;this.end();return}else if(this.#c.opcode===Q.PING){const A=this.consume(u);if(!this.ws[D]){const t=new v(A);this.ws[b].socket.write(t.createFrame(Q.PONG));if(x.ping.hasSubscribers){x.ping.publish({payload:A})}}this.#a=C.INFO;if(this.#o>0){continue}else{t();return}}else if(this.#c.opcode===Q.PONG){const A=this.consume(u);if(x.pong.hasSubscribers){x.pong.publish({payload:A})}if(this.#o>0){continue}else{t();return}}}else if(this.#a===C.PAYLOADLENGTH_16){if(this.#o<2){return t()}const A=this.consume(2);this.#c.payloadLength=A.readUInt16BE(0);this.#a=C.READ_DATA}else if(this.#a===C.PAYLOADLENGTH_64){if(this.#o<8){return t()}const A=this.consume(8);const u=A.readUInt32BE(0);if(u>2**31-1){k(this.ws,"Received payload length > 2^31 bytes.");return}const d=A.readUInt32BE(4);this.#c.payloadLength=(u<<8)+d;this.#a=C.READ_DATA}else if(this.#a===C.READ_DATA){if(this.#o<this.#c.payloadLength){return t()}else if(this.#o>=this.#c.payloadLength){const t=this.consume(this.#c.payloadLength);this.#l.push(t);if(!this.#c.fragmented||this.#c.fin&&this.#c.opcode===Q.CONTINUATION){const t=Buffer.concat(this.#l);S(this.ws,this.#c.originalOpcode,t);this.#c={};this.#l.length=0}this.#a=C.INFO}}if(this.#o>0){continue}else{t();break}}}consume(t){if(t>this.#o){return null}else if(t===0){return I}if(this.#n[0].length===t){this.#o-=this.#n[0].length;return this.#n.shift()}const A=Buffer.allocUnsafe(t);let u=0;while(u!==t){const d=this.#n[0];const{length:E}=d;if(E+u===t){A.set(this.#n.shift(),u);break}else if(E+u>t){A.set(d.subarray(0,t-u),u);this.#n[0]=d.subarray(t-u);break}else{A.set(this.#n.shift(),u);u+=d.length}}this.#o-=t;return A}parseCloseBody(t,A){let u;if(A.length>=2){u=A.readUInt16BE(0)}if(t){if(!R(u)){return null}return{code:u}}let d=A.subarray(2);if(d[0]===239&&d[1]===187&&d[2]===191){d=d.subarray(3)}if(u!==undefined&&!R(u)){return null}try{d=new TextDecoder("utf-8",{fatal:true}).decode(d)}catch{return null}return{code:u,reason:d}}get closingInfo(){return this.#c.closeInfo}}t.exports={ByteParser:ByteParser}},327:t=>{t.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:(t,A,u)=>{const{kReadyState:d,kController:E,kResponse:C,kBinaryType:Q,kWebSocketURL:B}=u(327);const{states:I,opcodes:m}=u(932);const{MessageEvent:y,ErrorEvent:b}=u(7952);function isEstablished(t){return t[d]===I.OPEN}function isClosing(t){return t[d]===I.CLOSING}function isClosed(t){return t[d]===I.CLOSED}function fireEvent(t,A,u=Event,d){const E=new u(t,d);A.dispatchEvent(E)}function websocketMessageReceived(t,A,u){if(t[d]!==I.OPEN){return}let E;if(A===m.TEXT){try{E=new TextDecoder("utf-8",{fatal:true}).decode(u)}catch{failWebsocketConnection(t,"Received invLine truncated
|
||||||
|
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */;const escape=t=>t.replace(/\n/g,"%0A").replace(/\r/g,"%0D").replace(/"/g,"%22");const normalizeLinefeeds=t=>t.replace(/\r?\n|\r/g,"\r\n");const d=[];const E=new Uint8Array([13,10]);y=0;let C=false;for(const[A,Q]of t){if(typeof Q==="string"){const t=G.encode(u+`; name="${escape(normalizeLinefeeds(A))}"`+`\r\n\r\n${normalizeLinefeeds(Q)}\r\n`);d.push(t);y+=t.byteLength}else{const t=G.encode(`${u}; name="${escape(normalizeLinefeeds(A))}"`+(Q.name?`; filename="${escape(Q.name)}"`:"")+"\r\n"+`Content-Type: ${Q.type||"application/octet-stream"}\r\n\r\n`);d.push(t,Q,E);if(typeof Q.size==="number"){y+=t.byteLength+Q.size+E.byteLength}else{C=true}}}const Q=G.encode(`--${A}--\r\n`);d.push(Q);y+=Q.byteLength;if(C){y=null}m=t;I=async function*(){for(const t of d){if(t.stream){yield*t.stream()}else{yield t}}};b=`multipart/form-data; boundary=${A}`}else if(C(t)){m=t;y=t.size;if(t.type){b=t.type}}else if(typeof t[Symbol.asyncIterator]==="function"){if(A){throw new TypeError("keepalive")}if(d.isDisturbed(t)||t.locked){throw new TypeError("Response body object should not be disturbed or locked")}u=t instanceof ReadableStream?t:E(t)}if(typeof m==="string"||d.isBuffer(m)){y=Buffer.byteLength(m)}if(I!=null){let A;u=new ReadableStream({async start(){A=I(t)[Symbol.asyncIterator]()},async pull(t){const{value:d,done:E}=await A.next();if(E){queueMicrotask(()=>{t.close();t.byobRequest?.respond(0)})}else{if(!x(u)){const A=new Uint8Array(d);if(A.byteLength){t.enqueue(A)}}}return t.desiredSize>0},async cancel(t){await A.return()},type:"bytes"})}const D={stream:u,source:m,length:y};return[D,b]}function safelyExtractBody(t,A=false){if(t instanceof ReadableStream){v(!d.isDisturbed(t),"The body has already been consumed.");v(!t.locked,"The stream is locked.")}return extractBody(t,A)}function cloneBody(t,A){const[u,d]=A.stream.tee();A.stream=u;return{stream:d,length:A.length,source:A.source}}function throwIfAborted(t){if(t.aborted){throw new DOMException("The operation was aborted.","AbortError")}}function bodyMixinMethods(t){const A={blob(){return consumeBody(this,t=>{let A=bodyMimeType(this);if(A===null){A=""}else if(A){A=M(A)}return new S([t],{type:A})},t)},arrayBuffer(){return consumeBody(this,t=>new Uint8Array(t).buffer,t)},text(){return consumeBody(this,b,t)},json(){return consumeBody(this,parseJSONFromBytes,t)},formData(){return consumeBody(this,t=>{const A=bodyMimeType(this);if(A!==null){switch(A.essence){case"multipart/form-data":{const u=T(t,A);if(u==="failure"){throw new TypeError("Failed to parse body as FormData.")}const d=new D;d[R]=u;return d}case"application/x-www-form-urlencoded":{const A=new URLSearchParams(t.toString());const u=new D;for(const[t,d]of A){u.append(t,d)}return u}}}throw new TypeError('Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".')},t)},bytes(){return consumeBody(this,t=>new Uint8Array(t),t)}};return A}function mixinBody(t){Object.assign(t.prototype,bodyMixinMethods(t))}async function consumeBody(t,A,u){k.brandCheck(t,u);if(bodyUnusable(t)){throw new TypeError("Body is unusable: Body has already been read")}throwIfAborted(t[R]);const d=I();const errorSteps=t=>d.reject(t);const successSteps=t=>{try{d.resolve(A(t))}catch(t){errorSteps(t)}};if(t[R].body==null){successSteps(Buffer.allocUnsafe(0));return d.promise}await m(t[R].body,successSteps,errorSteps);return d.promise}function bodyUnusable(t){const A=t[R].body;return A!=null&&(A.stream.locked||d.isDisturbed(A.stream))}function parseJSONFromBytes(t){return JSON.parse(b(t))}function bodyMimeType(t){const A=t[R].headersList;const u=y(A);if(u==="failure"){return null}return u}t.exports={extractBody:extractBody,safelyExtractBody:safelyExtractBody,cloneBody:cloneBody,mixinBody:mixinBody,streamRegistry:O,hasFinalizationRegistry:Y,bodyUnusable:bodyUnusable}},4578:t=>{const A=["GET","HEAD","POST"];const u=new Set(A);const d=[101,204,205,304];const E=[301,302,303,307,308];const C=new Set(E);const Q=["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 B=new Set(Q);const I=["","no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"];const m=new Set(I);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> */B[Q-4]=u[0];B[Q-3]=u[1];B[Q-2]=u[2];B[Q-1]=u[3];B[1]=C;if(C===126){B.writeUInt16BE(E,2)}else if(C===127){B[2]=B[3]=0;B.writeUIntBE(E,4,6)}B[1]|=128;for(let t=0;t<E;++t){B[Q+t]=A[t]^u[t&3]}return B}}t.exports={WebsocketFrameSend:WebsocketFrameSend}},6286:(t,A,u)=>{const{createInflateRaw:d,Z_DEFAULT_WINDOWBITS:E}=u(8522);const{isValidClientWindowBits:C}=u(8098);const{MessageSizeExceededError:Q}=u(6246);const B=Buffer.from([0,0,255,255]);const I=Symbol("kBuffer");const m=Symbol("kLength");class PerMessageDeflate{#re;#S={};#ie=0;constructor(t,A){this.#S.serverNoContextTakeover=t.has("server_no_context_takeover");this.#S.serverMaxWindowBits=t.get("server_max_window_bits");this.#ie=A.maxPayloadSize}decompress(t,A,u){if(!this.#re){let t=E;if(this.#S.serverMaxWindowBits){if(!C(this.#S.serverMaxWindowBits)){u(new Error("Invalid server_max_window_bits"));return}t=Number.parseInt(this.#S.serverMaxWindowBits)}try{this.#re=d({windowBits:t})}catch(t){u(t);return}this.#re[I]=[];this.#re[m]=0;this.#re.on("data",t=>{this.#re[m]+=t.length;if(this.#ie>0&&this.#re[m]>this.#ie){u(new Q);this.#re.removeAllListeners();this.#re=null;return}this.#re[I].push(t)});this.#re.on("error",t=>{this.#re=null;u(t)})}this.#re.write(t);if(A){this.#re.write(B)}this.#re.flush(()=>{if(!this.#re){return}const t=Buffer.concat(this.#re[I],this.#re[m]);this.#re[I].length=0;this.#re[m]=0;u(null,t)})}}t.exports={PerMessageDeflate:PerMessageDeflate}},5975:(t,A,u)=>{const{Writable:d}=u(7075);const E=u(4589);const{parserStates:C,opcodes:Q,states:B,emptyBuffer:I,sentCloseFrameState:m}=u(8397);const{kReadyState:y,kSentClose:b,kResponse:D,kReceivedClose:R}=u(7609);const{channels:k}=u(9978);const{isValidStatusCode:S,isValidOpcode:v,failWebsocketConnection:x,websocketMessageReceived:N,utf8Decode:L,isControlFrame:M,isTextBinaryFrame:T,isContinuationFrame:H}=u(8098);const{WebsocketFrameSend:G}=u(9321);const{closeWebSocketConnection:Y}=u(1874);const{PerMessageDeflate:O}=u(6286);const{MessageSizeExceededError:_}=u(6246);function failWebsocketConnectionWithCode(t,A,u){Y(t,A,u,Buffer.byteLength(u));x(t,u)}class ByteParser extends d{#n=[];#ne=0;#o=0;#oe=false;#a=C.INFO;#c={};#l=[];#d;#ae;#ie;constructor(t,A,u={}){super();this.ws=t;this.#d=A==null?new Map:A;this.#ae=u.maxFragments??0;this.#ie=u.maxPayloadSize??0;if(this.#d.has("permessage-deflate")){this.#d.set("permessage-deflate",new O(A,u))}}_write(t,A,u){this.#n.push(t);this.#o+=t.length;this.#oe=true;this.run(u)}#ce(){if(this.#ie>0&&!M(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(t){while(this.#oe){if(this.#a===C.INFO){if(this.#o<2){return t()}const A=this.consume(2);const u=(A[0]&128)!==0;const d=A[0]&15;const E=(A[1]&128)===128;const B=!u&&d!==Q.CONTINUATION;const I=A[1]&127;const m=A[0]&64;const y=A[0]&32;const b=A[0]&16;if(!v(d)){x(this.ws,"Invalid opcode received");return t()}if(E){x(this.ws,"Frame cannot be masked");return t()}if(m!==0&&!this.#d.has("permessage-deflate")){x(this.ws,"Expected RSV1 to be clear.");return}if(y!==0||b!==0){x(this.ws,"RSV1, RSV2, RSV3 must be clear");return}if(B&&!T(d)){x(this.ws,"Invalid frame type was fragmented.");return}if(T(d)&&this.#l.length>0){x(this.ws,"Expected continuation frame");return}if(this.#c.fragmented&&B){x(this.ws,"Fragmented frame exceeded 125 bytes.");return}if((I>125||B)&&M(d)){x(this.ws,"Control frame either too large or fragmented");return}if(H(d)&&this.#l.length===0&&!this.#c.compressed){x(this.ws,"Unexpected continuation frame");return}if(I<=125){this.#c.payloadLength=I;this.#a=C.READ_DATA;if(!this.#ce()){return}}else if(I===126){this.#a=C.PAYLOADLENGTH_16}else if(I===127){this.#a=C.PAYLOADLENGTH_64}if(T(d)){this.#c.binaryType=d;this.#c.compressed=m!==0}this.#c.opcode=d;this.#c.masked=E;this.#c.fin=u;this.#c.fragmented=B}else if(this.#a===C.PAYLOADLENGTH_16){if(this.#o<2){return t()}const A=this.consume(2);this.#c.payloadLength=A.readUInt16BE(0);this.#a=C.READ_DATA;if(!this.#ce()){return}}else if(this.#a===C.PAYLOADLENGTH_64){if(this.#o<8){return t()}const A=this.consume(8);const u=A.readUInt32BE(0);const d=A.readUInt32BE(4);if(u!==0||d>2**31-1){x(this.ws,"Received payload length > 2^31 bytes.");return}this.#c.payloadLength=d;this.#a=C.READ_DATA;if(!this.#ce()){return}}else if(this.#a===C.READ_DATA){if(this.#o<this.#c.payloadLength){return t()}const A=this.consume(this.#c.payloadLength);if(M(this.#c.opcode)){this.#oe=this.parseControlFrame(A);this.#a=C.INFO}else{if(!this.#c.compressed){if(!this.writeFragments(A)){return}if(this.#ie>0&&this.#ne>this.#ie){failWebsocketConnectionWithCode(this.ws,1009,(new _).message);return}if(!this.#c.fragmented&&this.#c.fin){N(this.ws,this.#c.binaryType,this.consumeFragments())}this.#a=C.INFO}else{this.#d.get("permessage-deflate").decompress(A,this.#c.fin,(A,u)=>{if(A){const t=A instanceof _?1009:1007;failWebsocketConnectionWithCLine truncated
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
const { ScreepsAPI } = require("screeps-api");
|
import { ScreepsAPI } from "screeps-api";
|
||||||
const core = require("@actions/core");
|
import * as core from "@actions/core";
|
||||||
const fs = require("fs");
|
import fs from "fs";
|
||||||
const { glob } = require("glob");
|
import { glob } from "glob";
|
||||||
const path = require("path");
|
import path from "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.
|
||||||
@@ -17,7 +19,7 @@ const path = require("path");
|
|||||||
* @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.
|
||||||
*/
|
*/
|
||||||
function replacePlaceholders(content, hostname) {
|
export 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)
|
||||||
@@ -37,8 +39,9 @@ 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.
|
||||||
*/
|
*/
|
||||||
async function readReplaceAndWriteFiles(pattern, prefix, hostname) {
|
export async function readReplaceAndWriteFiles(pattern, prefix, hostname) {
|
||||||
const globPattern = prefix ? path.join(prefix, pattern) : pattern;
|
let 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) => {
|
||||||
@@ -58,9 +61,10 @@ 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.
|
||||||
*/
|
*/
|
||||||
async function readFilesIntoDict(pattern, prefix) {
|
export async function readFilesIntoDict(pattern, prefix) {
|
||||||
// Prepend the prefix to the glob pattern
|
// Prepend the prefix to the glob pattern
|
||||||
const globPattern = prefix ? path.join(prefix, pattern) : pattern;
|
let 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 = {};
|
||||||
@@ -88,7 +92,7 @@ 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.
|
||||||
*/
|
*/
|
||||||
function validateAuthentication(token, username, password) {
|
export 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.";
|
||||||
@@ -107,10 +111,32 @@ 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.
|
||||||
*/
|
*/
|
||||||
async function postCode() {
|
export 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";
|
||||||
@@ -155,34 +181,116 @@ 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));
|
||||||
console.log(`Code set successfully to ${branch}`);
|
core.info(`Code set successfully to ${branch}`);
|
||||||
} else {
|
} catch (err) {
|
||||||
core.info(`Logging in as user ${username}`);
|
core.error(`Upload error: ${err}`);
|
||||||
await Promise.resolve()
|
throw err;
|
||||||
.then(() => api.auth(username, password, login_arguments))
|
}
|
||||||
.then(() => {
|
|
||||||
api.code.set(branch, files_to_push);
|
// Console monitoring (optional)
|
||||||
})
|
const monitorTicks = parseInt(core.getInput("monitor") || "0", 10);
|
||||||
.then(() => {
|
if (monitorTicks > 0) {
|
||||||
console.log(`Code set successfully to ${branch}`);
|
const onTraceback = core.getInput("on_traceback") || "fail";
|
||||||
})
|
const onErrorLog = core.getInput("on_error_log") || "warn";
|
||||||
.catch((err) => {
|
const onWarningLog = core.getInput("on_warning_log") || "ignore";
|
||||||
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
if (process.argv[1] === __filename) {
|
||||||
postCode();
|
postCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAuthentication,
|
|
||||||
replacePlaceholders,
|
|
||||||
postCode,
|
|
||||||
readReplaceAndWriteFiles,
|
|
||||||
readFilesIntoDict,
|
|
||||||
};
|
|
||||||
+420
@@ -0,0 +1,420 @@
|
|||||||
|
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(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/&/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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Generated
-2373
File diff suppressed because it is too large.
Load diff
+10
-7
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "screeps-deploy-action",
|
"name": "screeps-deploy-action",
|
||||||
"version": "0.1.1",
|
"version": "0.2.1",
|
||||||
"description": "Deploys screeps code to the official game or an pirvate server.",
|
"description": "Deploys screeps code to the official game or a private server.",
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@11.25.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
@@ -9,13 +11,14 @@
|
|||||||
"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/core": "^2.0.0",
|
"@actions/artifact": "^1.1.2",
|
||||||
"glob": "^13.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
|
"glob": "^14.0.0",
|
||||||
"screeps-api": "^1.7.2"
|
"screeps-api": "^1.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.38.4",
|
"@vercel/ncc": "^0.44.1",
|
||||||
"@vitest/coverage-v8": "^4.0.16",
|
"@vitest/coverage-v8": "^4.1.11",
|
||||||
"vitest": "^4.0.16"
|
"vitest": "^4.1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+1414
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
|||||||
|
allowBuilds:
|
||||||
|
bufferutil: true
|
||||||
|
utf-8-validate: true
|
||||||
+6
-15
@@ -5,20 +5,11 @@
|
|||||||
"config:recommended",
|
"config:recommended",
|
||||||
":automergeLinters"
|
":automergeLinters"
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"rebaseWhen": "behind-base-branch",
|
||||||
{
|
"branchConcurrentLimit": 2,
|
||||||
"matchManagers": [
|
"prConcurrentLimit": 2,
|
||||||
"npm"
|
"gitIgnoredAuthors": [
|
||||||
],
|
"github-actions[bot]@users.noreply.github.com",
|
||||||
"postUpgradeTasks": {
|
"bot@horstenkamp.eu"
|
||||||
"commands": [
|
|
||||||
"npm ci",
|
|
||||||
"npm run build"
|
|
||||||
],
|
|
||||||
"fileFilters": [
|
|
||||||
"dist/index.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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();
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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));
|
||||||
Reference in new issue
Block a user