@devdogsuga/devtools 0.1.10 → 0.1.12
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/dist/env/discovery.js +19 -6
- package/dist/gh/rulesets/api.js +3 -1
- package/dist/gh/rulesets/diff.js +39 -5
- package/dist/repo/peer-redirect-hooks.d.ts +16 -0
- package/dist/repo/peer-redirect-hooks.js +10 -0
- package/dist/repo/peer-redirect.d.ts +4 -0
- package/dist/repo/peer-redirect.js +26 -0
- package/dist/repo/peers.d.ts +13 -0
- package/dist/repo/peers.js +23 -1
- package/package.json +3 -3
package/dist/env/discovery.js
CHANGED
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
* its `declare()`/`define()` calls landing in the SAME `@devdogsuga/env`
|
|
38
38
|
* registry instance the target repo's manifests populate (module identity —
|
|
39
39
|
* see `repo/peers.ts`'s header and the devtools-dlx prototype's
|
|
40
|
-
* FINDINGS.md experiment 3)
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* FINDINGS.md experiment 3). Its bare `@devdogsuga/env` import is
|
|
41
|
+
* redirected to the copy `loadEnv()` resolved through the repo, because
|
|
42
|
+
* from a `pnpm dlx` copy the specifier does not resolve at all: devtools
|
|
43
|
+
* ships no copy of its own (`@devdogsuga/env` is only ever an optional
|
|
44
|
+
* peer), and nothing installs one next to it. See `repo/peer-redirect.ts`.
|
|
44
45
|
*
|
|
45
46
|
* Most workspace packages declare nothing, so "no env.ts found" means
|
|
46
47
|
* not-a-manifest rather than an error.
|
|
@@ -58,8 +59,9 @@
|
|
|
58
59
|
*/
|
|
59
60
|
import { existsSync, readdirSync } from "node:fs";
|
|
60
61
|
import { dirname, join } from "node:path";
|
|
61
|
-
import { fileURLToPath } from "node:url";
|
|
62
|
-
import {
|
|
62
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
63
|
+
import { redirectPeer } from "../repo/peer-redirect.js";
|
|
64
|
+
import { getEnvSync, loadEnv, repoPeerUrl } from "../repo/peers.js";
|
|
63
65
|
import { findRepoRoot } from "../repo/root.js";
|
|
64
66
|
import { importRepoTs } from "../repo/tsx-loader.js";
|
|
65
67
|
/**
|
|
@@ -105,6 +107,17 @@ async function importManifests() {
|
|
|
105
107
|
// see `repo/peers.ts`'s header), and `assertRegistryLoaded()` /
|
|
106
108
|
// `gh/environments.ts`'s getters read it back synchronously afterward.
|
|
107
109
|
await loadEnv();
|
|
110
|
+
// devtools' own manifest imports `@devdogsuga/env` by bare specifier, which
|
|
111
|
+
// does not resolve at all from a `pnpm dlx` copy. Point it at the copy
|
|
112
|
+
// `loadEnv()` just resolved through the repo. See `repo/peer-redirect.ts`.
|
|
113
|
+
const envUrl = repoPeerUrl("@devdogsuga/env");
|
|
114
|
+
if (envUrl) {
|
|
115
|
+
redirectPeer({
|
|
116
|
+
parentURL: pathToFileURL(ownManifestPath()).href,
|
|
117
|
+
specifier: "@devdogsuga/env",
|
|
118
|
+
url: envUrl,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
108
121
|
// The Next apps' manifests run `createEnv` at import time. Without this flag
|
|
109
122
|
// they would validate the AMBIENT environment, a devtools process rather than
|
|
110
123
|
// an app build, and throw on whatever is missing. The manifests already
|
package/dist/gh/rulesets/api.js
CHANGED
|
@@ -92,7 +92,9 @@ export async function createRuleset(r, desired) {
|
|
|
92
92
|
return writeJson("POST", path(r), desired);
|
|
93
93
|
}
|
|
94
94
|
export async function updateRuleset(r, id, desired) {
|
|
95
|
-
|
|
95
|
+
// PUT, not PATCH: GitHub's rulesets API has no PATCH route and answers one
|
|
96
|
+
// with a 404, which reads like a missing ruleset rather than a wrong verb.
|
|
97
|
+
return writeJson("PUT", path(r, `/${id}`), desired);
|
|
96
98
|
}
|
|
97
99
|
export async function deleteRuleset(r, id) {
|
|
98
100
|
await writeJson("DELETE", path(r, `/${id}`), {});
|
package/dist/gh/rulesets/diff.js
CHANGED
|
@@ -24,9 +24,22 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { DELETE_RULESET_NAMES, LEGACY_NAME_ALIASES, isPerTeamRulesetName, } from "./desired.js";
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Reduces a rule to the fields this reconciler manages, in a fixed key order,
|
|
28
|
+
* so a live rule and its desired twin compare equal whenever they protect
|
|
29
|
+
* the same way.
|
|
30
|
+
*
|
|
31
|
+
* Two ways GitHub's copy differs from what was sent, both seen live on
|
|
32
|
+
* 2026-09-27 right after creating `production`:
|
|
33
|
+
*
|
|
34
|
+
* - it omits parameters that hold their default, so `update` comes back with
|
|
35
|
+
* no `update_allows_fetch_and_merge` (see `UpdateRule.parameters`' doc in
|
|
36
|
+
* `types.ts`);
|
|
37
|
+
* - it adds `pull_request` parameters `desired.ts` never sets
|
|
38
|
+
* (`dismissal_restriction`, `require_extra_approval_for_unattributed_changes`,
|
|
39
|
+
* `required_reviewers`), in its own key order.
|
|
40
|
+
*
|
|
41
|
+
* Without this, every ruleset with a `pull_request` rule planned an update on
|
|
42
|
+
* every run, so the reconciler never settled.
|
|
30
43
|
*/
|
|
31
44
|
function canonicalizeRule(rule) {
|
|
32
45
|
if (rule.type === "update") {
|
|
@@ -37,15 +50,36 @@ function canonicalizeRule(rule) {
|
|
|
37
50
|
},
|
|
38
51
|
};
|
|
39
52
|
}
|
|
40
|
-
|
|
53
|
+
if (rule.type === "pull_request") {
|
|
54
|
+
const p = rule.parameters;
|
|
55
|
+
return {
|
|
56
|
+
type: "pull_request",
|
|
57
|
+
parameters: {
|
|
58
|
+
allowed_merge_methods: [...p.allowed_merge_methods].sort(),
|
|
59
|
+
dismiss_stale_reviews_on_push: p.dismiss_stale_reviews_on_push,
|
|
60
|
+
require_code_owner_review: p.require_code_owner_review,
|
|
61
|
+
require_last_push_approval: p.require_last_push_approval,
|
|
62
|
+
required_approving_review_count: p.required_approving_review_count,
|
|
63
|
+
required_review_thread_resolution: p.required_review_thread_resolution,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { type: rule.type };
|
|
41
68
|
}
|
|
42
69
|
function sortRules(rules) {
|
|
43
70
|
return [...rules]
|
|
44
71
|
.map(canonicalizeRule)
|
|
45
72
|
.sort((a, b) => a.type.localeCompare(b.type));
|
|
46
73
|
}
|
|
74
|
+
/** Sorted, and each actor rebuilt in a fixed key order for the same reason as `canonicalizeRule`. */
|
|
47
75
|
function sortActors(actors) {
|
|
48
|
-
return [...actors]
|
|
76
|
+
return [...actors]
|
|
77
|
+
.sort((a, b) => a.actor_type.localeCompare(b.actor_type) || a.actor_id - b.actor_id)
|
|
78
|
+
.map((a) => ({
|
|
79
|
+
actor_id: a.actor_id,
|
|
80
|
+
actor_type: a.actor_type,
|
|
81
|
+
bypass_mode: a.bypass_mode,
|
|
82
|
+
}));
|
|
49
83
|
}
|
|
50
84
|
/**
|
|
51
85
|
* A canonical, order-independent string for one ruleset's PROTECTION shape —
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-loader hooks for `repo/peer-redirect.ts`. Runs on Node's loader
|
|
3
|
+
* thread, not the main one, so nothing here can import devtools state: the
|
|
4
|
+
* redirects arrive once, as `initialize()`'s data, and are never updated.
|
|
5
|
+
*/
|
|
6
|
+
import type { InitializeHook, ResolveHook } from "node:module";
|
|
7
|
+
export interface PeerRedirect {
|
|
8
|
+
/** The exact file URL of the importing module the redirect applies to. */
|
|
9
|
+
parentURL: string;
|
|
10
|
+
/** The bare specifier it imports, e.g. `@devdogsuga/env`. */
|
|
11
|
+
specifier: string;
|
|
12
|
+
/** Where that specifier should resolve instead. */
|
|
13
|
+
url: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const initialize: InitializeHook<PeerRedirect[]>;
|
|
16
|
+
export declare const resolve: ResolveHook;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
let redirects = [];
|
|
2
|
+
export const initialize = (data) => {
|
|
3
|
+
redirects = data;
|
|
4
|
+
};
|
|
5
|
+
export const resolve = (specifier, context, nextResolve) => {
|
|
6
|
+
const redirect = redirects.find((r) => r.specifier === specifier && r.parentURL === context.parentURL);
|
|
7
|
+
if (redirect)
|
|
8
|
+
return { url: redirect.url, shortCircuit: true };
|
|
9
|
+
return nextResolve(specifier, context);
|
|
10
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PeerRedirect } from "./peer-redirect-hooks.js";
|
|
2
|
+
/** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
|
|
3
|
+
* loader for the rest of the process; call it once per redirect. */
|
|
4
|
+
export declare function redirectPeer(redirect: PeerRedirect): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Points one module's bare import of a peer at the copy the target repo
|
|
3
|
+
* resolved (`repo/peers.ts`).
|
|
4
|
+
*
|
|
5
|
+
* devtools' own `env.ts` manifest imports `@devdogsuga/env` by bare
|
|
6
|
+
* specifier, and from an installed copy that specifier resolves relative to
|
|
7
|
+
* the manifest's own location: inside the pnpm store, next to devtools,
|
|
8
|
+
* where an optional peer is never installed under `pnpm dlx`. The import
|
|
9
|
+
* failed with ERR_MODULE_NOT_FOUND, and every `env` command with it. Even
|
|
10
|
+
* where the specifier does resolve (devtools installed as a devDependency),
|
|
11
|
+
* the only copy that keeps the registry's module identity is the one the
|
|
12
|
+
* repo's manifests use, so the redirect is applied whenever that copy is
|
|
13
|
+
* known rather than only as a fallback.
|
|
14
|
+
*
|
|
15
|
+
* Node resolve hooks (`module.register`) rather than rewriting the file: the
|
|
16
|
+
* manifest also imports `zod`, which has to keep resolving from devtools'
|
|
17
|
+
* own location, and a copy written anywhere else would lose that.
|
|
18
|
+
*/
|
|
19
|
+
import { register } from "node:module";
|
|
20
|
+
/** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
|
|
21
|
+
* loader for the rest of the process; call it once per redirect. */
|
|
22
|
+
export function redirectPeer(redirect) {
|
|
23
|
+
register("./peer-redirect-hooks.js", import.meta.url, {
|
|
24
|
+
data: [redirect],
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/repo/peers.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/** Test-only: clears the memoized peer modules so a test can re-resolve with a fresh mock. */
|
|
2
2
|
export declare function resetPeerCacheForTests(): void;
|
|
3
|
+
/**
|
|
4
|
+
* The file URL `specifier` was loaded from through the target repo, or
|
|
5
|
+
* undefined if it has not been loaded yet or came from a plain bare-specifier
|
|
6
|
+
* `import()` (the test-suite and not-in-a-repo fallbacks above).
|
|
7
|
+
*
|
|
8
|
+
* For code devtools imports that itself names a peer by bare specifier --
|
|
9
|
+
* today only devtools' own `env.ts` manifest. From an installed copy that
|
|
10
|
+
* specifier cannot resolve at all (an optional peer is never installed next
|
|
11
|
+
* to devtools under `pnpm dlx`), and even where it can, the only copy that
|
|
12
|
+
* keeps module identity is this one. `repo/peer-redirect.ts` uses it to
|
|
13
|
+
* point the import here.
|
|
14
|
+
*/
|
|
15
|
+
export declare function repoPeerUrl(specifier: string): string | undefined;
|
|
3
16
|
import type * as EnvModule from "@devdogsuga/env";
|
|
4
17
|
import type * as EnvLoadModule from "@devdogsuga/env/load";
|
|
5
18
|
import type * as EnvSessionModule from "@devdogsuga/env/session";
|
package/dist/repo/peers.js
CHANGED
|
@@ -17,6 +17,10 @@ import { pathToFileURL } from "node:url";
|
|
|
17
17
|
import { findRepoRoot, RepoNotFoundError } from "./root.js";
|
|
18
18
|
import { findDependent, resolveFromRepo } from "./resolve.js";
|
|
19
19
|
const moduleCache = new Map();
|
|
20
|
+
/** The file URL each peer resolved to through the target repo, set only on
|
|
21
|
+
* that path (not on either plain bare-specifier fallback). See
|
|
22
|
+
* `repoPeerUrl()`. */
|
|
23
|
+
const resolvedUrls = new Map();
|
|
20
24
|
/**
|
|
21
25
|
* Dynamically imports `specifier` as the repo would resolve it, memoized so
|
|
22
26
|
* repeated calls in one process return the SAME module instance (required
|
|
@@ -59,7 +63,9 @@ function loadPeer(specifier) {
|
|
|
59
63
|
` or packages/* to declare it (dependencies/devDependencies/peerDependencies).`);
|
|
60
64
|
}
|
|
61
65
|
const { resolvedPath } = resolveFromRepo(repoRoot, resolutionBase, specifier);
|
|
62
|
-
|
|
66
|
+
const url = pathToFileURL(resolvedPath).href;
|
|
67
|
+
resolvedUrls.set(specifier, url);
|
|
68
|
+
return import(url);
|
|
63
69
|
})();
|
|
64
70
|
moduleCache.set(specifier, promise);
|
|
65
71
|
return promise;
|
|
@@ -67,6 +73,22 @@ function loadPeer(specifier) {
|
|
|
67
73
|
/** Test-only: clears the memoized peer modules so a test can re-resolve with a fresh mock. */
|
|
68
74
|
export function resetPeerCacheForTests() {
|
|
69
75
|
moduleCache.clear();
|
|
76
|
+
resolvedUrls.clear();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The file URL `specifier` was loaded from through the target repo, or
|
|
80
|
+
* undefined if it has not been loaded yet or came from a plain bare-specifier
|
|
81
|
+
* `import()` (the test-suite and not-in-a-repo fallbacks above).
|
|
82
|
+
*
|
|
83
|
+
* For code devtools imports that itself names a peer by bare specifier --
|
|
84
|
+
* today only devtools' own `env.ts` manifest. From an installed copy that
|
|
85
|
+
* specifier cannot resolve at all (an optional peer is never installed next
|
|
86
|
+
* to devtools under `pnpm dlx`), and even where it can, the only copy that
|
|
87
|
+
* keeps module identity is this one. `repo/peer-redirect.ts` uses it to
|
|
88
|
+
* point the import here.
|
|
89
|
+
*/
|
|
90
|
+
export function repoPeerUrl(specifier) {
|
|
91
|
+
return resolvedUrls.get(specifier);
|
|
70
92
|
}
|
|
71
93
|
let resolvedEnvModule;
|
|
72
94
|
export function loadEnv() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devdogsuga/devtools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Contributor CLI for a DevDogsUGA checkout: database, env, images, emails, cron, deploy. Ships built JS; run via `pnpm dlx @devdogsuga/devtools` (or the `devtools`/`devtools-ci` bins once installed) from inside a DevDogsUGA clone.",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"@vitest/coverage-v8": "^4.1.11",
|
|
63
63
|
"react": "^19.2.8",
|
|
64
64
|
"vitest": "^4.1.11",
|
|
65
|
-
"@devdogsuga/brand": "0.1.1",
|
|
66
65
|
"@devdogsuga/config": "0.1.1",
|
|
67
66
|
"@devdogsuga/db": "0.1.1",
|
|
68
|
-
"@devdogsuga/env": "0.1.1"
|
|
67
|
+
"@devdogsuga/env": "0.1.1",
|
|
68
|
+
"@devdogsuga/brand": "0.1.1"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc && node scripts/write-build-info.mjs",
|