@devdogsuga/devtools 0.1.8 → 0.1.10
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/cli.js +2 -4
- package/dist/launch.d.ts +0 -12
- package/dist/launch.js +3 -47
- package/dist/telemetry.js +1 -1
- package/dist/version.d.ts +7 -0
- package/dist/version.js +19 -0
- package/package.json +3 -3
- package/dist/repo/preflight.d.ts +0 -109
- package/dist/repo/preflight.js +0 -266
package/dist/cli.js
CHANGED
|
@@ -61,7 +61,7 @@ import { runGithubSettings } from "./gh/settings/commands.js";
|
|
|
61
61
|
import { runWorkflows } from "./workflows/commands.js";
|
|
62
62
|
import { runCf } from "./cf/commands.js";
|
|
63
63
|
import { captureDevtoolsError, initDevtoolsTelemetry } from "./telemetry.js";
|
|
64
|
-
import { ownVersion } from "./
|
|
64
|
+
import { ownVersion } from "./version.js";
|
|
65
65
|
const MODERATION_COMMANDS = ["moderation", "grant-root"];
|
|
66
66
|
function isModerationCommand(value) {
|
|
67
67
|
return MODERATION_COMMANDS.includes(value);
|
|
@@ -1014,9 +1014,7 @@ export async function main(argv) {
|
|
|
1014
1014
|
return;
|
|
1015
1015
|
}
|
|
1016
1016
|
// Clean stdout, same reasoning as `completions` above: mostly useful for
|
|
1017
|
-
// confirming which build actually ran
|
|
1018
|
-
// `repo/preflight.ts`) is otherwise invisible short of reading stderr's
|
|
1019
|
-
// one-line nudge/refresh notice.
|
|
1017
|
+
// confirming which build actually ran.
|
|
1020
1018
|
if (argv[0] === "version") {
|
|
1021
1019
|
process.stdout.write(`${ownVersion()}\n`);
|
|
1022
1020
|
return;
|
package/dist/launch.d.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulls the two preflight-only boolean flags out of `argv`, wherever they
|
|
3
|
-
* sit, the same way `stripTierFlag` does for `--tier` below. Neither takes
|
|
4
|
-
* a value. `DEVTOOLS_SKIP_PREFLIGHT=1` is the env-var spelling of
|
|
5
|
-
* `--skip-preflight`, for shells/CI wrappers that would rather set an env
|
|
6
|
-
* var once than remember a flag on every invocation.
|
|
7
|
-
*/
|
|
8
|
-
export declare function stripPreflightFlags(argv: readonly string[]): {
|
|
9
|
-
skipPreflight: boolean;
|
|
10
|
-
refresh: boolean;
|
|
11
|
-
rest: string[];
|
|
12
|
-
};
|
|
13
1
|
/**
|
|
14
2
|
* Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
|
|
15
3
|
* other argument untouched and in its original order. Exported for its own
|
package/dist/launch.js
CHANGED
|
@@ -34,32 +34,7 @@ import { confirm, select } from "@clack/prompts";
|
|
|
34
34
|
import { findCommand } from "./commands.js";
|
|
35
35
|
import { discoverRepoRoot, findRepoRoot, RepoNotFoundError } from "./repo/root.js";
|
|
36
36
|
import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
|
|
37
|
-
import { isDevMode, ownVersion, runPreflight } from "./repo/preflight.js";
|
|
38
37
|
import { errorMessage, unwrap } from "./ui.js";
|
|
39
|
-
/**
|
|
40
|
-
* Pulls the two preflight-only boolean flags out of `argv`, wherever they
|
|
41
|
-
* sit, the same way `stripTierFlag` does for `--tier` below. Neither takes
|
|
42
|
-
* a value. `DEVTOOLS_SKIP_PREFLIGHT=1` is the env-var spelling of
|
|
43
|
-
* `--skip-preflight`, for shells/CI wrappers that would rather set an env
|
|
44
|
-
* var once than remember a flag on every invocation.
|
|
45
|
-
*/
|
|
46
|
-
export function stripPreflightFlags(argv) {
|
|
47
|
-
const rest = [];
|
|
48
|
-
let skipPreflight = process.env.DEVTOOLS_SKIP_PREFLIGHT === "1";
|
|
49
|
-
let refresh = false;
|
|
50
|
-
for (const arg of argv) {
|
|
51
|
-
if (arg === "--skip-preflight") {
|
|
52
|
-
skipPreflight = true;
|
|
53
|
-
}
|
|
54
|
-
else if (arg === "--refresh") {
|
|
55
|
-
refresh = true;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
rest.push(arg);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return { skipPreflight, refresh, rest };
|
|
62
|
-
}
|
|
63
38
|
/**
|
|
64
39
|
* Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
|
|
65
40
|
* other argument untouched and in its original order. Exported for its own
|
|
@@ -89,8 +64,8 @@ export function stripTierFlag(argv) {
|
|
|
89
64
|
* Whether the command `rest` dispatches to is declared `envFree` in
|
|
90
65
|
* `commands.ts`'s catalog — the leading run of non-flag tokens is the
|
|
91
66
|
* command path (`["github", "rulesets"]` out of `["github", "rulesets",
|
|
92
|
-
* "--apply"]`), the same convention `stripTierFlag
|
|
93
|
-
*
|
|
67
|
+
* "--apply"]`), the same convention `stripTierFlag` already uses for
|
|
68
|
+
* pulling a flag out of argv wherever it sits.
|
|
94
69
|
*
|
|
95
70
|
* Catalog-driven rather than a second hardcoded name list: `setup` and
|
|
96
71
|
* `completions` below stay their own explicit branch (each skips tier
|
|
@@ -140,26 +115,7 @@ async function dispatch(argv) {
|
|
|
140
115
|
* nothing this function could return that would mean anything.
|
|
141
116
|
*/
|
|
142
117
|
export async function launch(argv) {
|
|
143
|
-
|
|
144
|
-
// `repo/preflight.ts`'s header. It never requires a repo (the manifest
|
|
145
|
-
// fetch/cache/self-refresh logic is entirely repo-independent), so it
|
|
146
|
-
// runs before `findRepoRoot()` is anywhere near the picture, unlike tier
|
|
147
|
-
// resolution below. `devtools-ci` (`launch-ci.ts`) deliberately does not
|
|
148
|
-
// call this at all — CI pins an exact version on purpose.
|
|
149
|
-
const { skipPreflight, refresh, rest: afterPreflight } = stripPreflightFlags(argv);
|
|
150
|
-
await runPreflight({
|
|
151
|
-
currentVersion: ownVersion(),
|
|
152
|
-
argv: afterPreflight,
|
|
153
|
-
skipPreflight,
|
|
154
|
-
refresh,
|
|
155
|
-
devMode: isDevMode(),
|
|
156
|
-
// Set only for testing against a local Verdaccio (see the end-to-end
|
|
157
|
-
// self-refresh validation in CUTOVER.md); unset in production, where a
|
|
158
|
-
// bare `pnpm dlx @devdogsuga/devtools@<version>` resolves against the
|
|
159
|
-
// default npm registry with no flag needed at all.
|
|
160
|
-
registry: process.env.DEVTOOLS_REGISTRY,
|
|
161
|
-
});
|
|
162
|
-
const { explicit, rest } = stripTierFlag(afterPreflight);
|
|
118
|
+
const { explicit, rest } = stripTierFlag(argv);
|
|
163
119
|
// `--help`/`-h` bypasses tier resolution entirely, BEFORE it can refuse.
|
|
164
120
|
// `cli.ts`'s own `main()` already answers these with no env in play (see
|
|
165
121
|
// its header comment); resolving a tier first anyway meant `pnpm devtools
|
package/dist/telemetry.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { readFileSync } from "node:fs";
|
|
30
30
|
import * as Sentry from "@sentry/node";
|
|
31
31
|
import { buildSentryOptions } from "@devdogsuga/telemetry";
|
|
32
|
-
import { ownVersion } from "./
|
|
32
|
+
import { ownVersion } from "./version.js";
|
|
33
33
|
/** The DSN baked into this build, or "" if there is none (a source run under
|
|
34
34
|
* tsx, or a build made without `DEVTOOLS_SENTRY_DSN`). */
|
|
35
35
|
function bakedSentryDsn() {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The directory this module's compiled file lives in one level under —
|
|
2
|
+
* i.e. the devtools package's own root (`dist/version.js` → up one).
|
|
3
|
+
* Exported for tests; memoized since it never changes within a process. */
|
|
4
|
+
export declare function ownPackageDir(): string;
|
|
5
|
+
/** Reads this build's own `package.json` version — used by `telemetry.ts`
|
|
6
|
+
* (release tagging) and `cli.ts`'s `version` command. */
|
|
7
|
+
export declare function ownVersion(): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
let cachedOwnDir;
|
|
5
|
+
/** The directory this module's compiled file lives in one level under —
|
|
6
|
+
* i.e. the devtools package's own root (`dist/version.js` → up one).
|
|
7
|
+
* Exported for tests; memoized since it never changes within a process. */
|
|
8
|
+
export function ownPackageDir() {
|
|
9
|
+
if (cachedOwnDir === undefined) {
|
|
10
|
+
cachedOwnDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
}
|
|
12
|
+
return cachedOwnDir;
|
|
13
|
+
}
|
|
14
|
+
/** Reads this build's own `package.json` version — used by `telemetry.ts`
|
|
15
|
+
* (release tagging) and `cli.ts`'s `version` command. */
|
|
16
|
+
export function ownVersion() {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(ownPackageDir(), "package.json"), "utf8"));
|
|
18
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devdogsuga/devtools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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.",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"react": "^19.2.8",
|
|
64
64
|
"vitest": "^4.1.11",
|
|
65
65
|
"@devdogsuga/brand": "0.1.1",
|
|
66
|
+
"@devdogsuga/config": "0.1.1",
|
|
66
67
|
"@devdogsuga/db": "0.1.1",
|
|
67
|
-
"@devdogsuga/env": "0.1.1"
|
|
68
|
-
"@devdogsuga/config": "0.1.1"
|
|
68
|
+
"@devdogsuga/env": "0.1.1"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc && node scripts/write-build-info.mjs",
|
package/dist/repo/preflight.d.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
declare const ManifestSchema: z.ZodObject<{
|
|
3
|
-
devtools: z.ZodObject<{
|
|
4
|
-
latest: z.ZodString;
|
|
5
|
-
minimum: z.ZodString;
|
|
6
|
-
}, z.core.$strip>;
|
|
7
|
-
}, z.core.$strip>;
|
|
8
|
-
export type DevtoolsMinimumsManifest = z.infer<typeof ManifestSchema>;
|
|
9
|
-
/** Exported so tests can point elsewhere without touching the real `~/.cache`. */
|
|
10
|
-
export declare function cacheFilePath(): string;
|
|
11
|
-
export interface ManifestResult {
|
|
12
|
-
manifest: DevtoolsMinimumsManifest | null;
|
|
13
|
-
source: "cache" | "network" | "fail-open";
|
|
14
|
-
error?: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Fetches (or reads the disk cache for) the minimums manifest. Never
|
|
18
|
-
* throws: any failure — timeout, network error, non-200, invalid JSON,
|
|
19
|
-
* schema mismatch — comes back as `{ manifest: null, source: "fail-open" }`.
|
|
20
|
-
*/
|
|
21
|
-
export declare function fetchManifest(options?: {
|
|
22
|
-
url?: string;
|
|
23
|
-
skipCache?: boolean;
|
|
24
|
-
timeoutMs?: number;
|
|
25
|
-
}): Promise<ManifestResult>;
|
|
26
|
-
export type PreflightAction = {
|
|
27
|
-
action: "skip-preflight";
|
|
28
|
-
} | {
|
|
29
|
-
action: "dev-mode";
|
|
30
|
-
} | {
|
|
31
|
-
action: "fail-open";
|
|
32
|
-
reason: string;
|
|
33
|
-
} | {
|
|
34
|
-
action: "self-refresh";
|
|
35
|
-
from: string;
|
|
36
|
-
to: string;
|
|
37
|
-
source: ManifestResult["source"];
|
|
38
|
-
} | {
|
|
39
|
-
action: "loop-guard-tripped";
|
|
40
|
-
detail: string;
|
|
41
|
-
} | {
|
|
42
|
-
action: "nudge";
|
|
43
|
-
from: string;
|
|
44
|
-
to: string;
|
|
45
|
-
source: ManifestResult["source"];
|
|
46
|
-
} | {
|
|
47
|
-
action: "up-to-date";
|
|
48
|
-
source: ManifestResult["source"];
|
|
49
|
-
};
|
|
50
|
-
export interface PreflightOptions {
|
|
51
|
-
currentVersion: string;
|
|
52
|
-
argv: readonly string[];
|
|
53
|
-
skipPreflight: boolean;
|
|
54
|
-
refresh: boolean;
|
|
55
|
-
registry?: string;
|
|
56
|
-
manifestUrl?: string;
|
|
57
|
-
devMode?: boolean;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Runs the decision half of preflight. Does not itself re-exec or print —
|
|
61
|
-
* `runPreflight` below does both, so the pure decision stays independently
|
|
62
|
-
* testable (loop guard, fail-open, version comparisons) without spawning a
|
|
63
|
-
* real child process in every test.
|
|
64
|
-
*/
|
|
65
|
-
export declare function decidePreflight(options: PreflightOptions): Promise<PreflightAction>;
|
|
66
|
-
/**
|
|
67
|
-
* Re-execs as the exact pinned `version`, inheriting stdio and propagating
|
|
68
|
-
* the child's exit code. Never returns — the caller relies on this to end
|
|
69
|
-
* the process, matching `spawnSync`'s synchronous nature (there is nothing
|
|
70
|
-
* meaningful left to do in this process either way).
|
|
71
|
-
*
|
|
72
|
-
* Exported separately from `runPreflight` so tests can stub it instead of
|
|
73
|
-
* spawning a real `pnpm dlx`.
|
|
74
|
-
*/
|
|
75
|
-
export declare function selfRefresh(options: {
|
|
76
|
-
version: string;
|
|
77
|
-
argv: readonly string[];
|
|
78
|
-
registry?: string;
|
|
79
|
-
}): never;
|
|
80
|
-
/**
|
|
81
|
-
* The full preflight: decides, prints the single line each outcome calls
|
|
82
|
-
* for (or nothing, for the common `up-to-date`/`skip-preflight`/`dev-mode`
|
|
83
|
-
* cases — see each branch), and re-execs on `self-refresh`. Returns once
|
|
84
|
-
* for every other outcome so `launch()` can continue dispatching the
|
|
85
|
-
* original command.
|
|
86
|
-
*/
|
|
87
|
-
export declare function runPreflight(options: PreflightOptions & {
|
|
88
|
-
reexec?: typeof selfRefresh;
|
|
89
|
-
}): Promise<PreflightAction>;
|
|
90
|
-
/** The directory this module's compiled file lives in one level under —
|
|
91
|
-
* i.e. the devtools package's own root (`dist/repo/preflight.js` → up two).
|
|
92
|
-
* Exported for tests; memoized since it never changes within a process. */
|
|
93
|
-
export declare function ownPackageDir(): string;
|
|
94
|
-
/** Reads this build's own `package.json` — the version preflight compares
|
|
95
|
-
* against the manifest, and the value dev-mode detection inspects. */
|
|
96
|
-
export declare function ownVersion(): string;
|
|
97
|
-
/**
|
|
98
|
-
* True when this devtools is running from a workspace/dev checkout rather
|
|
99
|
-
* than a published install — a `pnpm build && node ./bin/devtools.mjs` run
|
|
100
|
-
* inside `Backstage/packages/devtools` itself, or any other case where the
|
|
101
|
-
* package's own directory is not sitting under a `node_modules` tree (a
|
|
102
|
-
* real install — via dlx's isolated cache or an ordinary `node_modules` —
|
|
103
|
-
* always puts it under one). `version: "0.0.0-dev"` is the explicit,
|
|
104
|
-
* unambiguous override for anything that wants to force this (a fixture,
|
|
105
|
-
* an integration test); the path heuristic is the sensible default so
|
|
106
|
-
* nobody has to remember to set it.
|
|
107
|
-
*/
|
|
108
|
-
export declare function isDevMode(version?: string, dir?: string): boolean;
|
|
109
|
-
export {};
|
package/dist/repo/preflight.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The dlx-distribution preflight: fetches a tiny minimums manifest from
|
|
3
|
-
* DevDogsUGA's `main` branch, compares it against this build's own version,
|
|
4
|
-
* and decides whether to proceed, nudge, or self-refresh by re-exec'ing
|
|
5
|
-
* `pnpm dlx @devdogsuga/devtools@<latest>`.
|
|
6
|
-
*
|
|
7
|
-
* Ported from the `devtools-dlx` prototype
|
|
8
|
-
* (`/home/sloan/scratchpad/devdogs/prototypes/devtools-dlx/FINDINGS.md`,
|
|
9
|
-
* experiment 7 and its "Recommended contract" section) — read that file for
|
|
10
|
-
* why each design choice below is the one that survived testing rather than
|
|
11
|
-
* the first thing that seemed reasonable:
|
|
12
|
-
*
|
|
13
|
-
* - The re-exec spec MUST be an exact version (`@devdogsuga/devtools@0.4.2`),
|
|
14
|
-
* never a bare spec. Experiment 6 found TWO independent staleness
|
|
15
|
-
* mechanisms a bare spec (even with `dlx-cache-max-age=0`) does not
|
|
16
|
-
* reliably bypass: pnpm's dlx cache TTL, and pnpm 11's
|
|
17
|
-
* `minimumReleaseAge` cooldown on a just-published version. Only an
|
|
18
|
-
* exact pinned spec bypasses both.
|
|
19
|
-
* - `--config.registry=<url>`, not `--registry` after `dlx` — pnpm 11
|
|
20
|
-
* rejects the latter positioned there.
|
|
21
|
-
* - The manifest fetch fails open (log + proceed on the current version)
|
|
22
|
-
* on ANY error — dead host, timeout, malformed JSON, schema mismatch —
|
|
23
|
-
* so an offline contributor is never blocked by this check.
|
|
24
|
-
*
|
|
25
|
-
* `devtools-ci` (`launch-ci.ts`) never calls this module — CI pins an exact
|
|
26
|
-
* version on purpose (see the carve-out plan's §9 Wave 2 decisions), and a
|
|
27
|
-
* self-refreshing CI job would defeat that pin.
|
|
28
|
-
*/
|
|
29
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
30
|
-
import { spawnSync } from "node:child_process";
|
|
31
|
-
import { homedir } from "node:os";
|
|
32
|
-
import { dirname, join, sep } from "node:path";
|
|
33
|
-
import { fileURLToPath } from "node:url";
|
|
34
|
-
import { z } from "zod";
|
|
35
|
-
/** Env var set on the re-exec'd child so a still-below-minimum child refuses
|
|
36
|
-
* to loop a second time instead of re-exec'ing forever. */
|
|
37
|
-
const LOOP_GUARD_ENV = "DEVTOOLS_REEXEC_GUARD";
|
|
38
|
-
/** Default location of the manifest — a repo-root file in DevDogsUGA's own
|
|
39
|
-
* `main` branch, not buried inside a package, so it survives reorganization
|
|
40
|
-
* and is a trivial hotfix-PR edit. Overridable for tests and for use before
|
|
41
|
-
* the file exists on the real remote. */
|
|
42
|
-
const DEFAULT_MINIMUMS_URL = "https://raw.githubusercontent.com/DevDogsUGA/DevDogsUGA/main/devtools-minimums.json";
|
|
43
|
-
const FETCH_TIMEOUT_MS = 1500;
|
|
44
|
-
/**
|
|
45
|
-
* Deliberately short — 10 minutes, not the plan's original "a few hours" —
|
|
46
|
-
* so a mid-meeting hotfix (someone drops the `minimum` to force a
|
|
47
|
-
* self-refresh) propagates to every contributor's next command within one
|
|
48
|
-
* coffee break, not one workday. This is the one place stage A2 knowingly
|
|
49
|
-
* overrides §5's original wording; see the carve-out plan's §9 note.
|
|
50
|
-
*/
|
|
51
|
-
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
52
|
-
const ManifestSchema = z.object({
|
|
53
|
-
devtools: z.object({
|
|
54
|
-
latest: z.string().min(1),
|
|
55
|
-
minimum: z.string().min(1),
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
function xdgCacheHome() {
|
|
59
|
-
const fromEnv = process.env.XDG_CACHE_HOME;
|
|
60
|
-
return fromEnv !== undefined && fromEnv !== "" ? fromEnv : join(homedir(), ".cache");
|
|
61
|
-
}
|
|
62
|
-
/** Exported so tests can point elsewhere without touching the real `~/.cache`. */
|
|
63
|
-
export function cacheFilePath() {
|
|
64
|
-
return join(xdgCacheHome(), "devdogsuga-devtools", "minimums.json");
|
|
65
|
-
}
|
|
66
|
-
function readCache(url) {
|
|
67
|
-
const file = cacheFilePath();
|
|
68
|
-
if (!existsSync(file))
|
|
69
|
-
return null;
|
|
70
|
-
try {
|
|
71
|
-
const entry = JSON.parse(readFileSync(file, "utf8"));
|
|
72
|
-
if (entry.url !== url)
|
|
73
|
-
return null;
|
|
74
|
-
if (Date.now() - entry.fetchedAt > CACHE_TTL_MS)
|
|
75
|
-
return null;
|
|
76
|
-
const parsed = ManifestSchema.safeParse(entry.manifest);
|
|
77
|
-
return parsed.success ? parsed.data : null;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function writeCache(url, manifest) {
|
|
84
|
-
try {
|
|
85
|
-
const file = cacheFilePath();
|
|
86
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
87
|
-
const entry = { fetchedAt: Date.now(), url, manifest };
|
|
88
|
-
writeFileSync(file, JSON.stringify(entry));
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// Best-effort. A cache write failure (read-only home dir, full disk,
|
|
92
|
-
// whatever) must never fail the command it is only trying to speed up
|
|
93
|
-
// next time.
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Fetches (or reads the disk cache for) the minimums manifest. Never
|
|
98
|
-
* throws: any failure — timeout, network error, non-200, invalid JSON,
|
|
99
|
-
* schema mismatch — comes back as `{ manifest: null, source: "fail-open" }`.
|
|
100
|
-
*/
|
|
101
|
-
export async function fetchManifest(options) {
|
|
102
|
-
const url = options?.url ?? process.env.DEVTOOLS_MINIMUMS_URL ?? DEFAULT_MINIMUMS_URL;
|
|
103
|
-
const timeoutMs = options?.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
104
|
-
if (options?.skipCache !== true) {
|
|
105
|
-
const cached = readCache(url);
|
|
106
|
-
if (cached)
|
|
107
|
-
return { manifest: cached, source: "cache" };
|
|
108
|
-
}
|
|
109
|
-
const controller = new AbortController();
|
|
110
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
111
|
-
try {
|
|
112
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
113
|
-
if (!res.ok)
|
|
114
|
-
throw new Error(`manifest fetch: HTTP ${res.status}`);
|
|
115
|
-
const json = await res.json();
|
|
116
|
-
const parsed = ManifestSchema.safeParse(json);
|
|
117
|
-
if (!parsed.success) {
|
|
118
|
-
throw new Error(`manifest fetch: invalid shape (${parsed.error.message})`);
|
|
119
|
-
}
|
|
120
|
-
writeCache(url, parsed.data);
|
|
121
|
-
return { manifest: parsed.data, source: "network" };
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
return { manifest: null, source: "fail-open", error: errorText(err) };
|
|
125
|
-
}
|
|
126
|
-
finally {
|
|
127
|
-
clearTimeout(timer);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function errorText(err) {
|
|
131
|
-
return err instanceof Error ? err.message : String(err);
|
|
132
|
-
}
|
|
133
|
-
/** Three-part semver comparison; `<0` means `a` is older than `b`. Anything
|
|
134
|
-
* non-numeric in a segment sorts as `0`, which is permissive on purpose —
|
|
135
|
-
* this only ever compares two well-formed manifest/package versions. */
|
|
136
|
-
function compareVersions(a, b) {
|
|
137
|
-
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
138
|
-
const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
139
|
-
for (let i = 0; i < 3; i++) {
|
|
140
|
-
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
141
|
-
if (diff !== 0)
|
|
142
|
-
return diff;
|
|
143
|
-
}
|
|
144
|
-
return 0;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Runs the decision half of preflight. Does not itself re-exec or print —
|
|
148
|
-
* `runPreflight` below does both, so the pure decision stays independently
|
|
149
|
-
* testable (loop guard, fail-open, version comparisons) without spawning a
|
|
150
|
-
* real child process in every test.
|
|
151
|
-
*/
|
|
152
|
-
export async function decidePreflight(options) {
|
|
153
|
-
if (options.skipPreflight)
|
|
154
|
-
return { action: "skip-preflight" };
|
|
155
|
-
if (options.devMode === true)
|
|
156
|
-
return { action: "dev-mode" };
|
|
157
|
-
const { manifest, source, error } = await fetchManifest({
|
|
158
|
-
url: options.manifestUrl,
|
|
159
|
-
skipCache: options.refresh,
|
|
160
|
-
});
|
|
161
|
-
if (!manifest) {
|
|
162
|
-
return { action: "fail-open", reason: error ?? "unknown error" };
|
|
163
|
-
}
|
|
164
|
-
const { latest, minimum } = manifest.devtools;
|
|
165
|
-
if (compareVersions(options.currentVersion, minimum) < 0) {
|
|
166
|
-
if (process.env[LOOP_GUARD_ENV]) {
|
|
167
|
-
return {
|
|
168
|
-
action: "loop-guard-tripped",
|
|
169
|
-
detail: `already re-exec'd once (${LOOP_GUARD_ENV}=${process.env[LOOP_GUARD_ENV]}); refusing to loop again`,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return { action: "self-refresh", from: options.currentVersion, to: latest, source };
|
|
173
|
-
}
|
|
174
|
-
if (compareVersions(options.currentVersion, latest) < 0) {
|
|
175
|
-
return { action: "nudge", from: options.currentVersion, to: latest, source };
|
|
176
|
-
}
|
|
177
|
-
return { action: "up-to-date", source };
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Re-execs as the exact pinned `version`, inheriting stdio and propagating
|
|
181
|
-
* the child's exit code. Never returns — the caller relies on this to end
|
|
182
|
-
* the process, matching `spawnSync`'s synchronous nature (there is nothing
|
|
183
|
-
* meaningful left to do in this process either way).
|
|
184
|
-
*
|
|
185
|
-
* Exported separately from `runPreflight` so tests can stub it instead of
|
|
186
|
-
* spawning a real `pnpm dlx`.
|
|
187
|
-
*/
|
|
188
|
-
export function selfRefresh(options) {
|
|
189
|
-
const spec = `@devdogsuga/devtools@${options.version}`;
|
|
190
|
-
const registryArgs = options.registry !== undefined ? [`--config.registry=${options.registry}`] : [];
|
|
191
|
-
const result = spawnSync("pnpm", ["dlx", ...registryArgs, spec, ...options.argv], {
|
|
192
|
-
stdio: "inherit",
|
|
193
|
-
env: { ...process.env, [LOOP_GUARD_ENV]: "1" },
|
|
194
|
-
});
|
|
195
|
-
process.exit(result.status ?? 1);
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* The full preflight: decides, prints the single line each outcome calls
|
|
199
|
-
* for (or nothing, for the common `up-to-date`/`skip-preflight`/`dev-mode`
|
|
200
|
-
* cases — see each branch), and re-execs on `self-refresh`. Returns once
|
|
201
|
-
* for every other outcome so `launch()` can continue dispatching the
|
|
202
|
-
* original command.
|
|
203
|
-
*/
|
|
204
|
-
export async function runPreflight(options) {
|
|
205
|
-
const decision = await decidePreflight(options);
|
|
206
|
-
const reexec = options.reexec ?? selfRefresh;
|
|
207
|
-
switch (decision.action) {
|
|
208
|
-
case "skip-preflight":
|
|
209
|
-
case "dev-mode":
|
|
210
|
-
case "up-to-date":
|
|
211
|
-
// Silent — see the module header: this runs on every command, and a
|
|
212
|
-
// line of noise on the overwhelmingly common "nothing to do" path
|
|
213
|
-
// would defeat the point of the check running unconditionally.
|
|
214
|
-
break;
|
|
215
|
-
case "fail-open":
|
|
216
|
-
process.stderr.write(`devtools: preflight check skipped (${decision.reason})\n`);
|
|
217
|
-
break;
|
|
218
|
-
case "loop-guard-tripped":
|
|
219
|
-
process.stderr.write(`devtools: still below the minimum version after one self-refresh attempt; ` +
|
|
220
|
-
`continuing on the current version instead of looping (${decision.detail})\n`);
|
|
221
|
-
break;
|
|
222
|
-
case "nudge":
|
|
223
|
-
process.stderr.write(`devtools: a newer version (${decision.to}) is available — you're on ${decision.from}. ` +
|
|
224
|
-
`Run with --refresh, or it'll catch up on its own shortly.\n`);
|
|
225
|
-
break;
|
|
226
|
-
case "self-refresh":
|
|
227
|
-
process.stderr.write(`devtools: ${decision.from} is below the minimum supported version — ` +
|
|
228
|
-
`relaunching as ${decision.to}...\n`);
|
|
229
|
-
reexec({ version: decision.to, argv: options.argv, registry: options.registry });
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
return decision;
|
|
233
|
-
}
|
|
234
|
-
let cachedOwnDir;
|
|
235
|
-
/** The directory this module's compiled file lives in one level under —
|
|
236
|
-
* i.e. the devtools package's own root (`dist/repo/preflight.js` → up two).
|
|
237
|
-
* Exported for tests; memoized since it never changes within a process. */
|
|
238
|
-
export function ownPackageDir() {
|
|
239
|
-
if (cachedOwnDir === undefined) {
|
|
240
|
-
cachedOwnDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
241
|
-
}
|
|
242
|
-
return cachedOwnDir;
|
|
243
|
-
}
|
|
244
|
-
/** Reads this build's own `package.json` — the version preflight compares
|
|
245
|
-
* against the manifest, and the value dev-mode detection inspects. */
|
|
246
|
-
export function ownVersion() {
|
|
247
|
-
const pkg = JSON.parse(readFileSync(join(ownPackageDir(), "package.json"), "utf8"));
|
|
248
|
-
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* True when this devtools is running from a workspace/dev checkout rather
|
|
252
|
-
* than a published install — a `pnpm build && node ./bin/devtools.mjs` run
|
|
253
|
-
* inside `Backstage/packages/devtools` itself, or any other case where the
|
|
254
|
-
* package's own directory is not sitting under a `node_modules` tree (a
|
|
255
|
-
* real install — via dlx's isolated cache or an ordinary `node_modules` —
|
|
256
|
-
* always puts it under one). `version: "0.0.0-dev"` is the explicit,
|
|
257
|
-
* unambiguous override for anything that wants to force this (a fixture,
|
|
258
|
-
* an integration test); the path heuristic is the sensible default so
|
|
259
|
-
* nobody has to remember to set it.
|
|
260
|
-
*/
|
|
261
|
-
export function isDevMode(version = ownVersion(), dir = ownPackageDir()) {
|
|
262
|
-
if (version === "0.0.0-dev")
|
|
263
|
-
return true;
|
|
264
|
-
const marker = `${sep}node_modules${sep}`;
|
|
265
|
-
return !dir.includes(marker);
|
|
266
|
-
}
|