@edspencer/paddock 0.82.0 → 0.82.1

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.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/cli/args.js +2 -2
  3. package/packages/server/dist/cli/busy-command.js +1 -1
  4. package/packages/server/dist/cli/busy.js +10 -12
  5. package/packages/server/dist/cli/exit-codes.d.ts +21 -45
  6. package/packages/server/dist/cli/exit-codes.js +42 -45
  7. package/packages/server/dist/cli/output.js +5 -6
  8. package/packages/server/dist/cli/paddock.js +9 -9
  9. package/packages/server/dist/cli/service/index.d.ts +2 -2
  10. package/packages/server/dist/cli/service/index.js +30 -24
  11. package/packages/server/dist/cli/service/launchd.d.ts +31 -84
  12. package/packages/server/dist/cli/service/launchd.js +104 -304
  13. package/packages/server/dist/cli/service/logs.d.ts +9 -28
  14. package/packages/server/dist/cli/service/logs.js +33 -75
  15. package/packages/server/dist/cli/service/manager.d.ts +93 -0
  16. package/packages/server/dist/cli/service/manager.js +21 -0
  17. package/packages/server/dist/cli/service/path-env.d.ts +7 -18
  18. package/packages/server/dist/cli/service/path-env.js +30 -81
  19. package/packages/server/dist/cli/service/spec.d.ts +4 -31
  20. package/packages/server/dist/cli/service/spec.js +9 -0
  21. package/packages/server/dist/cli/service/systemd.d.ts +27 -101
  22. package/packages/server/dist/cli/service/systemd.js +106 -262
  23. package/packages/server/dist/cli/update/health.d.ts +48 -0
  24. package/packages/server/dist/cli/update/health.js +64 -0
  25. package/packages/server/dist/cli/update/install-method.d.ts +29 -76
  26. package/packages/server/dist/cli/update/install-method.js +103 -178
  27. package/packages/server/dist/cli/update/lock.d.ts +7 -12
  28. package/packages/server/dist/cli/update/lock.js +28 -62
  29. package/packages/server/dist/cli/update/package-manager.d.ts +39 -74
  30. package/packages/server/dist/cli/update/package-manager.js +95 -160
  31. package/packages/server/dist/cli/update/registry.d.ts +17 -54
  32. package/packages/server/dist/cli/update/registry.js +46 -89
  33. package/packages/server/dist/cli/update/semver.d.ts +18 -41
  34. package/packages/server/dist/cli/update/semver.js +74 -124
  35. package/packages/server/dist/cli/update/update-check.d.ts +15 -26
  36. package/packages/server/dist/cli/update/update-check.js +30 -60
  37. package/packages/server/dist/cli/update/update.d.ts +57 -91
  38. package/packages/server/dist/cli/update/update.js +230 -431
  39. package/packages/server/dist/cli/update-command.js +3 -5
  40. package/packages/server/package.json +1 -1
  41. package/packages/web/dist/assets/{DeleteProjectDialog-CphdGB3R.js → DeleteProjectDialog-CgHpQctt.js} +1 -1
  42. package/packages/web/dist/assets/DiscoverPage-CG6cf-0i.js +2 -0
  43. package/packages/web/dist/assets/{DiscoverView-B_kcF4sm.js → DiscoverView-BKqI0I1E.js} +1 -1
  44. package/packages/web/dist/assets/{InstanceConfigPage-CvUK5DKW.js → InstanceConfigPage-DL3raCK1.js} +1 -1
  45. package/packages/web/dist/assets/{MarkdownRenderer-DvZ-j_Ao.js → MarkdownRenderer-DcjEuf3s.js} +1 -1
  46. package/packages/web/dist/assets/{ProjectRedirect-DayHvUH2.js → ProjectRedirect-DerkA-nE.js} +1 -1
  47. package/packages/web/dist/assets/{ProjectView-CCw83gUi.js → ProjectView-NwfC3_w7.js} +3 -3
  48. package/packages/web/dist/assets/{ProjectsGrid-B3Klg2H4.js → ProjectsGrid-BeORa0UH.js} +1 -1
  49. package/packages/web/dist/assets/{RootHome-BxcDvpgI.js → RootHome-BYJs8qJ6.js} +1 -1
  50. package/packages/web/dist/assets/{Surfaces-7UlhFBSG.js → Surfaces-w-kEaAYU.js} +1 -1
  51. package/packages/web/dist/assets/{arc-DNkh5o5Q.js → arc-Bv56hasM.js} +1 -1
  52. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BrgN3oLF.js → architectureDiagram-3BPJPVTR-BHEknx9i.js} +1 -1
  53. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-D05XOJSk.js → blockDiagram-GPEHLZMM-DtxTMrPX.js} +1 -1
  54. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CKaMBhMP.js → c4Diagram-AAUBKEIU-DRpEYOlM.js} +1 -1
  55. package/packages/web/dist/assets/channel-CdIiAqY2.js +2 -0
  56. package/packages/web/dist/assets/{chunk-2J33WTMH-8hgadCQc.js → chunk-2J33WTMH-S4s7GCg7.js} +1 -1
  57. package/packages/web/dist/assets/{chunk-4BX2VUAB-CBfEC-pu.js → chunk-4BX2VUAB-BkWVPLf1.js} +1 -1
  58. package/packages/web/dist/assets/{chunk-55IACEB6-DV1eMgHk.js → chunk-55IACEB6-4-FQp01F.js} +1 -1
  59. package/packages/web/dist/assets/{chunk-727SXJPM-Cfgl5fxY.js → chunk-727SXJPM-BeyDWdIb.js} +1 -1
  60. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BXhqD295.js → chunk-AQP2D5EJ-ZSxKy745.js} +1 -1
  61. package/packages/web/dist/assets/{chunk-FMBD7UC4-TFGOPtek.js → chunk-FMBD7UC4-Bq8uV9j-.js} +1 -1
  62. package/packages/web/dist/assets/{chunk-ND2GUHAM-CHFM665R.js → chunk-ND2GUHAM-BOFsRyHk.js} +1 -1
  63. package/packages/web/dist/assets/{chunk-QZHKN3VN-CqEf6MMT.js → chunk-QZHKN3VN-uZ6oNc4m.js} +1 -1
  64. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +2 -0
  65. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +2 -0
  66. package/packages/web/dist/assets/{core-DmEaKxBb.js → core-CYuJAM2U.js} +1 -1
  67. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BnGI6jkr.js → cose-bilkent-S5V4N54A-DIzkVSFb.js} +1 -1
  68. package/packages/web/dist/assets/{dagre-BM42HDAG-CKoPx61E.js → dagre-BM42HDAG-CHR6jJBv.js} +1 -1
  69. package/packages/web/dist/assets/{diagram-2AECGRRQ-mMmtlBl1.js → diagram-2AECGRRQ-BAFXCH8Q.js} +1 -1
  70. package/packages/web/dist/assets/{diagram-5GNKFQAL-Cdz9Mtnt.js → diagram-5GNKFQAL-BJ5t5gbz.js} +1 -1
  71. package/packages/web/dist/assets/{diagram-KO2AKTUF-DQxM3wYj.js → diagram-KO2AKTUF-D9Xt0IVg.js} +1 -1
  72. package/packages/web/dist/assets/{diagram-LMA3HP47-BWrinPsq.js → diagram-LMA3HP47-Bq38u6sy.js} +1 -1
  73. package/packages/web/dist/assets/{diagram-OG6HWLK6-U6MYUve2.js → diagram-OG6HWLK6-CMQVqkpi.js} +1 -1
  74. package/packages/web/dist/assets/{documentTitle-DGjRXuTP.js → documentTitle-M1lrfXRU.js} +1 -1
  75. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-xqMCsXxY.js → erDiagram-TEJ5UH35-C63M1g9b.js} +1 -1
  76. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-PMfy_czF.js → flowDiagram-I6XJVG4X-D3j8ACN7.js} +1 -1
  77. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-whhYYE4N.js → ganttDiagram-6RSMTGT7-B1Fd1A2Y.js} +1 -1
  78. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-C51JW4tf.js → gitGraphDiagram-PVQCEYII-CEhZhDdR.js} +1 -1
  79. package/packages/web/dist/assets/{index-CHOtvlnO.js → index-1E9yHciS.js} +2 -2
  80. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-SH8cr905.js → infoDiagram-5YYISTIA-Ctquaew6.js} +1 -1
  81. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BjqaoU3U.js → ishikawaDiagram-YF4QCWOH-DV4GvA1R.js} +1 -1
  82. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CBmdEVrd.js → journeyDiagram-JHISSGLW-C_Cy9waA.js} +1 -1
  83. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-U8njjMLZ.js → kanban-definition-UN3LZRKU-CGr9ctEf.js} +1 -1
  84. package/packages/web/dist/assets/{linear-DF66GPs1.js → linear-D3U8NOkt.js} +1 -1
  85. package/packages/web/dist/assets/{mermaid.core-CouJz7Ph.js → mermaid.core-BiArdWKV.js} +4 -4
  86. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BYIf0oFt.js → mindmap-definition-RKZ34NQL-CZIpzj8Z.js} +1 -1
  87. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Cm8rgg09.js → pieDiagram-4H26LBE5-1MaeS3rt.js} +1 -1
  88. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-C3Il5kOH.js → quadrantDiagram-W4KKPZXB-B5pgQ54e.js} +1 -1
  89. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D3PE2nTe.js → requirementDiagram-4Y6WPE33-BNnxrpGr.js} +1 -1
  90. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DdmvBM18.js → sankeyDiagram-5OEKKPKP-DsxVH-hM.js} +1 -1
  91. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-B8M_GAIq.js → sequenceDiagram-3UESZ5HK-Ke9LcFZI.js} +1 -1
  92. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DZkXnK1_.js → stateDiagram-AJRCARHV-UJzmflND.js} +1 -1
  93. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +2 -0
  94. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-IHXq7rqi.js → timeline-definition-PNZ67QCA-BRjNALVI.js} +1 -1
  95. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dga7tLtT.js → vennDiagram-CIIHVFJN-ClMAspxz.js} +1 -1
  96. package/packages/web/dist/assets/{wardley-L42UT6IY-CmcwnEJ-.js → wardley-L42UT6IY-UPv4JEmz.js} +1 -1
  97. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-zHuSLrD4.js → wardleyDiagram-YWT4CUSO-BgYJbdEx.js} +1 -1
  98. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-D93MU_ic.js → xychartDiagram-2RQKCTM6-Bfk5HjGx.js} +1 -1
  99. package/packages/web/dist/index.html +1 -1
  100. package/packages/web/dist/sw.js +1 -1
  101. package/packages/server/dist/cli/service/backend.d.ts +0 -115
  102. package/packages/server/dist/cli/service/backend.js +0 -19
  103. package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +0 -2
  104. package/packages/web/dist/assets/channel-JrEedxB8.js +0 -2
  105. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +0 -2
  106. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +0 -2
  107. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +0 -2
@@ -1,131 +1,84 @@
1
- /** The ten rows of SPEC §6's table, in its order. Matches `schemas/update-check.schema.json`. */
2
- export type InstallMethod = "docker" | "managed" | "npx" | "checkout" | "npm-global" | "pnpm-global" | "bun-global" | "volta" | "yarn-global" | "dependency";
1
+ /** SPEC §6's rows, in its order. Matches `schemas/update-check.schema.json`. */
2
+ export type InstallMethod = 'docker' | 'managed' | 'npx' | 'checkout' | 'npm-global' | 'pnpm-global' | 'bun-global' | 'volta' | 'yarn-global' | 'dependency';
3
3
  /** The package managers that can replace an install in place. */
4
- export type PackageManager = "npm" | "pnpm" | "bun" | "volta";
4
+ export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'volta';
5
5
  export interface FsLike {
6
6
  existsSync(p: string): boolean;
7
- readFileSync(p: string, encoding: "utf8"): string;
7
+ readFileSync(p: string, encoding: 'utf8'): string;
8
8
  realpathSync(p: string): string;
9
9
  }
10
10
  export interface DetectInput {
11
- /** The published package's name, e.g. `@edspencer/paddock`. */
11
+ /** The published package's name: `@edspencer/paddock`. */
12
12
  packageName: string;
13
13
  /** The environment prefix: `PADDOCK` gives `PADDOCK_IN_DOCKER` and `PADDOCK_UPDATE_HINT`. */
14
14
  envPrefix: string;
15
- /**
16
- * The script to walk up from — `process.argv[1]`, or this module's own file.
17
- *
18
- * SPEC §6 says `realpath(process.argv[1])`. Realpathing is done HERE rather
19
- * than asked of the caller, because npm installs a `bin` as a symlink and the
20
- * whole table is about where the package really sits (args.ts has the long
21
- * version of why that distinction has bitten this CLI before).
22
- */
15
+ /** The script to walk up from: `process.argv[1]`. It's realpath'd here, because npm installs a bin as a symlink. */
23
16
  entry: string;
24
17
  env: NodeJS.ProcessEnv;
25
- /**
26
- * `npm root -g`, asked of the `npm` next to `process.execPath` (SPEC §6).
27
- *
28
- * Injected because it spawns a process: a unit test of the table must not
29
- * depend on what the machine running it happens to have installed globally.
30
- * Returning undefined (no npm, or it failed) is a normal answer — the row
31
- * simply does not match.
32
- */
18
+ /** `npm root -g`, asked of the npm beside `process.execPath` (`package-manager.ts`). Undefined if npm can't say. */
33
19
  npmGlobalRoot?: () => string | undefined;
34
20
  fs?: FsLike;
35
21
  }
36
22
  export interface InstallInfo {
37
23
  method: InstallMethod;
38
24
  /**
39
- * The folder whose `package.json` names {@link DetectInput.packageName}, or —
40
- * when no ancestor does — the nearest folder with a `package.json` at all.
41
- *
42
- * The fallback is what makes a checkout detectable. Paddock's repo publishes
43
- * a SYNTHESIZED package (`scripts/make-npm-package.mjs`), so in the checkout
44
- * no manifest is called `@edspencer/paddock`: the root one is `paddock` and
45
- * the workspace one is `@paddock/server`. A strict search would find nothing
46
- * and have nothing to say; the nearest root finds `packages/server` and the
47
- * "not under node_modules" test then correctly reports a checkout.
25
+ * The folder whose `package.json` has our name or, when none does, the nearest folder with a `package.json` at all.
26
+ * The fallback is what makes a checkout of a tool whose published package is put together at build time (Paddock's)
27
+ * come out as a checkout rather than as nothing.
48
28
  */
49
29
  packageRoot: string | undefined;
50
- /** True when the name was actually found, rather than fallen back to. */
30
+ /** Whether the name was found, rather than fallen back from. */
51
31
  named: boolean;
52
- /** Can `update` install a new version itself, or must it print a command and exit 5? */
32
+ /** Whether `update` can install a new version itself, or must print a command and exit 5. */
53
33
  canUpdate: boolean;
54
34
  /** The package manager to install with, when it can. */
55
35
  manager?: PackageManager;
56
- /** `<TOOL>_UPDATE_HINT`, when the environment set one. */
36
+ /** `<TOOL>_UPDATE_HINT`, when it's set. */
57
37
  hint?: string;
58
38
  }
59
39
  /** `PADDOCK` → `PADDOCK_IN_DOCKER`. */
60
40
  export declare const inDockerVar: (prefix: string) => string;
61
41
  /** `PADDOCK` → `PADDOCK_UPDATE_HINT`. */
62
42
  export declare const updateHintVar: (prefix: string) => string;
63
- /**
64
- * Walk up from `entry` looking for the package root.
65
- *
66
- * Returns the first ancestor whose `package.json` has our name, and the nearest
67
- * ancestor with any `package.json` as a fallback — see
68
- * {@link InstallInfo.packageRoot}.
69
- */
43
+ /** Walks up from `entry` to the folder whose `package.json` has our name (see `InstallInfo.packageRoot`). */
70
44
  export declare function findPackageRoot(entry: string, packageName: string, fs: FsLike): {
71
45
  root: string | undefined;
72
46
  named: boolean;
73
47
  };
74
- /**
75
- * Is `p` inside a `node_modules` directory?
76
- *
77
- * Split on the separator rather than matched as a substring, so a project
78
- * genuinely called `my-node_modules` is not mistaken for one (and so Windows
79
- * paths work, since `path.sep` is what split them). SPEC §6's "checkout or npm
80
- * link" row is exactly the negation of this.
81
- */
48
+ /** Whether a path is inside a `node_modules` folder: by segment, so `my-node_modules` isn't one. */
82
49
  export declare function underNodeModules(p: string): boolean;
83
50
  /**
84
- * The npx / pnpm dlx / bunx / yarn dlx caches (SPEC §6).
85
- *
86
- * `_npx` and `bunx-` are matched between separators for the reason above.
87
- * `dlx` has to be looser — pnpm's is `~/.local/share/pnpm/store/.../dlx/<hash>`
88
- * and yarn's is `$TMPDIR/xfs-<hash>/dlx-<n>` — so a path SEGMENT that is `dlx`
89
- * or starts `dlx-` counts.
51
+ * The npx, pnpm dlx, bunx and yarn dlx caches (SPEC §6). `_npx` and `bunx-…` are matched as whole segments; pnpm's
52
+ * `…/dlx/<hash>` and yarn's `xfs-<hash>/dlx-<n>` need a segment that is `dlx` or starts `dlx-`.
90
53
  */
91
54
  export declare function isEphemeralCachePath(p: string): boolean;
92
55
  /**
93
- * Which of SPEC §6's rows this install is, checked in the table's own order.
94
- *
95
- * The order is the contract, and two pairs of rows genuinely overlap:
56
+ * Which of SPEC §6's rows this install is, checked in the table's order.
96
57
  *
97
- * - **Docker beats managed.** A container that an Ansible role also labelled
98
- * has both variables set, and the honest answer is "this is an image" — but
99
- * the hint is still carried on {@link InstallInfo.hint} and
100
- * {@link updateCommand} prefers it, because a hint is a sentence an operator
101
- * deliberately wrote and our guess at a `docker compose` line is not.
102
- * - **Checkout beats every global.** They cannot both be true (every global
103
- * layout is under a `node_modules`), so the order between them never decides
104
- * anything; it is kept as the table has it so the two can be read side by
105
- * side.
58
+ * - **Docker beats managed.** A container an Ansible role also labelled is an image, but `hint` still carries the
59
+ * operator's sentence, and `updateCommand` prints it rather than guessing a `docker compose` line.
60
+ * - **No package root at all** reads as a checkout: it refuses to install, and "you're running from source" is the
61
+ * least wrong thing to say.
106
62
  */
107
63
  export declare function detectInstall(input: DetectInput): InstallInfo;
108
64
  export interface CommandInput {
109
65
  packageName: string;
110
- /** The version to install, when one has been resolved. */
66
+ /** The version to install, once it's known. */
111
67
  target?: string;
112
- /** The container image to pull, for the Docker row: `ghcr.io/edspencer/paddock`. */
68
+ /** The image, for the Docker row: `ghcr.io/edspencer/paddock`. */
113
69
  image?: string;
114
- /** Prefix the npm command with `sudo` — SPEC §5 step 5's not-writable case. */
70
+ /** `sudo` in front: SPEC §5 step 5's not-writable case. */
115
71
  sudo?: boolean;
116
- /** The command that brings a checkout up to date. */
72
+ /** What brings a checkout up to date, if not `git pull && npm install && npm run build`. */
117
73
  checkoutCommand?: string;
118
74
  }
119
75
  /**
120
- * The one command to print (SPEC §3, §6): what the reader should run instead.
121
- *
122
- * Returns the command and, where it helps, the parenthetical the spec's example
123
- * carries (`(the global folder isn't writable)`). An install that CAN update
124
- * itself gets `<tool> update`, which is the line the update notice prints.
76
+ * The command SPEC §3's notice prints for this install, and the note in brackets after it. The rows that can update
77
+ * themselves get the package manager's own command here; the notice prints `<tool> update` for those instead.
125
78
  */
126
79
  export declare function updateCommand(info: InstallInfo, input: CommandInput): {
127
80
  command: string;
128
81
  note?: string;
129
82
  };
130
- /** One line a person can act on, for every row of the table. */
83
+ /** A line a person can read, for each row. */
131
84
  export declare function describeInstall(info: InstallInfo, packageName: string): string;
@@ -1,51 +1,31 @@
1
- /**
2
- * How was this copy installed? (CLI contract SPEC §6.)
3
- *
4
- * `update` cannot sensibly run `npm install -g` on a checkout, inside a
5
- * container, or out of npx's cache — so before it touches anything it works out
6
- * which of the ten layouts it is living in, and either picks the matching
7
- * package manager or prints the one command that WOULD work and exits 5.
8
- *
9
- * ## Generic on purpose
10
- *
11
- * Nothing in this file knows about Paddock. The product arrives as data — the
12
- * package name, the environment prefix, the entry script — and every call that
13
- * touches the world (spawning `npm root -g`, reading a manifest, resolving a
14
- * symlink) is injected. That is what lets the §6 table be tested against
15
- * fabricated directory layouts for all ten rows on one machine, and it is what
16
- * wave C4 lifts into `reference/install-method.ts` unchanged.
17
- *
18
- * Node built-ins only.
19
- */
20
- import fsDefault from "node:fs";
21
- import path from "node:path";
1
+ // From edspencer/cli-contract reference/install-method.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
2
+ // How was this copy installed? (SPEC §6.) `update` can't run `npm install -g` on a checkout, in a container or out of
3
+ // npx's cache, so before it touches anything it works out which of the table's ten rows it's in, and either picks the
4
+ // package manager or prints the command that would work and exits 5.
5
+ //
6
+ // Nothing here knows about a particular tool: the package name, the environment prefix and the entry script come in
7
+ // as data, and everything that touches the world (`npm root -g`, reading a manifest, resolving a symlink) is injected,
8
+ // so all ten rows can be tested against made-up layouts on one machine. Node built-ins only.
9
+ import fsDefault from 'node:fs';
10
+ import path from 'node:path';
22
11
  /** `PADDOCK` → `PADDOCK_IN_DOCKER`. */
23
12
  export const inDockerVar = (prefix) => `${prefix}_IN_DOCKER`;
24
13
  /** `PADDOCK` → `PADDOCK_UPDATE_HINT`. */
25
14
  export const updateHintVar = (prefix) => `${prefix}_UPDATE_HINT`;
26
- /**
27
- * Walk up from `entry` looking for the package root.
28
- *
29
- * Returns the first ancestor whose `package.json` has our name, and the nearest
30
- * ancestor with any `package.json` as a fallback — see
31
- * {@link InstallInfo.packageRoot}.
32
- */
15
+ /** Walks up from `entry` to the folder whose `package.json` has our name (see `InstallInfo.packageRoot`). */
33
16
  export function findPackageRoot(entry, packageName, fs) {
34
17
  let dir = path.dirname(realpathOr(entry, fs));
35
18
  let nearest;
36
19
  for (;;) {
37
- const manifest = path.join(dir, "package.json");
20
+ const manifest = path.join(dir, 'package.json');
38
21
  if (fs.existsSync(manifest)) {
39
22
  nearest ??= dir;
40
23
  try {
41
- const raw = JSON.parse(fs.readFileSync(manifest, "utf8"));
42
- if (raw.name === packageName)
24
+ if (JSON.parse(fs.readFileSync(manifest, 'utf8')).name === packageName)
43
25
  return { root: dir, named: true };
44
26
  }
45
27
  catch {
46
- // An unreadable or malformed manifest is not our package. Keep walking:
47
- // a broken `package.json` in some intermediate directory must not stop
48
- // the search before it reaches the real root.
28
+ // A broken package.json on the way up isn't ours; keep walking.
49
29
  }
50
30
  }
51
31
  const parent = path.dirname(dir);
@@ -62,197 +42,142 @@ function realpathOr(p, fs) {
62
42
  return p;
63
43
  }
64
44
  }
65
- /**
66
- * Is `p` inside a `node_modules` directory?
67
- *
68
- * Split on the separator rather than matched as a substring, so a project
69
- * genuinely called `my-node_modules` is not mistaken for one (and so Windows
70
- * paths work, since `path.sep` is what split them). SPEC §6's "checkout or npm
71
- * link" row is exactly the negation of this.
72
- */
45
+ const segments = (p) => p.split(/[/\\]/);
46
+ /** Whether a path is inside a `node_modules` folder: by segment, so `my-node_modules` isn't one. */
73
47
  export function underNodeModules(p) {
74
- return p.split(/[/\\]/).includes("node_modules");
48
+ return segments(p).includes('node_modules');
75
49
  }
76
50
  /**
77
- * The npx / pnpm dlx / bunx / yarn dlx caches (SPEC §6).
78
- *
79
- * `_npx` and `bunx-` are matched between separators for the reason above.
80
- * `dlx` has to be looser — pnpm's is `~/.local/share/pnpm/store/.../dlx/<hash>`
81
- * and yarn's is `$TMPDIR/xfs-<hash>/dlx-<n>` — so a path SEGMENT that is `dlx`
82
- * or starts `dlx-` counts.
51
+ * The npx, pnpm dlx, bunx and yarn dlx caches (SPEC §6). `_npx` and `bunx-…` are matched as whole segments; pnpm's
52
+ * `…/dlx/<hash>` and yarn's `xfs-<hash>/dlx-<n>` need a segment that is `dlx` or starts `dlx-`.
83
53
  */
84
54
  export function isEphemeralCachePath(p) {
85
- const parts = p.split(/[/\\]/);
86
- return parts.some((seg) => seg === "_npx" || seg === "dlx" || seg.startsWith("dlx-") || seg.startsWith("bunx-"));
55
+ return segments(p).some((seg) => seg === '_npx' || seg === 'dlx' || seg.startsWith('dlx-') || seg.startsWith('bunx-'));
87
56
  }
88
- /** A path segment-aware "does this path contain this run of segments". */
89
- function hasSegments(p, segments) {
90
- const parts = p.split(/[/\\]/);
91
- for (let i = 0; i + segments.length <= parts.length; i++) {
92
- if (segments.every((s, j) => parts[i + j] === s))
57
+ function hasSegments(p, run) {
58
+ const parts = segments(p);
59
+ for (let i = 0; i + run.length <= parts.length; i++) {
60
+ if (run.every((s, j) => parts[i + j] === s))
93
61
  return true;
94
62
  }
95
63
  return false;
96
64
  }
97
65
  /**
98
- * Which of SPEC §6's rows this install is, checked in the table's own order.
99
- *
100
- * The order is the contract, and two pairs of rows genuinely overlap:
66
+ * Which of SPEC §6's rows this install is, checked in the table's order.
101
67
  *
102
- * - **Docker beats managed.** A container that an Ansible role also labelled
103
- * has both variables set, and the honest answer is "this is an image" — but
104
- * the hint is still carried on {@link InstallInfo.hint} and
105
- * {@link updateCommand} prefers it, because a hint is a sentence an operator
106
- * deliberately wrote and our guess at a `docker compose` line is not.
107
- * - **Checkout beats every global.** They cannot both be true (every global
108
- * layout is under a `node_modules`), so the order between them never decides
109
- * anything; it is kept as the table has it so the two can be read side by
110
- * side.
68
+ * - **Docker beats managed.** A container an Ansible role also labelled is an image, but `hint` still carries the
69
+ * operator's sentence, and `updateCommand` prints it rather than guessing a `docker compose` line.
70
+ * - **No package root at all** reads as a checkout: it refuses to install, and "you're running from source" is the
71
+ * least wrong thing to say.
111
72
  */
112
73
  export function detectInstall(input) {
113
74
  const fs = input.fs ?? fsDefault;
114
75
  const env = input.env;
115
76
  const { root, named } = findPackageRoot(input.entry, input.packageName, fs);
116
77
  const hint = env[updateHintVar(input.envPrefix)];
117
- const base = {
118
- packageRoot: root,
119
- named,
120
- ...(hint !== undefined && hint !== "" ? { hint } : {}),
121
- };
122
- // 1. Docker. The tool's own Dockerfile sets this; `/.dockerenv` is NOT used,
123
- // because dev containers have it too and a global npm install inside one
124
- // is a legitimate thing to update (SPEC §6).
125
- if (env[inDockerVar(input.envPrefix)] === "1") {
126
- return { ...base, method: "docker", canUpdate: false };
127
- }
128
- // 2. Managed: something outside owns this install and said so.
78
+ const base = { packageRoot: root, named, ...(hint !== undefined && hint !== '' ? { hint } : {}) };
79
+ // 1. The tool's own Dockerfile sets this. Never `/.dockerenv`: dev containers have it, and a global install in one
80
+ // is a legitimate thing to update.
81
+ if (env[inDockerVar(input.envPrefix)] === '1')
82
+ return { ...base, method: 'docker', canUpdate: false };
83
+ // 2. Something outside owns this install, and said so.
129
84
  if (base.hint !== undefined)
130
- return { ...base, method: "managed", canUpdate: false };
131
- // The rows below are all about WHERE the package sits, so with no package
132
- // root there is nothing to say. Treat it as a checkout: it is the row whose
133
- // advice ("you are running from source") is least wrong when we cannot tell,
134
- // and it refuses to install rather than guessing a package manager.
85
+ return { ...base, method: 'managed', canUpdate: false };
135
86
  if (root === undefined)
136
- return { ...base, method: "checkout", canUpdate: false };
137
- // 3. npx and friends: a hash-keyed cache that gets pruned.
87
+ return { ...base, method: 'checkout', canUpdate: false };
88
+ // 3. A hash-keyed cache that gets pruned.
138
89
  if (isEphemeralCachePath(root))
139
- return { ...base, method: "npx", canUpdate: false };
90
+ return { ...base, method: 'npx', canUpdate: false };
140
91
  // 4. A checkout or an `npm link`: not under a node_modules at all.
141
92
  if (!underNodeModules(root))
142
- return { ...base, method: "checkout", canUpdate: false };
143
- // 5. Global npm: the package root IS `<npm root -g>/<name>`.
144
- const globalRoot = (input.npmGlobalRoot ?? (() => undefined))();
93
+ return { ...base, method: 'checkout', canUpdate: false };
94
+ // 5. The package root is `<npm root -g>/<name>`.
95
+ const globalRoot = input.npmGlobalRoot?.();
145
96
  if (globalRoot !== undefined && samePath(root, path.join(globalRoot, input.packageName), fs)) {
146
- return { ...base, method: "npm-global", canUpdate: true, manager: "npm" };
147
- }
148
- // 6–9. The other global layouts, each identified by a marker in its path.
149
- if (hasSegments(root, ["pnpm", "global"])) {
150
- return { ...base, method: "pnpm-global", canUpdate: true, manager: "pnpm" };
97
+ return { ...base, method: 'npm-global', canUpdate: true, manager: 'npm' };
151
98
  }
152
- if (hasSegments(root, [".bun", "install", "global"])) {
153
- return { ...base, method: "bun-global", canUpdate: true, manager: "bun" };
154
- }
155
- if (hasSegments(root, [".volta", "tools", "image", "packages"])) {
156
- return { ...base, method: "volta", canUpdate: true, manager: "volta" };
157
- }
158
- // Yarn classic's global folder. `canUpdate: false`: `yarn global add` works,
159
- // but classic is unmaintained and berry has no equivalent, so SPEC §6 prints
160
- // the command rather than running it.
161
- if (hasSegments(root, ["yarn", "global", "node_modules"])) {
162
- return { ...base, method: "yarn-global", canUpdate: false };
163
- }
164
- // 10. Under somebody else's node_modules: a dependency of a project.
165
- return { ...base, method: "dependency", canUpdate: false };
99
+ // 6–9. The other global layouts, each known by a run of segments in its path.
100
+ if (hasSegments(root, ['pnpm', 'global']))
101
+ return { ...base, method: 'pnpm-global', canUpdate: true, manager: 'pnpm' };
102
+ if (hasSegments(root, ['.bun', 'install', 'global']))
103
+ return { ...base, method: 'bun-global', canUpdate: true, manager: 'bun' };
104
+ if (hasSegments(root, ['.volta', 'tools', 'image', 'packages']))
105
+ return { ...base, method: 'volta', canUpdate: true, manager: 'volta' };
106
+ // Yarn classic is unmaintained and berry has no global add, so SPEC §6 prints the command rather than running it.
107
+ if (hasSegments(root, ['yarn', 'global', 'node_modules']))
108
+ return { ...base, method: 'yarn-global', canUpdate: false };
109
+ // 10. Under some other node_modules: a dependency of a project.
110
+ return { ...base, method: 'dependency', canUpdate: false };
166
111
  }
112
+ /** A global root is often reached through a symlink (Homebrew's, nvm's), so realpaths count too. */
167
113
  function samePath(a, b, fs) {
168
- if (path.resolve(a) === path.resolve(b))
169
- return true;
170
- // A global root is routinely reached through a symlink (`/usr/local/lib` →
171
- // `/opt/homebrew/lib`, nvm's per-version dirs), so compare realpaths too.
172
- return realpathOr(a, fs) === realpathOr(b, fs);
114
+ return path.resolve(a) === path.resolve(b) || realpathOr(a, fs) === realpathOr(b, fs);
173
115
  }
174
116
  /**
175
- * The one command to print (SPEC §3, §6): what the reader should run instead.
176
- *
177
- * Returns the command and, where it helps, the parenthetical the spec's example
178
- * carries (`(the global folder isn't writable)`). An install that CAN update
179
- * itself gets `<tool> update`, which is the line the update notice prints.
117
+ * The command SPEC §3's notice prints for this install, and the note in brackets after it. The rows that can update
118
+ * themselves get the package manager's own command here; the notice prints `<tool> update` for those instead.
180
119
  */
181
120
  export function updateCommand(info, input) {
182
121
  const spec = input.target !== undefined ? `${input.packageName}@${input.target}` : input.packageName;
183
122
  switch (info.method) {
184
- case "docker": {
185
- // A hint beats our guess: see detectInstall's note.
123
+ case 'docker': {
186
124
  if (info.hint !== undefined)
187
125
  return { command: info.hint };
188
- const tag = input.target !== undefined ? `:${input.target}` : "";
189
- return {
190
- command: "docker compose pull && docker compose up -d",
191
- ...(input.image !== undefined ? { note: `image ${input.image}${tag}` } : {}),
192
- };
126
+ const tag = input.target !== undefined ? `:${input.target}` : '';
127
+ return { command: 'docker compose pull && docker compose up -d', ...(input.image !== undefined ? { note: `image ${input.image}${tag}` } : {}) };
193
128
  }
194
- case "managed":
195
- return { command: info.hint ?? "ask whoever manages this install" };
196
- case "npx":
197
- return {
198
- command: `npx ${input.packageName}@latest`,
199
- note: "npx already runs the latest version each time",
200
- };
201
- case "checkout":
202
- return {
203
- command: input.checkoutCommand ?? "git pull && npm install && npm run build",
204
- note: "this is a checkout",
205
- };
206
- case "npm-global":
207
- return {
208
- command: `${input.sudo === true ? "sudo " : ""}npm install -g ${spec}`,
209
- ...(input.sudo === true ? { note: "the global folder isn't writable" } : {}),
210
- };
211
- case "pnpm-global":
129
+ case 'managed':
130
+ return { command: info.hint ?? 'ask whoever manages this install' };
131
+ case 'npx':
132
+ return { command: `npx ${input.packageName}@latest`, note: 'npx already runs the latest version each time' };
133
+ case 'checkout':
134
+ return { command: input.checkoutCommand ?? 'git pull && npm install && npm run build', note: 'this is a checkout' };
135
+ case 'npm-global':
136
+ return input.sudo === true
137
+ ? { command: `sudo npm install -g ${spec}`, note: "the global folder isn't writable" }
138
+ : { command: `npm install -g ${spec}` };
139
+ case 'pnpm-global':
212
140
  return { command: `pnpm add -g ${spec}` };
213
- case "bun-global":
141
+ case 'bun-global':
214
142
  return { command: `bun add -g ${spec}` };
215
- case "volta":
143
+ case 'volta':
216
144
  return { command: `volta install ${spec}` };
217
- case "yarn-global":
145
+ case 'yarn-global':
218
146
  return { command: `yarn global add ${spec}` };
219
- case "dependency":
220
- return {
221
- command: `npm install ${spec}`,
222
- note: `run it in the project that depends on ${input.packageName}${info.packageRoot !== undefined ? ` (${projectOf(info.packageRoot)})` : ""}`,
223
- };
147
+ case 'dependency': {
148
+ const project = info.packageRoot !== undefined ? ` (${projectOf(info.packageRoot)})` : '';
149
+ return { command: `npm install ${spec}`, note: `run it in the project that depends on ${input.packageName}${project}` };
150
+ }
224
151
  }
225
152
  }
226
- /** The project a dependency install belongs to: the directory above its `node_modules`. */
153
+ /** The folder above a dependency's `node_modules`. */
227
154
  function projectOf(packageRoot) {
228
- const parts = packageRoot.split(path.sep);
229
- const i = parts.lastIndexOf("node_modules");
230
- return i <= 0 ? packageRoot : parts.slice(0, i).join(path.sep);
155
+ const parts = segments(packageRoot);
156
+ const i = parts.lastIndexOf('node_modules');
157
+ return i <= 0 ? packageRoot : packageRoot.slice(0, parts.slice(0, i).join('/').length);
231
158
  }
232
- /** One line a person can act on, for every row of the table. */
159
+ /** A line a person can read, for each row. */
233
160
  export function describeInstall(info, packageName) {
234
161
  switch (info.method) {
235
- case "docker":
236
- return "running from a container image";
237
- case "managed":
162
+ case 'docker':
163
+ return 'running from a container image';
164
+ case 'managed':
238
165
  return "managed by something outside this machine's package manager";
239
- case "npx":
240
- return "running from an npx / dlx cache";
241
- case "checkout":
242
- return info.packageRoot !== undefined
243
- ? `running from a checkout at ${info.packageRoot}`
244
- : "running from a checkout";
245
- case "npm-global":
246
- return "installed globally with npm";
247
- case "pnpm-global":
248
- return "installed globally with pnpm";
249
- case "bun-global":
250
- return "installed globally with bun";
251
- case "volta":
252
- return "installed with volta";
253
- case "yarn-global":
254
- return "installed globally with yarn (classic)";
255
- case "dependency":
166
+ case 'npx':
167
+ return 'running from an npx or dlx cache';
168
+ case 'checkout':
169
+ return info.packageRoot !== undefined ? `running from a checkout at ${info.packageRoot}` : 'running from a checkout';
170
+ case 'npm-global':
171
+ return 'installed globally with npm';
172
+ case 'pnpm-global':
173
+ return 'installed globally with pnpm';
174
+ case 'bun-global':
175
+ return 'installed globally with bun';
176
+ case 'volta':
177
+ return 'installed with volta';
178
+ case 'yarn-global':
179
+ return 'installed globally with yarn (classic)';
180
+ case 'dependency':
256
181
  return `installed as a dependency of another project (${packageName})`;
257
182
  }
258
183
  }
@@ -3,16 +3,17 @@ export declare function lockPath(home: string): string;
3
3
  export interface LockBody {
4
4
  pid: number;
5
5
  startedAt: string;
6
- /** What is holding it, for the message: `paddock update --to 0.81.0`. */
6
+ /** What holds it, for the message: `paddock update --to 0.81.0`. */
7
7
  command?: string;
8
8
  }
9
9
  export interface Lock {
10
10
  path: string;
11
- /** Remove the file, but only if it is still ours. Safe to call twice. */
11
+ /** Removes the file if it's still ours. Safe to call twice. */
12
12
  release(): void;
13
13
  }
14
- /** The lock is held by a live process. Carries its pid so the caller can say so. */
14
+ /** Another live process holds the lock. */
15
15
  export declare class LockHeldError extends Error {
16
+ name: string;
16
17
  readonly holder: LockBody;
17
18
  readonly path: string;
18
19
  constructor(file: string, holder: LockBody);
@@ -21,20 +22,14 @@ export interface AcquireOptions {
21
22
  home: string;
22
23
  pid?: number;
23
24
  command?: string;
24
- /** Injected so a stale lock can be tested without spawning a process to kill. */
25
25
  isAlive?: (pid: number) => boolean;
26
26
  now?: () => Date;
27
27
  }
28
28
  /**
29
- * Take the lock, or throw {@link LockHeldError}.
30
- *
31
- * The retry is deliberately ONCE. After removing a stale lock the create is
32
- * attempted again, and a second `EEXIST` means another process won the same
33
- * race in that microsecond — which is a genuine holder, not a stale file, so it
34
- * refuses. Looping instead would be a way to spin forever against a lock being
35
- * recreated as fast as it is deleted.
29
+ * Takes the lock, or throws `LockHeldError`. After removing a stale lock it tries once more, and only once: a second
30
+ * `EEXIST` means another process won the same race just now, which is a real holder.
36
31
  */
37
32
  export declare function acquireLock(o: AcquireOptions): Lock;
38
33
  export declare function readLock(file: string): LockBody | null;
39
- /** Remove the lock only when it still names `pid`: never delete somebody else's. */
34
+ /** Removes the lock only if it names `pid`, or names nothing: never somebody else's. */
40
35
  export declare function releaseLock(file: string, pid: number): void;