@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.
- package/package.json +1 -1
- package/packages/server/dist/cli/args.js +2 -2
- package/packages/server/dist/cli/busy-command.js +1 -1
- package/packages/server/dist/cli/busy.js +10 -12
- package/packages/server/dist/cli/exit-codes.d.ts +21 -45
- package/packages/server/dist/cli/exit-codes.js +42 -45
- package/packages/server/dist/cli/output.js +5 -6
- package/packages/server/dist/cli/paddock.js +9 -9
- package/packages/server/dist/cli/service/index.d.ts +2 -2
- package/packages/server/dist/cli/service/index.js +30 -24
- package/packages/server/dist/cli/service/launchd.d.ts +31 -84
- package/packages/server/dist/cli/service/launchd.js +104 -304
- package/packages/server/dist/cli/service/logs.d.ts +9 -28
- package/packages/server/dist/cli/service/logs.js +33 -75
- package/packages/server/dist/cli/service/manager.d.ts +93 -0
- package/packages/server/dist/cli/service/manager.js +21 -0
- package/packages/server/dist/cli/service/path-env.d.ts +7 -18
- package/packages/server/dist/cli/service/path-env.js +30 -81
- package/packages/server/dist/cli/service/spec.d.ts +4 -31
- package/packages/server/dist/cli/service/spec.js +9 -0
- package/packages/server/dist/cli/service/systemd.d.ts +27 -101
- package/packages/server/dist/cli/service/systemd.js +106 -262
- package/packages/server/dist/cli/update/health.d.ts +48 -0
- package/packages/server/dist/cli/update/health.js +64 -0
- package/packages/server/dist/cli/update/install-method.d.ts +29 -76
- package/packages/server/dist/cli/update/install-method.js +103 -178
- package/packages/server/dist/cli/update/lock.d.ts +7 -12
- package/packages/server/dist/cli/update/lock.js +28 -62
- package/packages/server/dist/cli/update/package-manager.d.ts +39 -74
- package/packages/server/dist/cli/update/package-manager.js +95 -160
- package/packages/server/dist/cli/update/registry.d.ts +17 -54
- package/packages/server/dist/cli/update/registry.js +46 -89
- package/packages/server/dist/cli/update/semver.d.ts +18 -41
- package/packages/server/dist/cli/update/semver.js +74 -124
- package/packages/server/dist/cli/update/update-check.d.ts +15 -26
- package/packages/server/dist/cli/update/update-check.js +30 -60
- package/packages/server/dist/cli/update/update.d.ts +57 -91
- package/packages/server/dist/cli/update/update.js +230 -431
- package/packages/server/dist/cli/update-command.js +3 -5
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-CphdGB3R.js → DeleteProjectDialog-CgHpQctt.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CG6cf-0i.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-B_kcF4sm.js → DiscoverView-BKqI0I1E.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CvUK5DKW.js → InstanceConfigPage-DL3raCK1.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DvZ-j_Ao.js → MarkdownRenderer-DcjEuf3s.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DayHvUH2.js → ProjectRedirect-DerkA-nE.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-CCw83gUi.js → ProjectView-NwfC3_w7.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-B3Klg2H4.js → ProjectsGrid-BeORa0UH.js} +1 -1
- package/packages/web/dist/assets/{RootHome-BxcDvpgI.js → RootHome-BYJs8qJ6.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-7UlhFBSG.js → Surfaces-w-kEaAYU.js} +1 -1
- package/packages/web/dist/assets/{arc-DNkh5o5Q.js → arc-Bv56hasM.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BrgN3oLF.js → architectureDiagram-3BPJPVTR-BHEknx9i.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-D05XOJSk.js → blockDiagram-GPEHLZMM-DtxTMrPX.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CKaMBhMP.js → c4Diagram-AAUBKEIU-DRpEYOlM.js} +1 -1
- package/packages/web/dist/assets/channel-CdIiAqY2.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8hgadCQc.js → chunk-2J33WTMH-S4s7GCg7.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-CBfEC-pu.js → chunk-4BX2VUAB-BkWVPLf1.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-DV1eMgHk.js → chunk-55IACEB6-4-FQp01F.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-Cfgl5fxY.js → chunk-727SXJPM-BeyDWdIb.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BXhqD295.js → chunk-AQP2D5EJ-ZSxKy745.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-TFGOPtek.js → chunk-FMBD7UC4-Bq8uV9j-.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CHFM665R.js → chunk-ND2GUHAM-BOFsRyHk.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CqEf6MMT.js → chunk-QZHKN3VN-uZ6oNc4m.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +2 -0
- package/packages/web/dist/assets/{core-DmEaKxBb.js → core-CYuJAM2U.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BnGI6jkr.js → cose-bilkent-S5V4N54A-DIzkVSFb.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CKoPx61E.js → dagre-BM42HDAG-CHR6jJBv.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-mMmtlBl1.js → diagram-2AECGRRQ-BAFXCH8Q.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Cdz9Mtnt.js → diagram-5GNKFQAL-BJ5t5gbz.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-DQxM3wYj.js → diagram-KO2AKTUF-D9Xt0IVg.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BWrinPsq.js → diagram-LMA3HP47-Bq38u6sy.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-U6MYUve2.js → diagram-OG6HWLK6-CMQVqkpi.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-DGjRXuTP.js → documentTitle-M1lrfXRU.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-xqMCsXxY.js → erDiagram-TEJ5UH35-C63M1g9b.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-PMfy_czF.js → flowDiagram-I6XJVG4X-D3j8ACN7.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-whhYYE4N.js → ganttDiagram-6RSMTGT7-B1Fd1A2Y.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-C51JW4tf.js → gitGraphDiagram-PVQCEYII-CEhZhDdR.js} +1 -1
- package/packages/web/dist/assets/{index-CHOtvlnO.js → index-1E9yHciS.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-SH8cr905.js → infoDiagram-5YYISTIA-Ctquaew6.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BjqaoU3U.js → ishikawaDiagram-YF4QCWOH-DV4GvA1R.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CBmdEVrd.js → journeyDiagram-JHISSGLW-C_Cy9waA.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-U8njjMLZ.js → kanban-definition-UN3LZRKU-CGr9ctEf.js} +1 -1
- package/packages/web/dist/assets/{linear-DF66GPs1.js → linear-D3U8NOkt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CouJz7Ph.js → mermaid.core-BiArdWKV.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BYIf0oFt.js → mindmap-definition-RKZ34NQL-CZIpzj8Z.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Cm8rgg09.js → pieDiagram-4H26LBE5-1MaeS3rt.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-C3Il5kOH.js → quadrantDiagram-W4KKPZXB-B5pgQ54e.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D3PE2nTe.js → requirementDiagram-4Y6WPE33-BNnxrpGr.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DdmvBM18.js → sankeyDiagram-5OEKKPKP-DsxVH-hM.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-B8M_GAIq.js → sequenceDiagram-3UESZ5HK-Ke9LcFZI.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DZkXnK1_.js → stateDiagram-AJRCARHV-UJzmflND.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-IHXq7rqi.js → timeline-definition-PNZ67QCA-BRjNALVI.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dga7tLtT.js → vennDiagram-CIIHVFJN-ClMAspxz.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CmcwnEJ-.js → wardley-L42UT6IY-UPv4JEmz.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-zHuSLrD4.js → wardleyDiagram-YWT4CUSO-BgYJbdEx.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-D93MU_ic.js → xychartDiagram-2RQKCTM6-Bfk5HjGx.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/server/dist/cli/service/backend.d.ts +0 -115
- package/packages/server/dist/cli/service/backend.js +0 -19
- package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +0 -2
- package/packages/web/dist/assets/channel-JrEedxB8.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +0 -2
|
@@ -1,131 +1,84 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export type InstallMethod =
|
|
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 =
|
|
4
|
+
export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'volta';
|
|
5
5
|
export interface FsLike {
|
|
6
6
|
existsSync(p: string): boolean;
|
|
7
|
-
readFileSync(p: string, encoding:
|
|
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
|
|
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`
|
|
40
|
-
*
|
|
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
|
-
/**
|
|
30
|
+
/** Whether the name was found, rather than fallen back from. */
|
|
51
31
|
named: boolean;
|
|
52
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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,
|
|
66
|
+
/** The version to install, once it's known. */
|
|
111
67
|
target?: string;
|
|
112
|
-
/** The
|
|
68
|
+
/** The image, for the Docker row: `ghcr.io/edspencer/paddock`. */
|
|
113
69
|
image?: string;
|
|
114
|
-
/**
|
|
70
|
+
/** `sudo` in front: SPEC §5 step 5's not-writable case. */
|
|
115
71
|
sudo?: boolean;
|
|
116
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
20
|
+
const manifest = path.join(dir, 'package.json');
|
|
38
21
|
if (fs.existsSync(manifest)) {
|
|
39
22
|
nearest ??= dir;
|
|
40
23
|
try {
|
|
41
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
48
|
+
return segments(p).includes('node_modules');
|
|
75
49
|
}
|
|
76
50
|
/**
|
|
77
|
-
* The npx
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
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:
|
|
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:
|
|
137
|
-
// 3.
|
|
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:
|
|
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:
|
|
143
|
-
// 5.
|
|
144
|
-
const globalRoot =
|
|
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:
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (hasSegments(root, [
|
|
156
|
-
return { ...base, method:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
195
|
-
return { command: info.hint ??
|
|
196
|
-
case
|
|
197
|
-
return {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
141
|
+
case 'bun-global':
|
|
214
142
|
return { command: `bun add -g ${spec}` };
|
|
215
|
-
case
|
|
143
|
+
case 'volta':
|
|
216
144
|
return { command: `volta install ${spec}` };
|
|
217
|
-
case
|
|
145
|
+
case 'yarn-global':
|
|
218
146
|
return { command: `yarn global add ${spec}` };
|
|
219
|
-
case
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
153
|
+
/** The folder above a dependency's `node_modules`. */
|
|
227
154
|
function projectOf(packageRoot) {
|
|
228
|
-
const parts = packageRoot
|
|
229
|
-
const i = parts.lastIndexOf(
|
|
230
|
-
return i <= 0 ? packageRoot : parts.slice(0, i).join(
|
|
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
|
-
/**
|
|
159
|
+
/** A line a person can read, for each row. */
|
|
233
160
|
export function describeInstall(info, packageName) {
|
|
234
161
|
switch (info.method) {
|
|
235
|
-
case
|
|
236
|
-
return
|
|
237
|
-
case
|
|
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
|
|
240
|
-
return
|
|
241
|
-
case
|
|
242
|
-
return info.packageRoot !== undefined
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
case
|
|
246
|
-
return
|
|
247
|
-
case
|
|
248
|
-
return
|
|
249
|
-
case
|
|
250
|
-
return
|
|
251
|
-
case
|
|
252
|
-
return
|
|
253
|
-
case
|
|
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
|
|
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
|
-
/**
|
|
11
|
+
/** Removes the file if it's still ours. Safe to call twice. */
|
|
12
12
|
release(): void;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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;
|