@deployanyway/excuse-js 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-08
4
+
5
+ - Useful structured API/CLI additions described in README.
6
+ - TypeScript declarations, CommonJS entry, coverage gates and installed archive checks.
7
+ - Linux Node 22/24 plus Windows/macOS Node 24 CI.
8
+
3
9
  ## 0.2.0 — 2026-10-08
4
10
 
5
11
  - Meeting preparation mode: Generate 1–20 excuses with `excuseBatch(category, { count, seed })`. A batch cycles through the category without repeats until its pool is exhausted. Seeds keep the starting point repeatable. CLI `--json` returns a string or array.
package/MIGRATION.md ADDED
@@ -0,0 +1,5 @@
1
+ # 0.3.0 migration
2
+
3
+ Node 22.13+ is required. Existing core APIs remain available; TypeScript declarations and CommonJS exports are new. The CLI now lives in src/cli.js behind the same executable path. Input/stdout behavior for new options is documented in README.
4
+
5
+ Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. Do not treat jokes or heuristic scores as production evidence.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # excuse-js
2
2
 
3
+ > **Version 0.3.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0.
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/%40deployanyway%2Fexcuse-js)](https://www.npmjs.com/package/@deployanyway/excuse-js)
4
6
  [![CI](https://github.com/DeployAnyway/excuse-js/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DeployAnyway/excuse-js/actions/workflows/ci.yml)
5
7
 
@@ -126,3 +128,41 @@ API (import the named functions from this package):
126
128
  ```js
127
129
  excuseBatch("deployment", { count: 3, seed: "demo" });
128
130
  ```
131
+
132
+ ## Jokes with a useful exit strategy
133
+
134
+ ```js
135
+ import { excuseReport } from "@deployanyway/excuse-js";
136
+ const report = excuseReport("testing", { seed: 42 });
137
+ console.log(report.excuse);
138
+ console.log(report.nextStep);
139
+ ```
140
+
141
+ The structured report pairs the original comic excuse with a category-specific, honest next step. It does not invent evidence or recommend lying. Seeds are stateless and version-specific; numeric 42 differs from the CLI's string seed "42".
142
+
143
+ ```sh
144
+ node bin/cli.js testing --report --seed demo
145
+ node bin/cli.js deployment --report --json
146
+ ```
147
+
148
+ --report cannot be combined with --count or --list. Existing string and batch JSON output stays available. No stdin is needed: this generator has no external input payload. Do not use jokes as incident reports.
149
+
150
+ ## Run from source
151
+
152
+ ```sh
153
+ git clone --branch main https://github.com/DeployAnyway/excuse-js.git
154
+ cd excuse-js
155
+ npm ci
156
+ npm run build
157
+ node bin/cli.js --help
158
+ ```
159
+
160
+ ## Candidate quality standard
161
+
162
+ Version 0.3 provides useful declaration types, ESM/CommonJS exports, installed-archive checks, and coverage gates (90% statements/lines/functions, 85% branches). CI covers Linux Node 22/24 and Windows/macOS Node 24. Node 22.13+ is required. No runtime dependencies, telemetry or network requests.
163
+
164
+ From a source checkout: npm ci, npm run build, npm run coverage, npm run test:types, npm run verify:package. Pack verification installs a temporary local archive and checks module entries, types, executable and offline npm exec.
165
+
166
+ [Contribution guide](CONTRIBUTING.md) · [Conduct](CODE_OF_CONDUCT.md) · [Security](SECURITY.md) · [Roadmap](ROADMAP.md) · [Migration](MIGRATION.md).
167
+
168
+ **Tools for developers who probably know better.** Software nobody requested, built with questionable priorities, and shipped with absolute confidence!
package/bin/cli.js CHANGED
@@ -1,61 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs } from "node:util";
3
- import { readFileSync } from "node:fs";
4
- import { URL } from "node:url";
5
- import { excuse, categories, excuseBatch } from "../src/index.js";
6
-
7
- try {
8
- const { values, positionals } = parseArgs({
9
- allowPositionals: true,
10
- options: {
11
- help: { type: "boolean", short: "h" },
12
- version: { type: "boolean", short: "v" },
13
- seed: { type: "string" },
14
- list: { type: "boolean" },
15
- count: { type: "string" },
16
- json: { type: "boolean" },
17
- },
18
- });
19
- if (values.help) {
20
- console.log(
21
- "Usage: excuse-js [category] [--seed text] [--count 3] [--json]\n\nDefault category: deployment\n\nOptions:\n --seed text Repeatable selection (always a string seed)\n --count 1..20 Prepare a batch for the meeting\n --json Structured string or batch\n --list List categories\n -h, --help Show help\n -v, --version Show version\n\nExample: excuse-js deployment --seed demo\nExit codes: 0 success; 2 invalid arguments.",
22
- );
23
- } else if (values.version) {
24
- console.log(
25
- JSON.parse(
26
- readFileSync(new URL("../package.json", import.meta.url), "utf8"),
27
- ).version,
28
- );
29
- } else if (values.list) {
30
- if (
31
- positionals.length ||
32
- values.seed !== undefined ||
33
- values.count !== undefined ||
34
- values.json
35
- )
36
- throw new TypeError("--list does not accept a category or seed.");
37
- console.log(categories().join("\n"));
38
- } else {
39
- if (positionals.length > 1)
40
- throw new TypeError("Provide only one category.");
41
- if (values.count !== undefined && !/^[0-9]+$/.test(values.count))
42
- throw new TypeError("count must be an integer.");
43
- const result =
44
- values.count === undefined
45
- ? excuse(positionals[0], { seed: values.seed })
46
- : excuseBatch(positionals[0], {
47
- seed: values.seed,
48
- count: Number(values.count),
49
- });
50
- console.log(
51
- values.json
52
- ? JSON.stringify(result)
53
- : Array.isArray(result)
54
- ? result.map((line, i) => `${i + 1}. ${line}`).join("\n")
55
- : result,
56
- );
57
- }
58
- } catch (error) {
59
- console.error(`excuse-js: ${error.message}\nRun with --help for usage.`);
60
- process.exitCode = 2;
61
- }
2
+ import "../src/cli.js";
package/dist/index.cjs ADDED
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ categories: () => categories,
24
+ excuse: () => excuse,
25
+ excuseBatch: () => excuseBatch,
26
+ excuseReport: () => excuseReport
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/excuses.js
31
+ var phrases = {
32
+ deployment: [
33
+ "The deployment passed rehearsal and requested a stunt double for production.",
34
+ "The release arrived on time. Its dependencies are still finding parking.",
35
+ "The deployment is waiting for the server to finish its dramatic entrance."
36
+ ],
37
+ build: [
38
+ "The compiler found a typo and promoted it to a company-wide announcement.",
39
+ "The build is assembling a committee to discuss the missing semicolon.",
40
+ "The artifacts are ready; the build is negotiating custody."
41
+ ],
42
+ production: [
43
+ "Production interpreted the happy path as an optional scenic route.",
44
+ "The server is performing an unscheduled demonstration of our recovery plan.",
45
+ "Production has requested that our assumptions submit identification."
46
+ ],
47
+ bug: [
48
+ "The edge case moved to the center and brought furniture.",
49
+ "That variable has been exploring career opportunities outside its scope.",
50
+ "The bug declined to reproduce until the screen-sharing session began."
51
+ ],
52
+ api: [
53
+ "The API sent a response, but it used the envelope for the payload.",
54
+ "The endpoint is observing a moment of silence for our timeout setting.",
55
+ "The contract was clear. The response preferred interpretive dance."
56
+ ],
57
+ database: [
58
+ "The database is taking a full-table stroll to consider our question.",
59
+ "The query asked for one row and accidentally invited the whole neighborhood.",
60
+ "The migration packed the columns but forgot the forwarding address."
61
+ ],
62
+ deadline: [
63
+ "The estimate included coding time, but the calendar added plot twists.",
64
+ "The deadline arrived before the requirements finished introducing themselves.",
65
+ "The last ten percent has applied for its own project manager."
66
+ ],
67
+ testing: [
68
+ "The test fixture has developed stronger opinions than the specification.",
69
+ "The flaky test only accepts results delivered under a full cache.",
70
+ "The assertion expected certainty; the asynchronous task offered suspense."
71
+ ],
72
+ network: [
73
+ "The packets took a detour to admire the routing table.",
74
+ "DNS is still deciding which address feels most like home.",
75
+ "The connection is buffering its explanation for the delay."
76
+ ],
77
+ "merge-conflict": [
78
+ "Both branches brought a solution and neither brought a seating chart.",
79
+ "Git discovered that two correct answers wanted the same line.",
80
+ "The branches agree on the goal and disagree on every comma."
81
+ ],
82
+ demo: [
83
+ "The demo saw an audience and switched from software to performance art.",
84
+ "The prototype is saving its best behavior for the recording we forgot to start.",
85
+ "The live demo has added a surprise question to the presentation."
86
+ ]
87
+ };
88
+
89
+ // src/index.js
90
+ var nextSteps = {
91
+ deployment: "Check the release diff and rollback plan before delivering this line.",
92
+ testing: "Reproduce the failure and add a regression test. The punchline is optional.",
93
+ build: "Read the first build error, verify dependencies, then rebuild. The compiler does not accept applause.",
94
+ production: "Check health metrics and the latest release diff; confirm rollback before improving the punchline.",
95
+ bug: "Save a minimal reproduction and add a regression test. The bug would prefer neither.",
96
+ api: "Inspect the status, payload and timeout contract. The endpoint is not communicating through vibes.",
97
+ database: "Check connectivity and migration state before retrying. The data deserves a backup, not a speech.",
98
+ deadline: "Name the smallest deliverable and communicate the revised estimate. Calendars cannot compile optimism.",
99
+ network: "Check DNS, connectivity and timeout settings. Blame needs a packet capture.",
100
+ demo: "Rehearse the happy path and prepare an offline fallback. The projector can smell confidence."
101
+ };
102
+ function excuseReport(category = "deployment", options = {}) {
103
+ const text = excuse(category, options);
104
+ const key = category.trim().toLowerCase();
105
+ return {
106
+ category: key,
107
+ excuse: text,
108
+ nextStep: nextSteps[key] ?? "Read the evidence, name an owner, and document the next small fix."
109
+ };
110
+ }
111
+ function categories() {
112
+ return Object.keys(phrases);
113
+ }
114
+ function excuse(category = "deployment", options = {}) {
115
+ if (typeof category !== "string" || !category.trim())
116
+ throw new TypeError("Category must be a nonempty string.");
117
+ if (!options || typeof options !== "object" || Array.isArray(options))
118
+ throw new TypeError("Options must be an object.");
119
+ const key = category.trim().toLowerCase();
120
+ if (!Object.hasOwn(phrases, key))
121
+ throw new RangeError(
122
+ `Unknown category: ${category}. Choose: ${categories().join(", ")}.`
123
+ );
124
+ const { seed } = options;
125
+ let index;
126
+ if (seed === void 0) {
127
+ index = Math.floor(Math.random() * phrases[key].length);
128
+ } else {
129
+ if (typeof seed !== "string" && !(typeof seed === "number" && Number.isFinite(seed)))
130
+ throw new TypeError("Seed must be a string or finite number.");
131
+ const input = `${key}:${typeof seed}:${seed}`;
132
+ let hash = 2166136261;
133
+ for (let i = 0; i < input.length; i++) {
134
+ hash ^= input.charCodeAt(i);
135
+ hash = Math.imul(hash, 16777619) >>> 0;
136
+ }
137
+ index = hash % phrases[key].length;
138
+ }
139
+ return phrases[key][index];
140
+ }
141
+ function excuseBatch(category = "deployment", options = {}) {
142
+ if (!options || typeof options !== "object" || Array.isArray(options))
143
+ throw new TypeError("Options must be an object.");
144
+ const count = options.count ?? 3;
145
+ if (!Number.isSafeInteger(count) || count < 1 || count > 20)
146
+ throw new RangeError("count must be an integer between 1 and 20.");
147
+ const first = excuse(category, { seed: options.seed });
148
+ const pool = phrases[category.trim().toLowerCase()];
149
+ const start = pool.indexOf(first);
150
+ return Array.from(
151
+ { length: count },
152
+ (_, i) => pool[(start + i) % pool.length]
153
+ );
154
+ }
155
+ // Annotate the CommonJS export names for ESM import in node:
156
+ 0 && (module.exports = {
157
+ categories,
158
+ excuse,
159
+ excuseBatch,
160
+ excuseReport
161
+ });
package/index.d.cts ADDED
@@ -0,0 +1,30 @@
1
+ export type Category =
2
+ | "deployment"
3
+ | "build"
4
+ | "production"
5
+ | "bug"
6
+ | "api"
7
+ | "database"
8
+ | "deadline"
9
+ | "testing"
10
+ | "network"
11
+ | "merge-conflict"
12
+ | "demo";
13
+ export interface ExcuseOptions {
14
+ seed?: string | number;
15
+ }
16
+ export interface ExcuseReport {
17
+ category: Category;
18
+ excuse: string;
19
+ nextStep: string;
20
+ }
21
+ export function categories(): Category[];
22
+ export function excuse(category?: Category, options?: ExcuseOptions): string;
23
+ export function excuseBatch(
24
+ category?: Category,
25
+ options?: ExcuseOptions & { count?: number },
26
+ ): string[];
27
+ export function excuseReport(
28
+ category?: Category,
29
+ options?: ExcuseOptions,
30
+ ): ExcuseReport;
package/index.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ export type Category =
2
+ | "deployment"
3
+ | "build"
4
+ | "production"
5
+ | "bug"
6
+ | "api"
7
+ | "database"
8
+ | "deadline"
9
+ | "testing"
10
+ | "network"
11
+ | "merge-conflict"
12
+ | "demo";
13
+ export interface ExcuseOptions {
14
+ seed?: string | number;
15
+ }
16
+ export interface ExcuseReport {
17
+ category: Category;
18
+ excuse: string;
19
+ nextStep: string;
20
+ }
21
+ export function categories(): Category[];
22
+ export function excuse(category?: Category, options?: ExcuseOptions): string;
23
+ export function excuseBatch(
24
+ category?: Category,
25
+ options?: ExcuseOptions & { count?: number },
26
+ ): string[];
27
+ export function excuseReport(
28
+ category?: Category,
29
+ options?: ExcuseOptions,
30
+ ): ExcuseReport;
package/package.json CHANGED
@@ -1,9 +1,20 @@
1
1
  {
2
2
  "name": "@deployanyway/excuse-js",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A seeded developer excuse generator for bugs, builds, and deployments. Finally, a dependency that takes the blame.",
5
5
  "type": "module",
6
- "exports": "./src/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": {
9
+ "types": "./index.d.ts",
10
+ "default": "./src/index.js"
11
+ },
12
+ "require": {
13
+ "types": "./index.d.cts",
14
+ "default": "./dist/index.cjs"
15
+ }
16
+ }
17
+ },
7
18
  "bin": {
8
19
  "excuse-js": "./bin/cli.js"
9
20
  },
@@ -12,16 +23,26 @@
12
23
  "bin",
13
24
  "README.md",
14
25
  "LICENSE",
15
- "CHANGELOG.md"
26
+ "CHANGELOG.md",
27
+ "dist",
28
+ "index.d.ts",
29
+ "index.d.cts",
30
+ "MIGRATION.md"
16
31
  ],
17
32
  "engines": {
18
- "node": ">=22"
33
+ "node": ">=22.13"
19
34
  },
20
35
  "scripts": {
21
- "test": "node --test",
36
+ "test": "node --test test/*.test.js",
22
37
  "lint": "eslint .",
23
38
  "format": "prettier --write .",
24
- "format:check": "prettier --check ."
39
+ "format:check": "prettier --check .",
40
+ "build": "node scripts/build.mjs",
41
+ "pretest": "npm run build",
42
+ "prepack": "npm run build",
43
+ "coverage": "c8 --all --include=src/**/*.js --reporter=text --reporter=json-summary --reporter=lcov --check-coverage --lines=90 --statements=90 --functions=90 --branches=85 node --test test/*.test.js",
44
+ "test:types": "tsc -p tsconfig.json",
45
+ "verify:package": "node scripts/verify-package.mjs"
25
46
  },
26
47
  "repository": {
27
48
  "type": "git",
@@ -35,8 +56,12 @@
35
56
  "access": "public"
36
57
  },
37
58
  "devDependencies": {
59
+ "@types/node": "^26.6.4",
60
+ "c8": "^12.0.0",
61
+ "esbuild": "^0.28.2",
38
62
  "eslint": "^10.12.0",
39
- "prettier": "^3.6.2"
63
+ "prettier": "^3.6.2",
64
+ "typescript": "^7.0.2"
40
65
  },
41
66
  "keywords": [
42
67
  "nodejs",
@@ -46,6 +71,12 @@
46
71
  "excuse-generator",
47
72
  "seeded",
48
73
  "developer-humor",
49
- "deployment"
50
- ]
74
+ "deployment",
75
+ "typescript",
76
+ "humor",
77
+ "deployanyway"
78
+ ],
79
+ "main": "./dist/index.cjs",
80
+ "types": "./index.d.ts",
81
+ "homepage": "https://deployanyway.github.io/"
51
82
  }
package/src/cli.js ADDED
@@ -0,0 +1,67 @@
1
+ import { parseArgs } from "node:util";
2
+ import { readFileSync } from "node:fs";
3
+ import { URL } from "node:url";
4
+ import { excuse, categories, excuseBatch, excuseReport } from "./index.js";
5
+
6
+ try {
7
+ const { values, positionals } = parseArgs({
8
+ allowPositionals: true,
9
+ options: {
10
+ help: { type: "boolean", short: "h" },
11
+ version: { type: "boolean", short: "v" },
12
+ seed: { type: "string" },
13
+ list: { type: "boolean" },
14
+ count: { type: "string" },
15
+ json: { type: "boolean" },
16
+ report: { type: "boolean" },
17
+ },
18
+ });
19
+ if (values.help) {
20
+ console.log(
21
+ "Usage: excuse-js [category] [--seed text] [--count 3] [--json]\n\nDefault category: deployment. --report pairs the joke with an honest next step.\n\nOptions:\n --seed text Repeatable selection (always a string seed)\n --count 1..20 Prepare a batch for the meeting\n --json Structured string or batch\n --list List categories\n -h, --help Show help\n -v, --version Show version\n\nExample: excuse-js deployment --seed demo\nExit codes: 0 success; 2 invalid arguments.",
22
+ );
23
+ } else if (values.version) {
24
+ console.log(
25
+ JSON.parse(
26
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
27
+ ).version,
28
+ );
29
+ } else if (values.list) {
30
+ if (
31
+ positionals.length ||
32
+ values.seed !== undefined ||
33
+ values.count !== undefined ||
34
+ values.json ||
35
+ values.report
36
+ )
37
+ throw new TypeError("--list does not accept a category or seed.");
38
+ console.log(categories().join("\n"));
39
+ } else {
40
+ if (positionals.length > 1)
41
+ throw new TypeError("Provide only one category.");
42
+ if (values.count !== undefined && !/^[0-9]+$/.test(values.count))
43
+ throw new TypeError("count must be an integer.");
44
+ if (values.report && values.count !== undefined)
45
+ throw new TypeError("--report cannot be combined with --count.");
46
+ const result = values.report
47
+ ? excuseReport(positionals[0], { seed: values.seed })
48
+ : values.count === undefined
49
+ ? excuse(positionals[0], { seed: values.seed })
50
+ : excuseBatch(positionals[0], {
51
+ seed: values.seed,
52
+ count: Number(values.count),
53
+ });
54
+ console.log(
55
+ values.json
56
+ ? JSON.stringify(result)
57
+ : Array.isArray(result)
58
+ ? result.map((line, i) => `${i + 1}. ${line}`).join("\n")
59
+ : values.report
60
+ ? `${result.excuse}\nNext step: ${result.nextStep}`
61
+ : result,
62
+ );
63
+ }
64
+ } catch (error) {
65
+ console.error(`excuse-js: ${error.message}\nRun with --help for usage.`);
66
+ process.exitCode = 2;
67
+ }
package/src/index.js CHANGED
@@ -1,4 +1,36 @@
1
1
  import { phrases } from "./excuses.js";
2
+ const nextSteps = {
3
+ deployment:
4
+ "Check the release diff and rollback plan before delivering this line.",
5
+ testing:
6
+ "Reproduce the failure and add a regression test. The punchline is optional.",
7
+ build:
8
+ "Read the first build error, verify dependencies, then rebuild. The compiler does not accept applause.",
9
+ production:
10
+ "Check health metrics and the latest release diff; confirm rollback before improving the punchline.",
11
+ bug: "Save a minimal reproduction and add a regression test. The bug would prefer neither.",
12
+ api: "Inspect the status, payload and timeout contract. The endpoint is not communicating through vibes.",
13
+ database:
14
+ "Check connectivity and migration state before retrying. The data deserves a backup, not a speech.",
15
+ deadline:
16
+ "Name the smallest deliverable and communicate the revised estimate. Calendars cannot compile optimism.",
17
+ network:
18
+ "Check DNS, connectivity and timeout settings. Blame needs a packet capture.",
19
+ demo: "Rehearse the happy path and prepare an offline fallback. The projector can smell confidence.",
20
+ };
21
+
22
+ /** A comic excuse paired with an honest, actionable follow-up. */
23
+ export function excuseReport(category = "deployment", options = {}) {
24
+ const text = excuse(category, options);
25
+ const key = category.trim().toLowerCase();
26
+ return {
27
+ category: key,
28
+ excuse: text,
29
+ nextStep:
30
+ nextSteps[key] ??
31
+ "Read the evidence, name an owner, and document the next small fix.",
32
+ };
33
+ }
2
34
 
3
35
  /** Return the available lowercase category names as a fresh array. */
4
36
  export function categories() {