@ozyman42/ozy-cli 0.4.6-windows-x64.0 → 0.4.6

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 (207) hide show
  1. package/bin/ozy +191 -0
  2. package/bin/ozy-signing-agent +191 -0
  3. package/bin/ozy-ssh-keygen +191 -0
  4. package/bin/ozy-virtual-security-key +191 -0
  5. package/dist/commands/git/hosts.js +18 -0
  6. package/dist/commands/git/hosts.js.map +1 -0
  7. package/dist/commands/git/index.js +8 -0
  8. package/dist/commands/git/index.js.map +1 -0
  9. package/dist/commands/git/setup.js +145 -0
  10. package/dist/commands/git/setup.js.map +1 -0
  11. package/dist/commands/index.js +13 -0
  12. package/dist/commands/index.js.map +1 -0
  13. package/dist/commands/npm/index.js +7 -0
  14. package/dist/commands/npm/index.js.map +1 -0
  15. package/dist/commands/npm/setup/git-remote.js +66 -0
  16. package/dist/commands/npm/setup/git-remote.js.map +1 -0
  17. package/dist/commands/npm/setup/index.js +168 -0
  18. package/dist/commands/npm/setup/index.js.map +1 -0
  19. package/dist/commands/npm/setup/npm-auth.js +62 -0
  20. package/dist/commands/npm/setup/npm-auth.js.map +1 -0
  21. package/dist/commands/npm/setup/package-json-validate.js +47 -0
  22. package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
  23. package/dist/commands/npm/setup/package-registry.js +20 -0
  24. package/dist/commands/npm/setup/package-registry.js.map +1 -0
  25. package/dist/commands/npm/setup/setup.test.js +95 -0
  26. package/dist/commands/npm/setup/setup.test.js.map +1 -0
  27. package/dist/commands/npm/setup/trusted-publishing.js +84 -0
  28. package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
  29. package/dist/commands/npm/setup/workflow.js +19 -0
  30. package/dist/commands/npm/setup/workflow.js.map +1 -0
  31. package/dist/commands/ssh/get-sk-credential.js +104 -0
  32. package/dist/commands/ssh/get-sk-credential.js.map +1 -0
  33. package/dist/commands/ssh/index.js +7 -0
  34. package/dist/commands/ssh/index.js.map +1 -0
  35. package/dist/commands/upgrade.js +81 -0
  36. package/dist/commands/upgrade.js.map +1 -0
  37. package/dist/common/command.js +15 -0
  38. package/dist/common/command.js.map +1 -0
  39. package/dist/common/constants.js +25 -0
  40. package/dist/common/constants.js.map +1 -0
  41. package/dist/common/effective-modules-extensions.js +13 -0
  42. package/dist/common/effective-modules-extensions.js.map +1 -0
  43. package/dist/common/log.js +4 -0
  44. package/dist/common/log.js.map +1 -0
  45. package/dist/common/render-caller-tree.js +20 -0
  46. package/dist/common/render-caller-tree.js.map +1 -0
  47. package/dist/entrypoints/ozy-signing-agent.js +48 -0
  48. package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
  49. package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
  50. package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
  51. package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
  52. package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
  53. package/dist/entrypoints/ozy.js +3 -0
  54. package/dist/entrypoints/ozy.js.map +1 -0
  55. package/dist/index.js +2 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/internal/thing.js +4 -0
  58. package/dist/internal/thing.js.map +1 -0
  59. package/dist/modules/cli/agent-client/impl.js +109 -0
  60. package/dist/modules/cli/agent-client/impl.js.map +1 -0
  61. package/dist/modules/cli/agent-client/interface.js +11 -0
  62. package/dist/modules/cli/agent-client/interface.js.map +1 -0
  63. package/dist/modules/cli/git/impl.js +124 -0
  64. package/dist/modules/cli/git/impl.js.map +1 -0
  65. package/dist/modules/cli/git/interface.js +3 -0
  66. package/dist/modules/cli/git/interface.js.map +1 -0
  67. package/dist/modules/cli/git/submodule/fix.js +257 -0
  68. package/dist/modules/cli/git/submodule/fix.js.map +1 -0
  69. package/dist/modules/cli/git/submodule/git-state.js +154 -0
  70. package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
  71. package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
  72. package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
  73. package/dist/modules/cli/git/submodule/printer.js +232 -0
  74. package/dist/modules/cli/git/submodule/printer.js.map +1 -0
  75. package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
  76. package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
  77. package/dist/modules/cli/github/impl.js +137 -0
  78. package/dist/modules/cli/github/impl.js.map +1 -0
  79. package/dist/modules/cli/github/interface.js +2 -0
  80. package/dist/modules/cli/github/interface.js.map +1 -0
  81. package/dist/modules/cli/index.js +9 -0
  82. package/dist/modules/cli/index.js.map +1 -0
  83. package/dist/modules/common/crypto/impl.js +133 -0
  84. package/dist/modules/common/crypto/impl.js.map +1 -0
  85. package/dist/modules/common/crypto/interface.js +2 -0
  86. package/dist/modules/common/crypto/interface.js.map +1 -0
  87. package/dist/modules/common/index.js +10 -0
  88. package/dist/modules/common/index.js.map +1 -0
  89. package/dist/modules/common/kep-map-store/impl.js +42 -0
  90. package/dist/modules/common/kep-map-store/impl.js.map +1 -0
  91. package/dist/modules/common/kep-map-store/interface.js +2 -0
  92. package/dist/modules/common/kep-map-store/interface.js.map +1 -0
  93. package/dist/modules/common/os-platform/caller-info.js +132 -0
  94. package/dist/modules/common/os-platform/caller-info.js.map +1 -0
  95. package/dist/modules/common/os-platform/impl.js +87 -0
  96. package/dist/modules/common/os-platform/impl.js.map +1 -0
  97. package/dist/modules/common/os-platform/interface.js +2 -0
  98. package/dist/modules/common/os-platform/interface.js.map +1 -0
  99. package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
  100. package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
  101. package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
  102. package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
  103. package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
  104. package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
  105. package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
  106. package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
  107. package/dist/modules/common/ssh-config/impl.js +116 -0
  108. package/dist/modules/common/ssh-config/impl.js.map +1 -0
  109. package/dist/modules/common/ssh-config/interface.js +10 -0
  110. package/dist/modules/common/ssh-config/interface.js.map +1 -0
  111. package/dist/modules/ssh-agent/index.js +8 -0
  112. package/dist/modules/ssh-agent/index.js.map +1 -0
  113. package/dist/modules/ssh-agent/session/impl.js +265 -0
  114. package/dist/modules/ssh-agent/session/impl.js.map +1 -0
  115. package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
  116. package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
  117. package/dist/modules/ssh-agent/session/interface.js +32 -0
  118. package/dist/modules/ssh-agent/session/interface.js.map +1 -0
  119. package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
  120. package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
  121. package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
  122. package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
  123. package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
  124. package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
  125. package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
  126. package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
  127. package/dist/scripts/build.js +412 -0
  128. package/dist/scripts/build.js.map +1 -0
  129. package/dist/scripts/check-npm-version.js +8 -0
  130. package/dist/scripts/check-npm-version.js.map +1 -0
  131. package/dist/scripts/kill-existing-agent.js +21 -0
  132. package/dist/scripts/kill-existing-agent.js.map +1 -0
  133. package/dist/scripts/publish.js +46 -0
  134. package/dist/scripts/publish.js.map +1 -0
  135. package/package.json +52 -11
  136. package/src/commands/git/hosts.ts +20 -0
  137. package/src/commands/git/index.ts +9 -0
  138. package/src/commands/git/setup.ts +182 -0
  139. package/src/commands/index.ts +15 -0
  140. package/src/commands/npm/index.ts +8 -0
  141. package/src/commands/npm/setup/git-remote.ts +78 -0
  142. package/src/commands/npm/setup/index.ts +189 -0
  143. package/src/commands/npm/setup/npm-auth.ts +58 -0
  144. package/src/commands/npm/setup/package-json-validate.ts +68 -0
  145. package/src/commands/npm/setup/package-registry.ts +24 -0
  146. package/src/commands/npm/setup/publish-workflow.yml +35 -0
  147. package/src/commands/npm/setup/setup.test.ts +117 -0
  148. package/src/commands/npm/setup/trusted-publishing.ts +106 -0
  149. package/src/commands/npm/setup/workflow.ts +20 -0
  150. package/src/commands/ssh/get-sk-credential.ts +110 -0
  151. package/src/commands/ssh/index.ts +8 -0
  152. package/src/commands/upgrade.ts +90 -0
  153. package/src/common/command.ts +16 -0
  154. package/src/common/constants.ts +26 -0
  155. package/src/common/effective-modules-extensions.ts +24 -0
  156. package/src/common/log.ts +3 -0
  157. package/src/common/render-caller-tree.ts +22 -0
  158. package/src/entrypoints/ozy-signing-agent.ts +48 -0
  159. package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
  160. package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
  161. package/src/entrypoints/ozy.ts +2 -0
  162. package/src/index.ts +1 -0
  163. package/src/internal/thing.ts +3 -0
  164. package/src/modules/cli/agent-client/impl.ts +152 -0
  165. package/src/modules/cli/agent-client/interface.ts +17 -0
  166. package/src/modules/cli/git/impl.ts +133 -0
  167. package/src/modules/cli/git/interface.ts +23 -0
  168. package/src/modules/cli/git/submodule/fix.ts +298 -0
  169. package/src/modules/cli/git/submodule/git-state.ts +217 -0
  170. package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
  171. package/src/modules/cli/git/submodule/printer.ts +256 -0
  172. package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
  173. package/src/modules/cli/github/impl.ts +169 -0
  174. package/src/modules/cli/github/interface.ts +29 -0
  175. package/src/modules/cli/index.ts +16 -0
  176. package/src/modules/common/crypto/impl.ts +173 -0
  177. package/src/modules/common/crypto/interface.ts +20 -0
  178. package/src/modules/common/index.ts +19 -0
  179. package/src/modules/common/kep-map-store/impl.ts +47 -0
  180. package/src/modules/common/kep-map-store/interface.ts +9 -0
  181. package/src/modules/common/os-platform/caller-info.ts +152 -0
  182. package/src/modules/common/os-platform/impl.ts +86 -0
  183. package/src/modules/common/os-platform/interface.ts +16 -0
  184. package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
  185. package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
  186. package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
  187. package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
  188. package/src/modules/common/ssh-config/impl.ts +130 -0
  189. package/src/modules/common/ssh-config/interface.ts +33 -0
  190. package/src/modules/ssh-agent/index.ts +13 -0
  191. package/src/modules/ssh-agent/session/agent-notes.md +210 -0
  192. package/src/modules/ssh-agent/session/design.md +2 -0
  193. package/src/modules/ssh-agent/session/html-refactor.md +23 -0
  194. package/src/modules/ssh-agent/session/impl.ts +328 -0
  195. package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
  196. package/src/modules/ssh-agent/session/interface.ts +62 -0
  197. package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
  198. package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
  199. package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
  200. package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
  201. package/src/scripts/build.ts +506 -0
  202. package/src/scripts/check-npm-version.ts +7 -0
  203. package/src/scripts/command-launcher.js.ejs +168 -0
  204. package/src/scripts/kill-existing-agent.ts +22 -0
  205. package/src/scripts/publish.ts +47 -0
  206. package/src/scripts/windows-bin-shims.md +1104 -0
  207. package/multi-call-binary.exe +0 -0
@@ -0,0 +1,1104 @@
1
+ # Windows bin shim audit
2
+
3
+ This document describes what the package managers do when a package exposes
4
+ commands through `package.json#bin`, with focus on the Windows first-run repair
5
+ problem for `ozy-cli`.
6
+
7
+ The goal is mechanical: list the files created by each installer, the target
8
+ they point at, the code that creates them, and what still needs real Windows
9
+ fixture verification. Do not implement a shim-replacement strategy from memory;
10
+ use this file as the checklist.
11
+
12
+ ## Package shape under discussion
13
+
14
+ Base package manifest shape:
15
+
16
+ ```json
17
+ {
18
+ "name": "@scope/tool",
19
+ "bin": {
20
+ "ozy": "./bin/ozy",
21
+ "ozy-ssh-keygen": "./bin/ozy-ssh-keygen",
22
+ "ozy-signing-agent": "./bin/ozy-signing-agent"
23
+ },
24
+ "optionalDependencies": {
25
+ "@scope/tool-darwin-arm64": "1.2.3",
26
+ "@scope/tool-darwin-x64": "1.2.3",
27
+ "@scope/tool-linux-arm64": "1.2.3",
28
+ "@scope/tool-linux-x64": "1.2.3",
29
+ "@scope/tool-windows-x64": "1.2.3"
30
+ }
31
+ }
32
+ ```
33
+
34
+ Base package file shape:
35
+
36
+ ```text
37
+ package/
38
+ package.json
39
+ bin/
40
+ ozy
41
+ ozy-ssh-keygen
42
+ ozy-signing-agent
43
+ src/
44
+ ...
45
+ ```
46
+
47
+ Each file in `bin/` can be identical JS install/repair code. The useful property
48
+ is that each command has a distinct installer-owned bin target, which gives the
49
+ package manager a chance to create one shim set per command.
50
+
51
+ Platform package shape:
52
+
53
+ ```text
54
+ package/
55
+ package.json
56
+ multi-call-binary # POSIX
57
+ ```
58
+
59
+ ```text
60
+ package/
61
+ package.json
62
+ multi-call-binary.exe # Windows
63
+ ```
64
+
65
+ The platform package should not be relied on to expose final command links for
66
+ the base package. A package manager generally links the bins of the package
67
+ being installed as a direct dependency/global package. It does not make an
68
+ optional dependency's `bin` entries stand in for the parent package commands in
69
+ all local/global cases.
70
+
71
+ ## Implemented package-local repair strategy
72
+
73
+ The base package owns every public command through package-owned files:
74
+
75
+ ```text
76
+ package/
77
+ bin/
78
+ ozy
79
+ ozy-signing-agent
80
+ ozy-ssh-keygen
81
+ ```
82
+
83
+ Each file has identical JS launcher contents. The filename is the command name.
84
+ The generated base `package.json#bin` points each command at its matching file:
85
+
86
+ ```json
87
+ {
88
+ "bin": {
89
+ "ozy": "./bin/ozy",
90
+ "ozy-signing-agent": "./bin/ozy-signing-agent",
91
+ "ozy-ssh-keygen": "./bin/ozy-ssh-keygen"
92
+ }
93
+ }
94
+ ```
95
+
96
+ The platform packages do not expose `bin` entries. They only contain the native
97
+ multi-call payload:
98
+
99
+ ```text
100
+ multi-call-binary # POSIX
101
+ multi-call-binary.exe # Windows
102
+ ```
103
+
104
+ At runtime, the JS launcher resolves the installed optional platform package and
105
+ materializes command-named executables next to the launchers.
106
+
107
+ POSIX first run:
108
+
109
+ ```text
110
+ package/bin/ozy # JS launcher
111
+ package/bin/ozy-signing-agent # JS launcher
112
+ package/bin/ozy-ssh-keygen # JS launcher
113
+ platform/multi-call-binary # native payload
114
+ ```
115
+
116
+ The launcher materializes the first command path from the platform
117
+ `multi-call-binary`, using a hardlink first and falling back to one copy. It
118
+ then hardlinks every other command path to that first materialized command:
119
+
120
+ ```text
121
+ package/bin/ozy # native multi-call executable after repair
122
+ package/bin/ozy-signing-agent # hardlink to package/bin/ozy
123
+ package/bin/ozy-ssh-keygen # hardlink to package/bin/ozy
124
+ ```
125
+
126
+ Then it spawns `package/bin/ozy`. Later invocations through package-manager
127
+ `.bin` symlinks run the native command path directly.
128
+
129
+ Windows first run:
130
+
131
+ ```text
132
+ package/bin/ozy # JS launcher; remains JS
133
+ package/bin/ozy-signing-agent # JS launcher; remains JS
134
+ package/bin/ozy-ssh-keygen # JS launcher; remains JS
135
+ platform/multi-call-binary.exe # native payload
136
+ ```
137
+
138
+ The launcher materializes all command `.exe` files together. It hardlinks the
139
+ first command `.exe` from `multi-call-binary.exe`, falling back to one copy when
140
+ that hardlink fails. It then hardlinks every other command `.exe` to the first
141
+ materialized `.exe`:
142
+
143
+ ```text
144
+ package/bin/ozy # JS launcher
145
+ package/bin/ozy.exe # native multi-call executable
146
+ package/bin/ozy-signing-agent # JS launcher
147
+ package/bin/ozy-signing-agent.exe # hardlink to package/bin/ozy.exe
148
+ package/bin/ozy-ssh-keygen # JS launcher
149
+ package/bin/ozy-ssh-keygen.exe # hardlink to package/bin/ozy.exe
150
+ package/bin/installed.json # { "version": "<platform-package-version>" }
151
+ ```
152
+
153
+ Then it spawns `package/bin/ozy.exe`. Later invocations still pass through the
154
+ package-manager shim and JS launcher, but the final process image is the
155
+ command-specific `.exe`.
156
+
157
+ The Windows `installed.json` records only the platform package version. If it
158
+ does not match the currently resolved optional dependency version, or if any
159
+ command `.exe` is missing, the launcher recreates all command `.exe` files
160
+ together.
161
+
162
+ ## Source links
163
+
164
+ ### npm
165
+
166
+ - npm `bin-links` selects Windows shim generation here:
167
+ https://github.com/npm/bin-links/blob/main/lib/link-bins.js
168
+ - npm `bin-links` local/global target rules:
169
+ https://github.com/npm/bin-links/blob/main/lib/index.js
170
+ https://github.com/npm/bin-links/blob/main/lib/bin-target.js
171
+ - npm `bin-links` calls `cmd-shim` and tracks `name`, `name.cmd`, `name.ps1`:
172
+ https://github.com/npm/bin-links/blob/main/lib/shim-bin.js
173
+ - npm `cmd-shim` writes the actual `name`, `name.cmd`, and `name.ps1` files:
174
+ https://github.com/npm/cmd-shim/blob/main/lib/index.js
175
+
176
+ ### pnpm
177
+
178
+ - pnpm `@pnpm/link-bins` gathers package bin entries and calls
179
+ `@zkochan/cmd-shim`:
180
+ https://github.com/pnpm/pnpm/blob/main/pkg-manager/link-bins/src/index.ts
181
+ - pnpm `@zkochan/cmd-shim` writes shell, CMD, and PowerShell shims:
182
+ https://github.com/pnpm/cmd-shim/blob/main/src/index.ts
183
+
184
+ ### Bun package manager
185
+
186
+ - Bun chooses POSIX symlink vs Windows shim in `src/install/bin.rs`:
187
+ https://github.com/oven-sh/bun/blob/main/src/install/bin.rs#L936-L952
188
+ - Bun writes `name.bunx` metadata and `name.exe` PE shim:
189
+ https://github.com/oven-sh/bun/blob/main/src/install/bin.rs#L1138-L1254
190
+ - Bun's Windows shim metadata maps target extensions to launchers:
191
+ https://github.com/oven-sh/bun/blob/main/src/install/windows-shim/BinLinkingShim.rs#L174-L260
192
+ - Bun's PE shim implementation reads adjacent `.bunx` metadata:
193
+ https://github.com/oven-sh/bun/blob/main/src/install/windows-shim/bun_shim_impl.rs
194
+
195
+ ### Yarn Classic
196
+
197
+ - Yarn Classic uses `@zkochan/cmd-shim` on Windows with
198
+ `createPwshFile: false`, and POSIX symlinks elsewhere:
199
+ https://github.com/yarnpkg/yarn/blob/master/src/package-linker.js#L22-L45
200
+ - Yarn Classic links package bins into `.bin` under the target install location:
201
+ https://github.com/yarnpkg/yarn/blob/master/src/package-linker.js#L74-L94
202
+
203
+ ### Yarn Berry
204
+
205
+ - Yarn Berry `nodeLinker: node-modules` builds `.bin` link maps from package
206
+ manifests:
207
+ https://github.com/yarnpkg/berry/blob/master/packages/plugin-nm/sources/NodeModulesLinker.ts#L1014-L1072
208
+ - Yarn Berry `nodeLinker: node-modules` writes Windows shims with
209
+ `@zkochan/cmd-shim` and `createPwshFile: false`:
210
+ https://github.com/yarnpkg/berry/blob/master/packages/plugin-nm/sources/NodeModulesLinker.ts#L1399-L1436
211
+ - Yarn Berry script execution creates temporary wrappers through
212
+ `scriptUtils.makePathWrapper`:
213
+ https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/scriptUtils.ts#L40-L50
214
+ - Yarn Berry script execution classifies native executables vs JS-like scripts:
215
+ https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/scriptUtils.ts#L607-L632
216
+ - Yarn Berry script execution installs temporary accessible binaries:
217
+ https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/scriptUtils.ts#L713-L739
218
+ - Yarn Berry `dlx` installs into a temporary project, then executes a workspace
219
+ accessible binary:
220
+ https://github.com/yarnpkg/berry/blob/master/packages/plugin-dlx/sources/commands/dlx.ts#L46-L132
221
+
222
+ ## Generated files by package manager
223
+
224
+ The examples below assume this manifest:
225
+
226
+ ```json
227
+ {
228
+ "name": "@scope/tool",
229
+ "bin": {
230
+ "ozy": "./bin/ozy"
231
+ }
232
+ }
233
+ ```
234
+
235
+ and the package contains:
236
+
237
+ ```text
238
+ package/
239
+ bin/
240
+ ozy
241
+ ```
242
+
243
+ ### npm, local dependency install on Windows
244
+
245
+ Observed from source:
246
+
247
+ - `bin-links/lib/link-bins.js` selects `shim-bin.js` on Windows.
248
+ - `bin-links/lib/bin-target.js` targets `node_modules/.bin` for non-top
249
+ packages.
250
+ - `shim-bin.js` tracks and creates `ozy`, `ozy.cmd`, and `ozy.ps1`.
251
+ - `cmd-shim` writes all three files.
252
+
253
+ Expected directory structure:
254
+
255
+ ```text
256
+ project/
257
+ node_modules/
258
+ .bin/
259
+ ozy
260
+ ozy.cmd
261
+ ozy.ps1
262
+ @scope/
263
+ tool/
264
+ package.json
265
+ bin/
266
+ ozy
267
+ ```
268
+
269
+ Important npm top-package detail: npm `bin-links` returns without linking bins
270
+ for a non-global top package. That is about the project root package itself, not
271
+ about a package installed into `node_modules` as a dependency.
272
+
273
+ ### npm, global install on Windows
274
+
275
+ Observed from source:
276
+
277
+ - `bin-links/lib/index.js` states that global top packages on Windows get bins
278
+ installed in `{prefix}`.
279
+ - `bin-target.js` returns `getPrefix(path)` for Windows global top packages.
280
+ - The package payload lives under `{prefix}/node_modules`.
281
+
282
+ Expected directory structure:
283
+
284
+ ```text
285
+ <npm-prefix>/
286
+ ozy
287
+ ozy.cmd
288
+ ozy.ps1
289
+ node_modules/
290
+ @scope/
291
+ tool/
292
+ package.json
293
+ bin/
294
+ ozy
295
+ ```
296
+
297
+ Fixture verification still needed: exact `<npm-prefix>` for current npm on
298
+ Windows with `npm prefix -g`, and shell precedence when `ozy.exe` is later added
299
+ beside these shims.
300
+
301
+ ### pnpm, local dependency install on Windows
302
+
303
+ Observed from source:
304
+
305
+ - `@pnpm/link-bins` reads dependency manifests and links bins into the provided
306
+ bins directory.
307
+ - On Windows, before writing shims it removes an existing `ozy.exe` from the bin
308
+ directory.
309
+ - It calls `@zkochan/cmd-shim`.
310
+ - For normal packages, `makePowerShellShim` is true on Windows unless the
311
+ package being linked is named `pnpm`.
312
+
313
+ Expected directory structure:
314
+
315
+ ```text
316
+ project/
317
+ node_modules/
318
+ .bin/
319
+ ozy
320
+ ozy.cmd
321
+ ozy.ps1
322
+ @scope/
323
+ tool -> .pnpm/@scope+tool@1.2.3/node_modules/@scope/tool
324
+ .pnpm/
325
+ @scope+tool@1.2.3/
326
+ node_modules/
327
+ @scope/
328
+ tool/
329
+ package.json
330
+ bin/
331
+ ozy
332
+ ```
333
+
334
+ pnpm-specific warning: because `@pnpm/link-bins` removes `ozy.exe` when linking
335
+ on Windows, a self-repair strategy that creates `node_modules/.bin/ozy.exe`
336
+ could be undone by a later `pnpm install`.
337
+
338
+ ### pnpm, global install on Windows
339
+
340
+ Expected shape, but must be verified with a fixture:
341
+
342
+ ```text
343
+ <PNPM_HOME>/
344
+ ozy
345
+ ozy.cmd
346
+ ozy.ps1
347
+ <pnpm-global-store-or-virtual-store>/
348
+ ...
349
+ ```
350
+
351
+ The source inspected here proves the shim writer and local bin behavior. It does
352
+ not by itself prove the exact current global directory layout for every pnpm
353
+ configuration.
354
+
355
+ ### Bun, local dependency install on Windows
356
+
357
+ Observed from source:
358
+
359
+ - Bun uses a POSIX symlink path on non-Windows and `create_windows_shim` on
360
+ Windows.
361
+ - Windows `create_windows_shim` writes adjacent `ozy.bunx` metadata and
362
+ `ozy.exe`.
363
+ - The `.bunx` metadata stores the relative target and launcher information.
364
+ - The PE `ozy.exe` reads its adjacent `.bunx` metadata.
365
+
366
+ Expected directory structure:
367
+
368
+ ```text
369
+ project/
370
+ node_modules/
371
+ .bin/
372
+ ozy.bunx
373
+ ozy.exe
374
+ @scope/
375
+ tool/
376
+ package.json
377
+ bin/
378
+ ozy
379
+ ```
380
+
381
+ Bun does not use npm-style `ozy.cmd` and `ozy.ps1` shims for package bins in the
382
+ source inspected here.
383
+
384
+ ### Bun, global install on Windows
385
+
386
+ Expected shape, but must be verified with a fixture:
387
+
388
+ ```text
389
+ <bun-global-bin>/
390
+ ozy.bunx
391
+ ozy.exe
392
+ <bun-global-package-location>/
393
+ @scope/
394
+ tool/
395
+ package.json
396
+ bin/
397
+ ozy
398
+ ```
399
+
400
+ The source proves the pair of files Bun writes for a Windows bin destination.
401
+ The exact global install root should be recorded from a real `bun add -g`
402
+ fixture.
403
+
404
+ ### Yarn Classic, local dependency install on Windows
405
+
406
+ Observed from source:
407
+
408
+ - `src/package-linker.js` calls `@zkochan/cmd-shim` on Windows.
409
+ - It passes `createPwshFile: false`.
410
+ - It links into `targetBinLoc/.bin`.
411
+
412
+ Expected directory structure:
413
+
414
+ ```text
415
+ project/
416
+ node_modules/
417
+ .bin/
418
+ ozy
419
+ ozy.cmd
420
+ @scope/
421
+ tool/
422
+ package.json
423
+ bin/
424
+ ozy
425
+ ```
426
+
427
+ No `ozy.ps1` is expected from Yarn Classic because it disables PowerShell shim
428
+ creation.
429
+
430
+ ### Yarn Classic, global install on Windows
431
+
432
+ Expected shape, but must be verified with a fixture:
433
+
434
+ ```text
435
+ <yarn-classic-global-bin>/
436
+ ozy
437
+ ozy.cmd
438
+ <yarn-classic-global-package-location>/
439
+ ...
440
+ ```
441
+
442
+ The source proves shim type and local `.bin` behavior. The exact global path
443
+ depends on Yarn Classic configuration and needs Windows fixture capture.
444
+
445
+ ### Yarn Berry with `nodeLinker: node-modules` on Windows
446
+
447
+ Observed from source:
448
+
449
+ - The node-modules linker reads manifests and builds a `binSymlinks` map.
450
+ - On Windows it calls `@zkochan/cmd-shim` with `createPwshFile: false`.
451
+ - It removes outdated `name` and `name.cmd`; it does not remove `name.ps1` in
452
+ the inspected code path because it does not create one.
453
+
454
+ Expected directory structure:
455
+
456
+ ```text
457
+ project/
458
+ node_modules/
459
+ .bin/
460
+ ozy
461
+ ozy.cmd
462
+ @scope/
463
+ tool/
464
+ package.json
465
+ bin/
466
+ ozy
467
+ ```
468
+
469
+ No persistent `ozy.ps1` is expected from this path.
470
+
471
+ ### Yarn Berry with default PnP on Windows
472
+
473
+ Observed from source:
474
+
475
+ - PnP does not create the normal persistent `node_modules/.bin` tree.
476
+ - `scriptUtils.makePathWrapper` creates temporary wrappers in a Yarn-managed
477
+ `binFolder` for script execution.
478
+ - On Windows, `makePathWrapper` creates `name.cmd`.
479
+ - It also creates a no-extension POSIX shell wrapper.
480
+ - It does not create `name.ps1` in the inspected code.
481
+ - `installBinaries` decides whether to invoke a target through Node or directly
482
+ by calling `isNodeScript`.
483
+
484
+ Expected script-time shape:
485
+
486
+ ```text
487
+ <temporary-yarn-bin-folder>/
488
+ ozy
489
+ ozy.cmd
490
+ ```
491
+
492
+ There may be no stable file path for a self-repairing command to mutate when
493
+ the command is only available through `yarn run` in PnP mode. That is a major
494
+ constraint.
495
+
496
+ ### Yarn Berry `dlx` on Windows
497
+
498
+ Observed from source:
499
+
500
+ - `plugin-dlx` creates a temporary project directory.
501
+ - It writes a temporary `package.json`, `yarn.lock`, and `.yarnrc.yml`.
502
+ - It runs `yarn add --fixed -- ...` in that temporary project.
503
+ - It loads the temporary project install state.
504
+ - It executes the command through `scriptUtils.executeWorkspaceAccessibleBinary`.
505
+
506
+ Expected shape:
507
+
508
+ ```text
509
+ <temporary-base>/
510
+ dlx-<pid>/
511
+ package.json
512
+ yarn.lock
513
+ .yarnrc.yml
514
+ .pnp.cjs or node_modules/ # depends on effective Yarn config
515
+ ```
516
+
517
+ Then command execution follows the same `scriptUtils` temporary wrapper behavior
518
+ described in the PnP section. `dlx` is not a durable global command install
519
+ surface; it is a temporary execution surface.
520
+
521
+ ## Literal shim contents
522
+
523
+ These examples use this package layout:
524
+
525
+ ```text
526
+ project/
527
+ node_modules/
528
+ .bin/
529
+ ozy
530
+ ozy.cmd
531
+ ozy.ps1
532
+ @scope/
533
+ tool/
534
+ bin/
535
+ ozy
536
+ ```
537
+
538
+ and this bin target:
539
+
540
+ ```javascript
541
+ #!/usr/bin/env node
542
+ console.log(process.argv)
543
+ ```
544
+
545
+ The exact absolute paths vary by install location. The examples below keep the
546
+ same relative relationship the package managers generate.
547
+
548
+ Markdown renders the `.cmd` examples with normal line breaks. The generated
549
+ `.cmd` files are Windows command scripts and should be treated as CRLF-oriented
550
+ text files when comparing raw bytes.
551
+
552
+ ### npm `cmd-shim`: `ozy`
553
+
554
+ npm's no-extension shim is a POSIX shell script for Git Bash/MSYS/Cygwin-style
555
+ execution.
556
+
557
+ ```sh
558
+ #!/bin/sh
559
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
560
+ basedir_win="$basedir"
561
+
562
+ case `uname -a` in
563
+ *CYGWIN*|*MINGW*|*MSYS*)
564
+ if command -v cygpath > /dev/null 2>&1; then
565
+ basedir_win=`cygpath -w "$basedir"`
566
+ fi
567
+ ;;
568
+ *WSL2*)
569
+ if command -v wslpath > /dev/null 2>&1; then
570
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
571
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
572
+ echo "Error: wslpath failed to convert path. WSL environment may be misconfigured." >&2
573
+ exit 1
574
+ fi
575
+ fi
576
+ ;;
577
+ esac
578
+
579
+ PROG_EXE="$basedir/node.exe"
580
+ if ! [ -x "$PROG_EXE" ]; then
581
+ PROG_EXE="$basedir/node"
582
+ if ! [ -x "$PROG_EXE" ]; then
583
+ PROG_EXE=node
584
+ if ! [ -x "$PROG_EXE" ]; then
585
+ PROG_EXE=node.exe
586
+ fi
587
+ fi
588
+ fi
589
+
590
+ exec "$PROG_EXE" "$basedir_win/../@scope/tool/bin/ozy" "$@"
591
+ ```
592
+
593
+ ### npm `cmd-shim`: `ozy.cmd`
594
+
595
+ ```bat
596
+ @ECHO off
597
+ GOTO start
598
+ :find_dp0
599
+ SET dp0=%~dp0
600
+ EXIT /b
601
+ :start
602
+ SETLOCAL
603
+ CALL :find_dp0
604
+
605
+ IF EXIST "%dp0%\node.exe" (
606
+ SET "_prog=%dp0%\node.exe"
607
+ ) ELSE (
608
+ SET "_prog=node"
609
+ )
610
+
611
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & set PATHEXT=%PATHEXT:;.JS;=;% & "%_prog%" "%dp0%\..\@scope\tool\bin\ozy" %*
612
+ ```
613
+
614
+ ### npm `cmd-shim`: `ozy.ps1`
615
+
616
+ ```powershell
617
+ #!/usr/bin/env pwsh
618
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
619
+
620
+ $exe=""
621
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
622
+ # Fix case when both the Windows and Linux builds of Node
623
+ # are installed in the same directory
624
+ $exe=".exe"
625
+ }
626
+ $ret=0
627
+ if (Test-Path "$basedir/node$exe") {
628
+ # Support pipeline input
629
+ if ($MyInvocation.ExpectingInput) {
630
+ $input | & "$basedir/node$exe" "$basedir/../@scope/tool/bin/ozy" $args
631
+ } else {
632
+ & "$basedir/node$exe" "$basedir/../@scope/tool/bin/ozy" $args
633
+ }
634
+ $ret=$LASTEXITCODE
635
+ } else {
636
+ # Support pipeline input
637
+ if ($MyInvocation.ExpectingInput) {
638
+ $input | & "node$exe" "$basedir/../@scope/tool/bin/ozy" $args
639
+ } else {
640
+ & "node$exe" "$basedir/../@scope/tool/bin/ozy" $args
641
+ }
642
+ $ret=$LASTEXITCODE
643
+ }
644
+ exit $ret
645
+ ```
646
+
647
+ ### pnpm `@zkochan/cmd-shim`: `ozy`
648
+
649
+ pnpm uses `@zkochan/cmd-shim`. Yarn Classic and Yarn Berry node-modules use the
650
+ same shim generator, but pass `createPwshFile: false`.
651
+
652
+ ```sh
653
+ #!/bin/sh
654
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
655
+ basedir_win="$basedir"
656
+ exe=""
657
+ msys=""
658
+
659
+ case `uname -a` in
660
+ *CYGWIN*|*MINGW*|*MSYS*)
661
+ if command -v cygpath > /dev/null 2>&1; then
662
+ basedir_win=`cygpath -w "$basedir"`
663
+ fi
664
+ exe=".exe"
665
+ msys="true"
666
+ ;;
667
+ *WSL2*)
668
+ if command -v wslpath > /dev/null 2>&1; then
669
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
670
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
671
+ basedir_win="$basedir"
672
+ else
673
+ exe=".exe"
674
+ fi
675
+ fi
676
+ ;;
677
+ esac
678
+
679
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
680
+ exec "$basedir/node.exe" "$basedir_win/../@scope/tool/bin/ozy" "$@"
681
+ elif [ -x "$basedir/node" ]; then
682
+ exec "$basedir/node" "$basedir/../@scope/tool/bin/ozy" "$@"
683
+ elif command -v node >/dev/null 2>&1; then
684
+ exec node "$basedir/../@scope/tool/bin/ozy" "$@"
685
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
686
+ exec node.exe "$basedir_win/../@scope/tool/bin/ozy" "$@"
687
+ else
688
+ exec node "$basedir/../@scope/tool/bin/ozy" "$@"
689
+ fi
690
+ # cmd-shim-target=<absolute path to project/node_modules/@scope/tool/bin/ozy>
691
+ ```
692
+
693
+ ### pnpm `@zkochan/cmd-shim`: `ozy.cmd`
694
+
695
+ ```bat
696
+ @SETLOCAL
697
+ @IF EXIST "%~dp0\node.exe" (
698
+ "%~dp0\node.exe" "%~dp0\..\@scope\tool\bin\ozy" %*
699
+ ) ELSE (
700
+ @SET PATHEXT=%PATHEXT:;.JS;=;%
701
+ node "%~dp0\..\@scope\tool\bin\ozy" %*
702
+ )
703
+ ```
704
+
705
+ ### pnpm `@zkochan/cmd-shim`: `ozy.ps1`
706
+
707
+ ```powershell
708
+ #!/usr/bin/env pwsh
709
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
710
+
711
+ $exe=""
712
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
713
+ # Fix case when both the Windows and Linux builds of Node
714
+ # are installed in the same directory
715
+ $exe=".exe"
716
+ }
717
+ $ret=0
718
+ if (Test-Path "$basedir/node$exe") {
719
+ # Support pipeline input
720
+ if ($MyInvocation.ExpectingInput) {
721
+ $input | & "$basedir/node$exe" "$basedir/../@scope/tool/bin/ozy" $args
722
+ } else {
723
+ & "$basedir/node$exe" "$basedir/../@scope/tool/bin/ozy" $args
724
+ }
725
+ $ret=$LASTEXITCODE
726
+ } else {
727
+ # Support pipeline input
728
+ if ($MyInvocation.ExpectingInput) {
729
+ $input | & "node$exe" "$basedir/../@scope/tool/bin/ozy" $args
730
+ } else {
731
+ & "node$exe" "$basedir/../@scope/tool/bin/ozy" $args
732
+ }
733
+ $ret=$LASTEXITCODE
734
+ }
735
+ exit $ret
736
+ ```
737
+
738
+ ### Yarn Classic and Yarn Berry node-modules
739
+
740
+ Yarn Classic and Yarn Berry node-modules emit the `@zkochan/cmd-shim` `ozy` and
741
+ `ozy.cmd` contents shown above. They do not emit `ozy.ps1` in the inspected
742
+ source paths because both pass `createPwshFile: false`.
743
+
744
+ ### Yarn Berry PnP/script wrappers: `ozy`
745
+
746
+ Yarn Berry PnP does not create persistent `node_modules/.bin` shims. For
747
+ script-time execution, `scriptUtils.makePathWrapper` creates a temporary
748
+ no-extension shell wrapper:
749
+
750
+ ```sh
751
+ #!/bin/sh
752
+ exec "<argv0>" '<arg0>' '<arg1>' "$@"
753
+ ```
754
+
755
+ For a JS package binary, Yarn passes Node as `argv0` and the package binary path
756
+ as an argument, so the shape is:
757
+
758
+ ```sh
759
+ #!/bin/sh
760
+ exec "<path-to-node>" '<path-to-package-bin-ozy>' "$@"
761
+ ```
762
+
763
+ For a native binary, Yarn passes the binary path as `argv0` with no package-bin
764
+ argument:
765
+
766
+ ```sh
767
+ #!/bin/sh
768
+ exec "<path-to-native-binary>" "$@"
769
+ ```
770
+
771
+ ### Yarn Berry PnP/script wrappers: `ozy.cmd`
772
+
773
+ Yarn Berry's temporary CMD wrapper is one line:
774
+
775
+ ```bat
776
+ @goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @"<argv0>" "<arg0>" "<arg1>" %*
777
+ ```
778
+
779
+ For a JS package binary:
780
+
781
+ ```bat
782
+ @goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @"<path-to-node>" "<path-to-package-bin-ozy>" %*
783
+ ```
784
+
785
+ For a native binary:
786
+
787
+ ```bat
788
+ @goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @"<path-to-native-binary>" %*
789
+ ```
790
+
791
+ Yarn Berry's `makePathWrapper` does not create a `.ps1` file.
792
+
793
+ ### Bun package manager: `ozy.exe`
794
+
795
+ Bun's Windows command shim is not a text shim. It writes:
796
+
797
+ ```text
798
+ ozy.exe # copied PE executable bytes from bun_shim_impl.exe
799
+ ozy.bunx # target-specific binary metadata read by ozy.exe
800
+ ```
801
+
802
+ `ozy.exe` is target-independent PE content embedded in Bun. Its file starts with
803
+ the normal DOS/PE magic bytes:
804
+
805
+ ```text
806
+ 4d 5a ... # "MZ"
807
+ ```
808
+
809
+ The target-specific content lives in `ozy.bunx`.
810
+
811
+ ### Bun package manager: `ozy.bunx`
812
+
813
+ The `.bunx` file is binary metadata, not text. Bun's source documents and
814
+ encodes it as:
815
+
816
+ ```text
817
+ [UTF-16LE bin_path][u16 quote][u16 zero](shebang?)[flags:u16]
818
+
819
+ if shebang:
820
+ [UTF-16LE launcher][u16 space][u32 bin_path_byte_len][u32 launcher_plus_space_byte_len]
821
+ ```
822
+
823
+ The flags are a little-endian `u16` bitfield:
824
+
825
+ ```text
826
+ bit 0: launcher is node or bun
827
+ bit 1: launcher is node
828
+ bit 2: has shebang
829
+ bits 3..15: version tag, currently 5478
830
+ ```
831
+
832
+ For `node_modules/.bin/ozy` pointing at `node_modules/@scope/tool/bin/ozy`, Bun
833
+ stores the target path relative to `node_modules`, after stripping the leading
834
+ `..\` from the `.bin` relative path:
835
+
836
+ ```text
837
+ @scope\tool\bin\ozy
838
+ ```
839
+
840
+ For a target whose first line is `#!/usr/bin/env node`, the concrete `.bunx`
841
+ bytes are:
842
+
843
+ ```text
844
+ 00000000 40 00 73 00 63 00 6f 00 70 00 65 00 5c 00 74 00 @.s.c.o.p.e.\.t.
845
+ 00000010 6f 00 6f 00 6c 00 5c 00 62 00 69 00 6e 00 5c 00 o.o.l.\.b.i.n.\.
846
+ 00000020 6f 00 7a 00 79 00 22 00 00 00 6e 00 6f 00 64 00 o.z.y."...n.o.d.
847
+ 00000030 65 00 20 00 26 00 00 00 0a 00 00 00 37 ab e. .&.......7.
848
+ ```
849
+
850
+ Decoded:
851
+
852
+ ```text
853
+ bin_path: @scope\tool\bin\ozy
854
+ quote terminator: u16 0x0022
855
+ nul terminator: u16 0x0000
856
+ launcher: node
857
+ space: u16 0x0020
858
+ bin_path_byte_len: 0x00000026
859
+ launcher_plus_space_byte_len: 0x0000000a
860
+ flags: 0xab37
861
+ ```
862
+
863
+ `0xab37` means:
864
+
865
+ ```text
866
+ version tag: 5478
867
+ has shebang: true
868
+ launcher is node: true
869
+ launcher is node or bun: true
870
+ ```
871
+
872
+ ## Target invocation rules
873
+
874
+ ### npm `cmd-shim`
875
+
876
+ For a Windows bin target, npm's `cmd-shim`:
877
+
878
+ - Reads the target file.
879
+ - Parses a shebang from the first line when one exists.
880
+ - If a shebang exists, the generated shim runs that program and passes the
881
+ target path as an argument.
882
+ - If no shebang exists, the generated shim calls the target path directly.
883
+ - Writes `name`, `name.cmd`, and `name.ps1`.
884
+
885
+ Implication: if `./bin/ozy` starts with `#!/usr/bin/env node`, npm's `.cmd` and
886
+ `.ps1` shims invoke Node and run `bin/ozy`; they do not execute `bin/ozy` as a
887
+ PowerShell file just because a `.ps1` shim exists.
888
+
889
+ ### pnpm and Yarn `@zkochan/cmd-shim`
890
+
891
+ For a Windows bin target, `@zkochan/cmd-shim`:
892
+
893
+ - Creates a no-extension shell shim.
894
+ - Creates `name.cmd` when `createCmdFile` is enabled.
895
+ - Creates `name.ps1` only when `createPwshFile` is enabled.
896
+ - Uses shebang parsing and extension fallback.
897
+ - Extension fallback maps `.js`, `.cjs`, `.mjs` to Node, `.cmd` and `.bat` to
898
+ CMD, `.ps1` to PowerShell, and `.sh` to sh in the inspected package version.
899
+
900
+ Implications:
901
+
902
+ - pnpm normally creates `name`, `name.cmd`, and `name.ps1`.
903
+ - Yarn Classic creates `name` and `name.cmd`.
904
+ - Yarn Berry node-modules creates `name` and `name.cmd`.
905
+
906
+ ### Bun Windows PE shim
907
+
908
+ For a Windows bin target, Bun:
909
+
910
+ - Writes `name.exe`.
911
+ - Writes `name.bunx`.
912
+ - Encodes the target path and launcher into `.bunx`.
913
+ - Uses a shebang when it can parse one.
914
+ - If there is no shebang, extension fallback maps JS/TS-like extensions and
915
+ `.sh` to `bun run`, `.cmd`/`.bat` to `cmd /c`, and `.ps1` to
916
+ `powershell -ExecutionPolicy Bypass -File`.
917
+ - If the target has no recognized extension and no shebang, the PE shim runs the
918
+ target path directly.
919
+
920
+ ## What happens if the target itself is `.cmd`, `.bat`, `.ps1`, or `.exe`
921
+
922
+ This section is about `package.json#bin` pointing at different target files:
923
+
924
+ ```json
925
+ {
926
+ "bin": {
927
+ "ozy": "./bin/ozy.cmd"
928
+ }
929
+ }
930
+ ```
931
+
932
+ or:
933
+
934
+ ```json
935
+ {
936
+ "bin": {
937
+ "ozy": "./bin/ozy.exe"
938
+ }
939
+ }
940
+ ```
941
+
942
+ Package manager behavior is not "link target as-is" on Windows, except for Bun's
943
+ PE shim strategy. npm, pnpm, and Yarn generally create manager-owned shims that
944
+ then invoke the target.
945
+
946
+ Expected effects from source:
947
+
948
+ | Target path | npm `cmd-shim` | pnpm/Yarn `@zkochan/cmd-shim` | Bun |
949
+ | --- | --- | --- | --- |
950
+ | `./bin/ozy` with shebang | Generated shim invokes shebang program | Generated shim invokes shebang program | `.exe` shim uses parsed shebang |
951
+ | `./bin/ozy` without shebang | Generated shim invokes target path directly | Generated shim likely invokes target path directly | `.exe` shim invokes target path directly |
952
+ | `./bin/ozy.js` | Shebang if present, otherwise direct target path | Node by extension fallback if no shebang | `bun run` by extension fallback if no shebang |
953
+ | `./bin/ozy.cmd` | Shebang if present, otherwise direct target path | `cmd` by extension fallback if no shebang | `cmd /c` by extension fallback if no shebang |
954
+ | `./bin/ozy.bat` | Shebang if present, otherwise direct target path | `cmd` by extension fallback if no shebang | `cmd /c` by extension fallback if no shebang |
955
+ | `./bin/ozy.ps1` | Shebang if present, otherwise direct target path | PowerShell by extension fallback if no shebang | PowerShell by extension fallback if no shebang |
956
+ | `./bin/ozy.exe` | Shebang read fails or no shebang, then direct target path | Direct target path when treated as native executable | Direct target path via PE shim metadata |
957
+
958
+ The `.ps1` case is easy to misunderstand. This file is valid PowerShell:
959
+
960
+ ```powershell
961
+ #!/usr/bin/env node
962
+ node install.js
963
+ ```
964
+
965
+ because `#` begins a comment in PowerShell. But if a package manager reads that
966
+ file as a bin target and honors the shebang, the generated shim may run it with
967
+ Node instead of PowerShell. That means a polyglot `.ps1` target does not
968
+ guarantee PowerShell execution.
969
+
970
+ Replacing a `.ps1` file with PE executable bytes is not a safe second-run
971
+ strategy. PowerShell treats `.ps1` as a script path. A PE file with a `.ps1`
972
+ extension will be parsed as script text and fail.
973
+
974
+ ## Same-name `.exe` beside text shims
975
+
976
+ One possible Windows repair strategy is:
977
+
978
+ ```text
979
+ node_modules/.bin/
980
+ ozy
981
+ ozy.cmd
982
+ ozy.ps1
983
+ ozy.exe # added by self-repair
984
+ ```
985
+
986
+ This might make `ozy` resolve to `ozy.exe` in `cmd.exe` because common `PATHEXT`
987
+ orders `.EXE` before `.BAT` and `.CMD`. It is not yet proven for:
988
+
989
+ - PowerShell command resolution.
990
+ - Git Bash/MSYS.
991
+ - npm global installs.
992
+ - pnpm local/global installs, especially because pnpm removes existing
993
+ `name.exe` before relinking bins.
994
+ - Yarn Classic and Yarn Berry without a `.ps1` shim.
995
+ - Bun, which already creates `name.exe`.
996
+ - uninstall cleanup.
997
+
998
+ Do not rely on this strategy without the fixture matrix below.
999
+
1000
+ ## Why shim discovery is hard
1001
+
1002
+ The installer JS can inspect:
1003
+
1004
+ - `process.argv[0]`
1005
+ - `process.argv[1]`
1006
+ - `process.execPath`
1007
+ - `process.env.npm_execpath`
1008
+ - `process.env.npm_config_user_agent`
1009
+ - parent process information, if available
1010
+
1011
+ Those values do not uniformly identify the package-manager-created shim file.
1012
+ Depending on manager and shell, the current process may be:
1013
+
1014
+ - `node.exe` running a target script selected by a text shim.
1015
+ - `powershell.exe` running a `.ps1` shim.
1016
+ - `cmd.exe` running a `.cmd` shim.
1017
+ - Bun's PE shim running through `.bunx` metadata.
1018
+ - Yarn Berry running through a temporary wrapper directory.
1019
+
1020
+ Caller-tree inspection can help debugging, but it is not a stable installer API.
1021
+ It also does not tell us what files the package manager considers owned for
1022
+ uninstall. For a repair strategy, file ownership matters as much as locating the
1023
+ file.
1024
+
1025
+ ## Current conclusions
1026
+
1027
+ 1. The base package needs to own the command `bin` entries. Platform-package
1028
+ `bin` entries alone are not enough for a generic local/global install story.
1029
+ 2. POSIX can be clean: a first-run JS launcher replaces the package-owned
1030
+ command file with a hardlink or copy of the platform `multi-call-binary`,
1031
+ then executes that command path.
1032
+ 3. Windows is not one case:
1033
+ - npm writes `name`, `name.cmd`, and `name.ps1`.
1034
+ - pnpm normally writes `name`, `name.cmd`, and `name.ps1`, and removes a
1035
+ pre-existing `name.exe` while linking.
1036
+ - Bun writes `name.exe` and `name.bunx`.
1037
+ - Yarn Classic writes `name` and `name.cmd`.
1038
+ - Yarn Berry node-modules writes `name` and `name.cmd`.
1039
+ - Yarn Berry PnP uses temporary script wrappers rather than persistent
1040
+ `node_modules/.bin` files.
1041
+ 4. Replacing `.cmd` or `.ps1` with PE executable bytes is not a general
1042
+ solution. `.cmd` and `.ps1` are interpreted as text by their shells.
1043
+ 5. Windows keeps the package-manager shim and JS launcher, but the launcher
1044
+ creates and runs a command-named `.exe` next to itself. This avoids locating
1045
+ or mutating package-manager-owned shims while still giving the multi-call
1046
+ executable a command-specific invocation name.
1047
+
1048
+ ## Required Windows fixture matrix
1049
+
1050
+ Create a small package with one command and repeat with these target variants:
1051
+
1052
+ ```text
1053
+ bin/ozy # JS with #!/usr/bin/env node
1054
+ bin/ozy-no-shebang # JS without shebang
1055
+ bin/ozy.cmd
1056
+ bin/ozy.bat
1057
+ bin/ozy.ps1
1058
+ bin/ozy.exe
1059
+ ```
1060
+
1061
+ For each package manager:
1062
+
1063
+ - npm
1064
+ - pnpm
1065
+ - bun
1066
+ - Yarn Classic
1067
+ - Yarn Berry with PnP
1068
+ - Yarn Berry with `nodeLinker: node-modules`
1069
+
1070
+ Test install modes:
1071
+
1072
+ - local dependency install
1073
+ - global install
1074
+ - Yarn Berry `dlx` temporary install
1075
+
1076
+ Test shells:
1077
+
1078
+ - `cmd.exe`
1079
+ - PowerShell 5 if available
1080
+ - PowerShell 7
1081
+ - Git Bash/MSYS if installed
1082
+
1083
+ Record:
1084
+
1085
+ ```text
1086
+ manager:
1087
+ install mode:
1088
+ shell:
1089
+ target variant:
1090
+ created files:
1091
+ Get-Command ozy -All:
1092
+ where.exe ozy:
1093
+ command executed:
1094
+ process.argv:
1095
+ process.execPath:
1096
+ process.cwd:
1097
+ npm_execpath:
1098
+ npm_config_user_agent:
1099
+ after adding ozy.exe beside shims, command executed:
1100
+ after uninstall, leftover files:
1101
+ ```
1102
+
1103
+ Do not implement the Windows shim mutation logic until this matrix is filled in
1104
+ for the package managers we intend to support.