@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 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 "./repo/preflight.js";
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, since a dlx self-refresh (see
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`/`stripPreflightFlags`
93
- * already use for pulling a flag out of argv wherever it sits.
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
- // Preflight runs on EVERY command of this bin, including `--help` — see
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 "./repo/preflight.js";
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;
@@ -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.8",
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",
@@ -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 {};
@@ -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
- }