@chatluna/patchbay 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChatLunaLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # patchbay
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Patches that run Koishi, Satori, the Satori adapters and a few Cordis plugins on **Cordis 4**, kept in one place and wired into yarn, pnpm or npm by one command.
4
+
5
+ Each patch targets one exact upstream version listed in [`patchbay.json`](patchbay.json). Nothing is republished: your package manager installs the upstream tarball and applies the patch from this package.
6
+
7
+ ## Use
8
+
9
+ ```sh
10
+ npm i -D @chatluna/patchbay # or yarn add -D / pnpm add -D
11
+ npx patchbay sync # writes .patchbay/ and the package-manager config
12
+ npm install # or yarn install / pnpm install
13
+ ```
14
+
15
+ Commit `.patchbay/` together with the config `sync` writes. Run `patchbay sync` again after upgrading patchbay. It replaces every pin the previous version wrote.
16
+
17
+ | Package manager | What `sync` writes | When patches apply |
18
+ | --------------- | ---------------------------------------------------------------------------------------------- | -------------------------- |
19
+ | Yarn 2+ | `resolutions` → `patch:<pkg>@npm:<version>#~/.patchbay/<pkg>@<version>.patch` | on install |
20
+ | pnpm 10+ | `pnpm-workspace.yaml`: `overrides`, `patchedDependencies`, `allowUnusedPatches` | on install |
21
+ | npm | `overrides`, direct dependencies pinned to the same spec, `postinstall: patchbay apply` | after install, in place |
22
+
23
+ `sync` reads the package manager from `packageManager`, then the lockfile, then the one running it. Pass `--pm yarn|pnpm|npm` to choose it yourself. Yarn 1 has no patch support and is rejected.
24
+
25
+ Every patched package is pinned to its patched version, along with these aliases:
26
+
27
+ - `koishi` → `@koishijs/core`
28
+ - `minato` → `@cordisjs/plugin-database`
29
+
30
+ A plugin that later installs one of them gets the patched build.
31
+
32
+ On npm, `patchbay apply` keeps a copy of the patch it applied in each package (`.patchbay.patch`). It skips packages that are already current and undoes an older patch before applying a newer one.
33
+
34
+ ### From code
35
+
36
+ ```ts
37
+ import { apply, sync } from '@chatluna/patchbay'
38
+
39
+ const pm = sync(projectDir) // 'yarn' | 'pnpm' | 'npm'
40
+ // ...run the install...
41
+ if (pm === 'npm') apply(projectDir)
42
+ ```
43
+
44
+ ## Maintain
45
+
46
+ ```sh
47
+ yarn refresh edit <name> # unpack upstream to .work/<pkg>/{a,b}, current patch applied to b
48
+ # edit .work/<pkg>/b
49
+ yarn refresh commit <name> # diff a → b into patches/<pkg>@<version>.patch
50
+ yarn verify # every patch applies to its upstream tarball
51
+ yarn test # unit tests plus real yarn, pnpm and npm installs
52
+ ```
53
+
54
+ To patch a new package or version, add its entry to `patchbay.json`, run `yarn refresh edit <name>`, make the change, then commit it.
55
+
56
+ ## License
57
+
58
+ MIT
package/lib/apply.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Patch the matching packages in `<dir>/node_modules` in place, for npm, which has no patch protocol.
3
+ * Packages already carrying the current patch are skipped. Returns the patched paths.
4
+ */
5
+ export declare function apply(dir: string): string[];
package/lib/apply.js ADDED
@@ -0,0 +1,59 @@
1
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { patchFileName } from './manifest.js';
4
+ import { applyPatchText } from './patch.js';
5
+ import { readPackageJson, readState, STATE_DIR } from './sync.js';
6
+ /** Each patched package keeps the patch it carries, so a changed patch can be undone before the new one. */
7
+ const MARKER = '.patchbay.patch';
8
+ /** Real package directories under a `node_modules`, nested ones included; symlinks are not followed. */
9
+ function* packages(modules) {
10
+ if (!existsSync(modules))
11
+ return;
12
+ for (const item of readdirSync(modules, { withFileTypes: true })) {
13
+ if (!item.isDirectory() || item.name.startsWith('.'))
14
+ continue;
15
+ const path = join(modules, item.name);
16
+ if (item.name.startsWith('@')) {
17
+ yield* packages(path);
18
+ continue;
19
+ }
20
+ if (existsSync(join(path, 'package.json')))
21
+ yield path;
22
+ yield* packages(join(path, 'node_modules'));
23
+ }
24
+ }
25
+ /**
26
+ * Patch the matching packages in `<dir>/node_modules` in place, for npm, which has no patch protocol.
27
+ * Packages already carrying the current patch are skipped. Returns the patched paths.
28
+ */
29
+ export function apply(dir) {
30
+ const manifest = readState(dir);
31
+ if (!manifest)
32
+ throw new Error(`no ${STATE_DIR}/patchbay.json in ${dir}; run patchbay sync first`);
33
+ const entries = new Map(manifest.patches.map((entry) => [`${entry.name}@${entry.version}`, entry]));
34
+ const applied = [];
35
+ for (const path of packages(join(dir, 'node_modules'))) {
36
+ const { name, version } = readPackageJson(path);
37
+ const entry = entries.get(`${name}@${version}`);
38
+ if (!entry)
39
+ continue;
40
+ const text = readFileSync(join(dir, STATE_DIR, patchFileName(entry)), 'utf8');
41
+ const marker = join(path, MARKER);
42
+ const carried = existsSync(marker) ? readFileSync(marker, 'utf8') : undefined;
43
+ if (carried === text)
44
+ continue;
45
+ try {
46
+ if (carried !== undefined) {
47
+ applyPatchText(path, carried, { reverse: true });
48
+ rmSync(marker);
49
+ }
50
+ applyPatchText(path, text);
51
+ }
52
+ catch (error) {
53
+ throw new Error(`${name}@${version} at ${relative(dir, path)}: ${String(error)}`, { cause: error });
54
+ }
55
+ writeFileSync(marker, text);
56
+ applied.push(relative(dir, path));
57
+ }
58
+ return applied;
59
+ }
package/lib/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/lib/cli.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { apply } from './apply.js';
4
+ import { loadManifest } from './manifest.js';
5
+ import { isPackageManager, sync } from './sync.js';
6
+ const usage = `usage: patchbay <command> [--dir <project>]
7
+
8
+ sync [--pm yarn|pnpm|npm] copy the patches into .patchbay/ and wire them into the package manager
9
+ apply patch node_modules in place (npm postinstall; yarn and pnpm patch on install)
10
+ list print the patched packages`;
11
+ const { positionals, values } = parseArgs({
12
+ allowPositionals: true,
13
+ options: { dir: { type: 'string', default: process.cwd() }, pm: { type: 'string' } }
14
+ });
15
+ const [command] = positionals;
16
+ if (command === 'sync') {
17
+ if (values.pm !== undefined && !isPackageManager(values.pm))
18
+ throw new Error(`--pm must be yarn, pnpm or npm`);
19
+ const pm = sync(values.dir, values.pm === undefined ? {} : { pm: values.pm });
20
+ console.log(`patchbay: wired ${loadManifest().patches.length} patches into ${pm}; run \`${pm} install\``);
21
+ }
22
+ else if (command === 'apply') {
23
+ for (const path of apply(values.dir))
24
+ console.log(`patchbay: patched ${path}`);
25
+ }
26
+ else if (command === 'list') {
27
+ for (const entry of loadManifest().patches)
28
+ console.log(`${entry.name}@${entry.version} ${entry.description}`);
29
+ }
30
+ else {
31
+ console.error(usage);
32
+ process.exitCode = 1;
33
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { apply } from './apply.ts';
2
+ export { loadManifest, patchFileName, patchPath, type Manifest, type PatchEntry } from './manifest.ts';
3
+ export { applyPatchText } from './patch.ts';
4
+ export { detect, isPackageManager, STATE_DIR, sync, type PackageManager, type SyncOptions } from './sync.ts';
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { apply } from './apply.js';
2
+ export { loadManifest, patchFileName, patchPath } from './manifest.js';
3
+ export { applyPatchText } from './patch.js';
4
+ export { detect, isPackageManager, STATE_DIR, sync } from './sync.js';
@@ -0,0 +1,24 @@
1
+ export interface PatchEntry {
2
+ /** The package's own `package.json` name. */
3
+ name: string;
4
+ /** The exact upstream version the patch applies to. */
5
+ version: string;
6
+ description: string;
7
+ /** Issues the patch fixes, as `owner/repo#n`. */
8
+ fixes?: string[];
9
+ /**
10
+ * Dependency ranges the patch changes in `package.json`, written by `refresh commit`. npm resolves
11
+ * dependencies before `patchbay apply` runs, so sync passes these to npm as nested overrides.
12
+ */
13
+ dependencies?: Record<string, string>;
14
+ }
15
+ export interface Manifest {
16
+ patches: PatchEntry[];
17
+ /** Dependency names installed as another patched package, e.g. `koishi` → `@koishijs/core`. */
18
+ aliases: Record<string, string>;
19
+ }
20
+ export declare const ROOT: string;
21
+ export declare function loadManifest(): Manifest;
22
+ /** `@satorijs/core` + `4.6.1` → `@satorijs+core@4.6.1.patch`, the file name under `patches/`. */
23
+ export declare function patchFileName(entry: Pick<PatchEntry, 'name' | 'version'>): string;
24
+ export declare function patchPath(entry: Pick<PatchEntry, 'name' | 'version'>): string;
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ export const ROOT = fileURLToPath(new URL('..', import.meta.url));
4
+ export function loadManifest() {
5
+ return JSON.parse(readFileSync(new URL('../patchbay.json', import.meta.url), 'utf8'));
6
+ }
7
+ /** `@satorijs/core` + `4.6.1` → `@satorijs+core@4.6.1.patch`, the file name under `patches/`. */
8
+ export function patchFileName(entry) {
9
+ return `${entry.name.replace('/', '+')}@${entry.version}.patch`;
10
+ }
11
+ export function patchPath(entry) {
12
+ return fileURLToPath(new URL(`../patches/${patchFileName(entry)}`, import.meta.url));
13
+ }
package/lib/patch.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Apply a git-style unified diff to a package directory, or undo it with `reverse`. Every file is
3
+ * checked before any is written, so a patch that does not fit leaves the directory untouched.
4
+ */
5
+ export declare function applyPatchText(dir: string, text: string, { reverse }?: {
6
+ reverse?: boolean | undefined;
7
+ }): void;
package/lib/patch.js ADDED
@@ -0,0 +1,43 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { applyPatch, parsePatch, reversePatch } from 'diff';
4
+ /** Strip the `a/` / `b/` prefix git diffs put on paths; `/dev/null` stays as is. */
5
+ function stripPrefix(path) {
6
+ if (path === undefined || path === '/dev/null')
7
+ return path;
8
+ return path.replace(/^[ab]\//, '');
9
+ }
10
+ /**
11
+ * Apply a git-style unified diff to a package directory, or undo it with `reverse`. Every file is
12
+ * checked before any is written, so a patch that does not fit leaves the directory untouched.
13
+ */
14
+ export function applyPatchText(dir, text, { reverse = false } = {}) {
15
+ const parsed = parsePatch(text);
16
+ const writes = [];
17
+ for (const file of reverse ? reversePatch(parsed) : parsed) {
18
+ const from = stripPrefix(file.oldFileName);
19
+ const to = stripPrefix(file.newFileName);
20
+ const target = to === '/dev/null' ? from : to;
21
+ if (target === undefined)
22
+ continue;
23
+ const path = join(dir, target);
24
+ if (to === '/dev/null') {
25
+ writes.push({ path, content: undefined });
26
+ continue;
27
+ }
28
+ const original = from === '/dev/null' ? '' : readFileSync(join(dir, from ?? target), 'utf8');
29
+ const result = applyPatch(original, file);
30
+ if (result === false)
31
+ throw new Error(`patch does not apply to ${target}`);
32
+ writes.push({ path, content: result });
33
+ }
34
+ for (const { path, content } of writes) {
35
+ if (content === undefined) {
36
+ rmSync(path, { force: true });
37
+ continue;
38
+ }
39
+ if (!existsSync(dirname(path)))
40
+ mkdirSync(dirname(path), { recursive: true });
41
+ writeFileSync(path, content);
42
+ }
43
+ }
package/lib/sync.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type Manifest } from './manifest.ts';
2
+ export type PackageManager = 'yarn' | 'pnpm' | 'npm';
3
+ /** Project directory that holds the synced patch files and the manifest they came from. */
4
+ export declare const STATE_DIR = ".patchbay";
5
+ export interface PackageJson {
6
+ name?: string;
7
+ version?: string;
8
+ packageManager?: string;
9
+ scripts?: Record<string, string>;
10
+ dependencies?: Record<string, string>;
11
+ devDependencies?: Record<string, string>;
12
+ optionalDependencies?: Record<string, string>;
13
+ peerDependencies?: Record<string, string>;
14
+ resolutions?: Record<string, string>;
15
+ overrides?: Record<string, unknown>;
16
+ }
17
+ export interface SyncOptions {
18
+ /** Skip detection and write for this package manager. */
19
+ pm?: PackageManager;
20
+ }
21
+ export declare function isPackageManager(value: unknown): value is PackageManager;
22
+ export declare function readPackageJson(dir: string): PackageJson;
23
+ /** The project's package manager: `packageManager`, then lockfiles, then the running one, then npm. */
24
+ export declare function detect(dir: string): PackageManager;
25
+ export declare function readState(dir: string): Manifest | undefined;
26
+ /**
27
+ * Copy the patches into `<dir>/.patchbay` and point the project's package manager at them: yarn
28
+ * `resolutions`, pnpm `overrides` + `patchedDependencies`, or npm `overrides` + a `patchbay apply`
29
+ * postinstall. Entries a previous sync wrote are replaced, so upgrading patchbay drops stale pins.
30
+ */
31
+ export declare function sync(dir: string, options?: SyncOptions): PackageManager;
package/lib/sync.js ADDED
@@ -0,0 +1,144 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { Document, parseDocument } from 'yaml';
4
+ import { loadManifest, patchFileName, patchPath } from './manifest.js';
5
+ /** Project directory that holds the synced patch files and the manifest they came from. */
6
+ export const STATE_DIR = '.patchbay';
7
+ export function isPackageManager(value) {
8
+ return value === 'yarn' || value === 'pnpm' || value === 'npm';
9
+ }
10
+ export function readPackageJson(dir) {
11
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
12
+ }
13
+ function editPackageJson(dir, edit) {
14
+ const path = join(dir, 'package.json');
15
+ const text = readFileSync(path, 'utf8');
16
+ const pkg = JSON.parse(text);
17
+ edit(pkg);
18
+ writeFileSync(path, JSON.stringify(pkg, null, /^[ \t]+/m.exec(text)?.[0] ?? 2) + '\n');
19
+ }
20
+ function checkVersion(pm, major) {
21
+ if (pm === 'yarn' && major < 2)
22
+ throw new Error('Yarn 1 has no patch support; use Yarn 2+, pnpm 10+ or npm');
23
+ if (pm === 'pnpm' && major < 10)
24
+ throw new Error('pnpm reads patches from pnpm-workspace.yaml only since 10; upgrade pnpm');
25
+ if (!isPackageManager(pm))
26
+ throw new Error(`unknown package manager ${pm}`);
27
+ return pm;
28
+ }
29
+ /** The project's package manager: `packageManager`, then lockfiles, then the running one, then npm. */
30
+ export function detect(dir) {
31
+ const declared = /^(yarn|pnpm|npm)@(\d+)/.exec(readPackageJson(dir).packageManager ?? '');
32
+ if (declared)
33
+ return checkVersion(declared[1], Number(declared[2]));
34
+ if (existsSync(join(dir, 'pnpm-lock.yaml')))
35
+ return 'pnpm';
36
+ if (existsSync(join(dir, '.yarnrc.yml')))
37
+ return 'yarn';
38
+ const yarnLock = join(dir, 'yarn.lock');
39
+ if (existsSync(yarnLock))
40
+ return checkVersion('yarn', readFileSync(yarnLock, 'utf8').includes('__metadata:') ? 2 : 1);
41
+ if (existsSync(join(dir, 'package-lock.json')))
42
+ return 'npm';
43
+ const agent = /^(yarn|pnpm|npm)\/(\d+)/.exec(process.env['npm_config_user_agent'] ?? '');
44
+ if (agent)
45
+ return checkVersion(agent[1], Number(agent[2]));
46
+ return 'npm';
47
+ }
48
+ function find(manifest, name) {
49
+ const entry = manifest.patches.find((item) => item.name === name);
50
+ if (!entry)
51
+ throw new Error(`alias target ${name} is not in patchbay.json`);
52
+ return entry;
53
+ }
54
+ /** Every top-level dependency name a manifest pins: patched packages plus their aliases. */
55
+ function pinned(manifest) {
56
+ return new Map([
57
+ ...manifest.patches.map((entry) => [entry.name, entry]),
58
+ ...Object.entries(manifest.aliases).map(([alias, name]) => [alias, find(manifest, name)])
59
+ ]);
60
+ }
61
+ function npmSpec(name, entry) {
62
+ return name === entry.name ? entry.version : `npm:${entry.name}@${entry.version}`;
63
+ }
64
+ function writeYarn(dir, manifest, previous) {
65
+ editPackageJson(dir, (pkg) => {
66
+ const resolutions = { ...pkg.resolutions };
67
+ for (const name of previous ? pinned(previous).keys() : [])
68
+ delete resolutions[name];
69
+ for (const [name, entry] of pinned(manifest)) {
70
+ resolutions[name] = `patch:${entry.name}@npm%3A${entry.version}#~/${STATE_DIR}/${patchFileName(entry)}`;
71
+ }
72
+ pkg.resolutions = resolutions;
73
+ });
74
+ }
75
+ function writePnpm(dir, manifest, previous) {
76
+ const path = join(dir, 'pnpm-workspace.yaml');
77
+ const doc = existsSync(path) ? parseDocument(readFileSync(path, 'utf8')) : new Document({});
78
+ for (const [name, entry] of previous ? pinned(previous) : []) {
79
+ doc.deleteIn(['overrides', name]);
80
+ doc.deleteIn(['patchedDependencies', `${entry.name}@${entry.version}`]);
81
+ }
82
+ for (const [name, entry] of pinned(manifest))
83
+ doc.setIn(['overrides', name], npmSpec(name, entry));
84
+ for (const entry of manifest.patches) {
85
+ doc.setIn(['patchedDependencies', `${entry.name}@${entry.version}`], `${STATE_DIR}/${patchFileName(entry)}`);
86
+ }
87
+ // The manifest pins every patch it ships; a project uses only some of them.
88
+ doc.set('allowUnusedPatches', true);
89
+ writeFileSync(path, doc.toString());
90
+ }
91
+ function writeNpm(dir, manifest, previous) {
92
+ editPackageJson(dir, (pkg) => {
93
+ const overrides = { ...pkg.overrides };
94
+ for (const name of previous ? pinned(previous).keys() : [])
95
+ delete overrides[name];
96
+ const specs = pinned(manifest);
97
+ for (const [name, entry] of specs) {
98
+ // Ranges the patch changes would otherwise resolve from the unpatched package.json.
99
+ overrides[name] = entry.dependencies ? { '.': npmSpec(name, entry), ...entry.dependencies } : npmSpec(name, entry);
100
+ }
101
+ pkg.overrides = overrides;
102
+ // npm rejects an override that differs from a direct dependency's own spec (EOVERRIDE).
103
+ for (const deps of [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies]) {
104
+ for (const name of Object.keys(deps ?? {})) {
105
+ const entry = specs.get(name);
106
+ if (deps && entry)
107
+ deps[name] = npmSpec(name, entry);
108
+ }
109
+ }
110
+ const postinstall = pkg.scripts?.['postinstall'];
111
+ if (postinstall?.includes('patchbay apply'))
112
+ return;
113
+ pkg.scripts = { ...pkg.scripts, postinstall: postinstall ? `${postinstall} && patchbay apply` : 'patchbay apply' };
114
+ });
115
+ }
116
+ export function readState(dir) {
117
+ const path = join(dir, STATE_DIR, 'patchbay.json');
118
+ if (!existsSync(path))
119
+ return;
120
+ return JSON.parse(readFileSync(path, 'utf8'));
121
+ }
122
+ /**
123
+ * Copy the patches into `<dir>/.patchbay` and point the project's package manager at them: yarn
124
+ * `resolutions`, pnpm `overrides` + `patchedDependencies`, or npm `overrides` + a `patchbay apply`
125
+ * postinstall. Entries a previous sync wrote are replaced, so upgrading patchbay drops stale pins.
126
+ */
127
+ export function sync(dir, options = {}) {
128
+ const pm = options.pm ?? detect(dir);
129
+ const manifest = loadManifest();
130
+ const previous = readState(dir);
131
+ const state = join(dir, STATE_DIR);
132
+ rmSync(state, { recursive: true, force: true });
133
+ mkdirSync(state);
134
+ for (const entry of manifest.patches)
135
+ copyFileSync(patchPath(entry), join(state, patchFileName(entry)));
136
+ writeFileSync(join(state, 'patchbay.json'), JSON.stringify(manifest, null, 2) + '\n');
137
+ if (pm === 'yarn')
138
+ writeYarn(dir, manifest, previous);
139
+ else if (pm === 'pnpm')
140
+ writePnpm(dir, manifest, previous);
141
+ else
142
+ writeNpm(dir, manifest, previous);
143
+ return pm;
144
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@chatluna/patchbay",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Cordis 4 patches for Koishi, Satori, Satori adapters and Cordis plugins, wired into yarn, pnpm or npm",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ChatLunaLab/patchbay.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ChatLunaLab/patchbay/issues"
13
+ },
14
+ "keywords": [
15
+ "cordis",
16
+ "koishi",
17
+ "satori",
18
+ "patch",
19
+ "chatluna"
20
+ ],
21
+ "bin": "lib/cli.js",
22
+ "main": "lib/index.js",
23
+ "types": "lib/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./lib/index.d.ts",
27
+ "default": "./lib/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "lib",
33
+ "patches",
34
+ "patchbay.json"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
42
+ "test": "yarn build && node --test tests/*.test.ts",
43
+ "verify": "node scripts/refresh.ts verify",
44
+ "refresh": "node scripts/refresh.ts",
45
+ "prepack": "yarn build"
46
+ },
47
+ "dependencies": {
48
+ "diff": "^8.0.2",
49
+ "yaml": "^2.9.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^24.13.3",
53
+ "typescript": "7.0.2"
54
+ },
55
+ "packageManager": "yarn@4.18.0"
56
+ }
package/patchbay.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "patches": [
3
+ {
4
+ "name": "@cordisjs/plugin-cli-cordis",
5
+ "version": "1.0.5",
6
+ "description": "Stable root include id, launcher include overrides and IPC health pings"
7
+ },
8
+ {
9
+ "name": "@cordisjs/plugin-database",
10
+ "version": "4.1.1",
11
+ "description": "Prepare tables extended while the driver was still starting; also installed as minato"
12
+ },
13
+ {
14
+ "name": "@cordisjs/plugin-http",
15
+ "version": "1.5.2",
16
+ "description": "Use resolveIntercept, which Cordis 4 renamed from resolveConfig"
17
+ },
18
+ {
19
+ "name": "@cordisjs/plugin-loader",
20
+ "version": "1.0.0-rc.9",
21
+ "description": "Load prelude entries first and plugins one at a time"
22
+ },
23
+ {
24
+ "name": "@cordisjs/plugin-server",
25
+ "version": "1.7.0",
26
+ "description": "Use resolveIntercept, which Cordis 4 renamed from resolveConfig"
27
+ },
28
+ {
29
+ "name": "@cordisjs/unyaml",
30
+ "version": "2.0.3",
31
+ "description": "Register the Vite YAML plugin through rolldownOptions instead of the retired esbuildOptions"
32
+ },
33
+ {
34
+ "name": "@koishijs/core",
35
+ "version": "4.18.11",
36
+ "description": "Run Koishi on Cordis 4; also installed as koishi. Inject shapes are normalized by the @satorijs/core patch",
37
+ "fixes": [
38
+ "ChatLunaLab/boilerplate#12"
39
+ ],
40
+ "dependencies": {
41
+ "cordis": "^4.0.0-rc.8"
42
+ }
43
+ },
44
+ {
45
+ "name": "@satorijs/core",
46
+ "version": "4.6.1",
47
+ "description": "Run Satori and its adapters on Cordis 4: { required, optional } inject, ready/dispose events, Koishi 4 service lookups, a Quester-compatible http and a Koa-style server for adapter fibers",
48
+ "fixes": [
49
+ "ChatLunaLab/v2-mirror#14",
50
+ "ChatLunaLab/boilerplate#12",
51
+ "ChatLunaLab/boilerplate#13"
52
+ ],
53
+ "dependencies": {
54
+ "@cordisjs/plugin-http": "^1.5.1",
55
+ "cordis": "^4.0.0-rc.8"
56
+ }
57
+ },
58
+ {
59
+ "name": "koishi-plugin-adapter-onebot",
60
+ "version": "6.9.5",
61
+ "description": "Import Satori directly and inject http, satori and timer"
62
+ }
63
+ ],
64
+ "aliases": {
65
+ "koishi": "@koishijs/core",
66
+ "minato": "@cordisjs/plugin-database"
67
+ }
68
+ }
@@ -0,0 +1,56 @@
1
+ diff --git a/lib/worker/index.d.ts b/lib/worker/index.d.ts
2
+ index 4362cf8..ae92999 100644
3
+ --- a/lib/worker/index.d.ts
4
+ +++ b/lib/worker/index.d.ts
5
+ @@ -4,6 +4,10 @@ export interface Config {
6
+ baseUrl?: string;
7
+ execArgv?: string[];
8
+ path: string;
9
+ + /** Fixed id for the root include entry; defaults to 'chatluna' (keeps tree ids stable). */
10
+ + includeId?: string;
11
+ + /** Override/extend the root include config (e.g. inject `patches` to enable a group). */
12
+ + include?: Record<string, unknown>;
13
+ daemon: daemon.Config;
14
+ prelude?: EntryOptions[];
15
+ }
16
+ diff --git a/lib/worker/index.js b/lib/worker/index.js
17
+ index 91ff829..ec55279 100644
18
+ --- a/lib/worker/index.js
19
+ +++ b/lib/worker/index.js
20
+ @@ -31,6 +31,12 @@ function* apply(ctx, config = {}) {
21
+ process.on("SIGTERM", handleSignal);
22
+ yield () => process.off("SIGTERM", handleSignal);
23
+ process.send({ type: "start", body: config });
24
+ + // Health over IPC: the supervisor pings, the worker answers; no network address involved.
25
+ + const onMessage = /* @__PURE__ */ __name((message) => {
26
+ + if (message?.type === "ping") process.send({ type: "pong", id: message.id });
27
+ + }, "onMessage");
28
+ + process.on("message", onMessage);
29
+ + yield () => process.off("message", onMessage);
30
+ if (config.heartbeatInterval) {
31
+ const timer = setInterval(() => {
32
+ process.send({ type: "heartbeat" });
33
+ @@ -59,12 +65,22 @@ async function start(config) {
34
+ await ctx.loader.create(plugin);
35
+ }
36
+ await ctx.loader.create({
37
+ + // Fixed id so tree-side entries keep stable ids (chatluna:<id>) across restarts.
38
+ + id: config.includeId ?? "chatluna",
39
+ name: "@cordisjs/plugin-include",
40
+ config: {
41
+ + // `include` lets the launcher override the include config wholesale (e.g.
42
+ + // `chatluna dev` injects a `patches` entry to enable the dev group).
43
+ path: config.path,
44
+ - enableLogs: true
45
+ + enableLogs: true,
46
+ + ...config.include
47
+ }
48
+ });
49
+ + // Ready once every entry has finished loading (failed plugins included; they are logged).
50
+ + if (config.daemon.enabled) {
51
+ + await ctx.loader.await();
52
+ + process.send({ type: "ready" });
53
+ + }
54
+ }
55
+ __name(start, "start");
56
+ export {