@hover-dev/cli 0.3.0
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/LICENSE +201 -0
- package/README.md +61 -0
- package/dist/detect.d.ts +41 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +68 -0
- package/dist/frameworks.d.ts +44 -0
- package/dist/frameworks.d.ts.map +1 -0
- package/dist/frameworks.js +47 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/install.d.ts +12 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +36 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +26 -0
- package/dist/mutate.d.ts +36 -0
- package/dist/mutate.d.ts.map +1 -0
- package/dist/mutate.js +184 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Hyperyond Studio
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @hover-dev/cli
|
|
2
|
+
|
|
3
|
+
One-command setup for [Hover](https://github.com/Hyperyond/Hover) — detects your bundler, installs the right integration package, and wires it into your config file.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
No installation required. `npx` runs the latest published version on demand:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @hover-dev/cli add
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That's it. The CLI:
|
|
14
|
+
|
|
15
|
+
1. **Reads your `package.json`** to figure out your bundler (Vite, Astro, Nuxt, or Webpack).
|
|
16
|
+
2. **Reads your lockfile** to pick the right package manager (pnpm, yarn, bun, or npm).
|
|
17
|
+
3. **Installs** the matching Hover package as a dev dependency.
|
|
18
|
+
4. **Updates** your config file (`vite.config.ts` / `astro.config.mjs` / `nuxt.config.ts` / `webpack.config.js`) to load the plugin.
|
|
19
|
+
|
|
20
|
+
Then run your dev server and click the floating ✨ in the bottom-right corner.
|
|
21
|
+
|
|
22
|
+
## Force a specific bundler
|
|
23
|
+
|
|
24
|
+
If detection picks the wrong one (e.g. your project has multiple bundlers, or you're starting from a fresh repo), use a flag:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @hover-dev/cli add --vite # vite-plugin-hover
|
|
28
|
+
npx @hover-dev/cli add --astro # @hover-dev/astro
|
|
29
|
+
npx @hover-dev/cli add --nuxt # @hover-dev/nuxt
|
|
30
|
+
npx @hover-dev/cli add --webpack # webpack-plugin-hover
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Preview without modifying anything
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @hover-dev/cli add --dry-run
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Prints what would be installed + which config file would be modified, then exits without changing anything.
|
|
40
|
+
|
|
41
|
+
## What if my config file is unusual?
|
|
42
|
+
|
|
43
|
+
The CLI uses [magicast](https://github.com/unjs/magicast) to safely mutate `defineConfig({ ... })` and bare-object configs. If your config has an unusual shape (re-exported config, conditional logic, etc.), the CLI will:
|
|
44
|
+
|
|
45
|
+
1. Still install the right Hover package.
|
|
46
|
+
2. Skip the config mutation and print the exact lines you need to paste in.
|
|
47
|
+
|
|
48
|
+
This is also what happens if you have no config file at all — many projects rely on bundler defaults until they need to customise.
|
|
49
|
+
|
|
50
|
+
## Use as a project dep (optional)
|
|
51
|
+
|
|
52
|
+
`npx` is the default and recommended way. If you want to lock the CLI version per-project (e.g. to make a setup command for new teammates), install it as a dev dep:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm add -D @hover-dev/cli
|
|
56
|
+
pnpm hover add
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
Apache-2.0 — same as the rest of Hover.
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Framework } from './frameworks.js';
|
|
2
|
+
export interface PackageJson {
|
|
3
|
+
dependencies?: Record<string, string>;
|
|
4
|
+
devDependencies?: Record<string, string>;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Locate and parse the user's package.json. Returns the parsed contents +
|
|
9
|
+
* the absolute directory it lives in (so callers know the project root
|
|
10
|
+
* for spawning the package manager + resolving config-file paths).
|
|
11
|
+
*
|
|
12
|
+
* Walks up from `startDir` looking for `package.json` — this lets the user
|
|
13
|
+
* run `npx @hover-dev/cli add` from a subdirectory and still target the
|
|
14
|
+
* project root. Stops at the filesystem root.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readUserPackageJson(startDir?: string): {
|
|
17
|
+
pkg: PackageJson;
|
|
18
|
+
rootDir: string;
|
|
19
|
+
} | null;
|
|
20
|
+
/**
|
|
21
|
+
* Detect which framework the user's package.json signals. Returns the
|
|
22
|
+
* highest-priority framework whose `detectDeps` overlap with the user's
|
|
23
|
+
* combined dep tree. Returns null if none match (likely a fresh project
|
|
24
|
+
* or non-JS repo).
|
|
25
|
+
*/
|
|
26
|
+
export declare function detectFramework(pkg: PackageJson): Framework | null;
|
|
27
|
+
/**
|
|
28
|
+
* Detect which package manager the user is on by sniffing the lockfile.
|
|
29
|
+
* Falls back to npm if nothing matches (every Node install has npm), but
|
|
30
|
+
* prints a warning so the user knows we're guessing.
|
|
31
|
+
*
|
|
32
|
+
* Priority is lockfile-first because lockfile commitment is the strongest
|
|
33
|
+
* signal — `packageManager` field in package.json is a softer hint that
|
|
34
|
+
* users often forget to update.
|
|
35
|
+
*/
|
|
36
|
+
export type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';
|
|
37
|
+
export declare function detectPackageManager(rootDir: string): {
|
|
38
|
+
pm: PackageManager;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,MAAsB,GAAG;IAAE,GAAG,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAgBlH;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAQlE;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAE7D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoB5F"}
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { FRAMEWORKS } from './frameworks.js';
|
|
4
|
+
/**
|
|
5
|
+
* Locate and parse the user's package.json. Returns the parsed contents +
|
|
6
|
+
* the absolute directory it lives in (so callers know the project root
|
|
7
|
+
* for spawning the package manager + resolving config-file paths).
|
|
8
|
+
*
|
|
9
|
+
* Walks up from `startDir` looking for `package.json` — this lets the user
|
|
10
|
+
* run `npx @hover-dev/cli add` from a subdirectory and still target the
|
|
11
|
+
* project root. Stops at the filesystem root.
|
|
12
|
+
*/
|
|
13
|
+
export function readUserPackageJson(startDir = process.cwd()) {
|
|
14
|
+
let dir = resolve(startDir);
|
|
15
|
+
while (true) {
|
|
16
|
+
const candidate = join(dir, 'package.json');
|
|
17
|
+
if (existsSync(candidate)) {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
20
|
+
return { pkg, rootDir: dir };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const parent = resolve(dir, '..');
|
|
27
|
+
if (parent === dir)
|
|
28
|
+
return null;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detect which framework the user's package.json signals. Returns the
|
|
34
|
+
* highest-priority framework whose `detectDeps` overlap with the user's
|
|
35
|
+
* combined dep tree. Returns null if none match (likely a fresh project
|
|
36
|
+
* or non-JS repo).
|
|
37
|
+
*/
|
|
38
|
+
export function detectFramework(pkg) {
|
|
39
|
+
const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
40
|
+
for (const framework of FRAMEWORKS) {
|
|
41
|
+
for (const dep of framework.detectDeps) {
|
|
42
|
+
if (dep in allDeps)
|
|
43
|
+
return framework;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
export function detectPackageManager(rootDir) {
|
|
49
|
+
const lockfileMap = [
|
|
50
|
+
{ file: 'pnpm-lock.yaml', pm: 'pnpm' },
|
|
51
|
+
{ file: 'yarn.lock', pm: 'yarn' },
|
|
52
|
+
{ file: 'bun.lockb', pm: 'bun' },
|
|
53
|
+
{ file: 'bun.lock', pm: 'bun' },
|
|
54
|
+
{ file: 'package-lock.json', pm: 'npm' },
|
|
55
|
+
];
|
|
56
|
+
for (const { file, pm } of lockfileMap) {
|
|
57
|
+
if (existsSync(join(rootDir, file))) {
|
|
58
|
+
return { pm, reason: `lockfile ${file}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Soft hint: explicit `packageManager` field.
|
|
62
|
+
const pkgRaw = readFileSync(join(rootDir, 'package.json'), 'utf-8');
|
|
63
|
+
const pkgManagerMatch = /"packageManager"\s*:\s*"(pnpm|yarn|bun|npm)/.exec(pkgRaw);
|
|
64
|
+
if (pkgManagerMatch) {
|
|
65
|
+
return { pm: pkgManagerMatch[1], reason: 'packageManager field' };
|
|
66
|
+
}
|
|
67
|
+
return { pm: 'npm', reason: 'no lockfile found; defaulting to npm' };
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of frameworks `@hover-dev/cli` knows how to wire Hover into.
|
|
3
|
+
*
|
|
4
|
+
* To add a new framework (e.g. SvelteKit, SolidStart, Qwik, Next-with-Turbopack):
|
|
5
|
+
* write a new entry here and a matching mutator in `mutate.ts`. Nothing else
|
|
6
|
+
* in the CLI changes — `detect.ts`, `install.ts`, and the top-level
|
|
7
|
+
* dispatcher all consume this list generically.
|
|
8
|
+
*
|
|
9
|
+
* `detectDeps` is matched against the user's `package.json` `dependencies` +
|
|
10
|
+
* `devDependencies` keys (and a couple of indirect signals). The order in
|
|
11
|
+
* the array is the priority order — a Nuxt project legitimately has `vite`
|
|
12
|
+
* in its dep tree, so Nuxt must check before Vite. `--<framework>` flags
|
|
13
|
+
* override detection entirely.
|
|
14
|
+
*
|
|
15
|
+
* `configCandidates` is the list of filenames the mutator will look for,
|
|
16
|
+
* in priority order. The first one that exists in cwd wins.
|
|
17
|
+
*/
|
|
18
|
+
export type FrameworkId = 'astro' | 'nuxt' | 'webpack' | 'vite';
|
|
19
|
+
export interface Framework {
|
|
20
|
+
/** Short id used as the --<id> CLI flag and the `Detected: <id>` output. */
|
|
21
|
+
id: FrameworkId;
|
|
22
|
+
/** Human-readable name printed in logs. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Hover package the user gets installed for this framework. */
|
|
25
|
+
hoverPackage: string;
|
|
26
|
+
/** package.json dependency keys whose presence signals this framework.
|
|
27
|
+
* Checked in priority order across the registry. */
|
|
28
|
+
detectDeps: string[];
|
|
29
|
+
/** Possible filenames for the framework's config file. First match wins. */
|
|
30
|
+
configCandidates: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detection priority is high → low. A monorepo or framework whose dep tree
|
|
34
|
+
* legitimately contains a lower-priority framework (Nuxt has `vite`, Astro
|
|
35
|
+
* has `vite`) must come first so the right shim wins.
|
|
36
|
+
*
|
|
37
|
+
* The `Webpack` entry below intentionally checks the user-facing webpack
|
|
38
|
+
* tooling (webpack-cli / next) rather than a transitive `webpack` dep —
|
|
39
|
+
* every Vite project has `webpack` somewhere in its tree because of
|
|
40
|
+
* dev-server internals.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FRAMEWORKS: Framework[];
|
|
43
|
+
export declare function findFrameworkById(id: FrameworkId): Framework | undefined;
|
|
44
|
+
//# sourceMappingURL=frameworks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frameworks.d.ts","sourceRoot":"","sources":["../src/frameworks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,EAAE,EAAE,WAAW,CAAC;IAChB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB;yDACqD;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,EAiCjC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAExE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection priority is high → low. A monorepo or framework whose dep tree
|
|
3
|
+
* legitimately contains a lower-priority framework (Nuxt has `vite`, Astro
|
|
4
|
+
* has `vite`) must come first so the right shim wins.
|
|
5
|
+
*
|
|
6
|
+
* The `Webpack` entry below intentionally checks the user-facing webpack
|
|
7
|
+
* tooling (webpack-cli / next) rather than a transitive `webpack` dep —
|
|
8
|
+
* every Vite project has `webpack` somewhere in its tree because of
|
|
9
|
+
* dev-server internals.
|
|
10
|
+
*/
|
|
11
|
+
export const FRAMEWORKS = [
|
|
12
|
+
{
|
|
13
|
+
id: 'astro',
|
|
14
|
+
label: 'Astro',
|
|
15
|
+
hoverPackage: '@hover-dev/astro',
|
|
16
|
+
detectDeps: ['astro'],
|
|
17
|
+
configCandidates: ['astro.config.mjs', 'astro.config.ts', 'astro.config.js'],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'nuxt',
|
|
21
|
+
label: 'Nuxt',
|
|
22
|
+
hoverPackage: '@hover-dev/nuxt',
|
|
23
|
+
detectDeps: ['nuxt'],
|
|
24
|
+
configCandidates: ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'webpack',
|
|
28
|
+
label: 'Webpack',
|
|
29
|
+
hoverPackage: 'webpack-plugin-hover',
|
|
30
|
+
// `webpack-cli` is the user-facing wrapper; `next` ships its own webpack
|
|
31
|
+
// (and our plugin works under `next dev --webpack`). Pure `webpack` as a
|
|
32
|
+
// transitive dep is too noisy to detect on.
|
|
33
|
+
detectDeps: ['webpack-cli', 'next'],
|
|
34
|
+
configCandidates: ['webpack.config.js', 'webpack.config.mjs', 'webpack.config.ts'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'vite',
|
|
38
|
+
label: 'Vite',
|
|
39
|
+
hoverPackage: 'vite-plugin-hover',
|
|
40
|
+
// Catch-all for Vite-based projects that aren't Astro / Nuxt / Svelte.
|
|
41
|
+
detectDeps: ['vite'],
|
|
42
|
+
configCandidates: ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
export function findFrameworkById(id) {
|
|
46
|
+
return FRAMEWORKS.find(f => f.id === id);
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { detectFramework, detectPackageManager, readUserPackageJson } from './detect.js';
|
|
6
|
+
import { findFrameworkById, FRAMEWORKS } from './frameworks.js';
|
|
7
|
+
import { installPackage } from './install.js';
|
|
8
|
+
import { mutateConfig } from './mutate.js';
|
|
9
|
+
import { bold, cyan, dim, err, info, ok, spark, warn } from './log.js';
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const out = {
|
|
12
|
+
command: null,
|
|
13
|
+
framework: null,
|
|
14
|
+
dryRun: false,
|
|
15
|
+
help: false,
|
|
16
|
+
version: false,
|
|
17
|
+
};
|
|
18
|
+
for (const arg of argv) {
|
|
19
|
+
if (arg === '--help' || arg === '-h')
|
|
20
|
+
out.help = true;
|
|
21
|
+
else if (arg === '--version' || arg === '-v')
|
|
22
|
+
out.version = true;
|
|
23
|
+
else if (arg === '--dry-run')
|
|
24
|
+
out.dryRun = true;
|
|
25
|
+
else if (arg.startsWith('--')) {
|
|
26
|
+
const candidate = arg.slice(2);
|
|
27
|
+
if (FRAMEWORKS.some(f => f.id === candidate)) {
|
|
28
|
+
out.framework = candidate;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
err(`Unknown flag: ${arg}`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (!out.command) {
|
|
36
|
+
out.command = arg;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
err(`Unexpected positional argument: ${arg}`);
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function printUsage() {
|
|
46
|
+
console.log(`${bold('@hover-dev/cli')} — wire Hover into your dev workflow
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
npx @hover-dev/cli add ${dim('# auto-detect bundler, install, wire')}
|
|
50
|
+
npx @hover-dev/cli add --vite ${dim('# force a specific bundler')}
|
|
51
|
+
npx @hover-dev/cli add --astro
|
|
52
|
+
npx @hover-dev/cli add --nuxt
|
|
53
|
+
npx @hover-dev/cli add --webpack
|
|
54
|
+
npx @hover-dev/cli add --dry-run ${dim('# show what would happen, change nothing')}
|
|
55
|
+
npx @hover-dev/cli --help
|
|
56
|
+
npx @hover-dev/cli --version
|
|
57
|
+
|
|
58
|
+
What it does:
|
|
59
|
+
1. Detects your bundler (Vite / Astro / Nuxt / Webpack) from package.json.
|
|
60
|
+
2. Detects your package manager (pnpm / yarn / bun / npm) from your lockfile.
|
|
61
|
+
3. Installs the matching Hover integration as a dev dependency.
|
|
62
|
+
4. Adds the plugin/integration to your config file.
|
|
63
|
+
|
|
64
|
+
For more: https://github.com/Hyperyond/Hover
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
async function runAdd(args) {
|
|
68
|
+
const found = readUserPackageJson();
|
|
69
|
+
if (!found) {
|
|
70
|
+
err(`No package.json found in the current directory or any parent.`);
|
|
71
|
+
err(`Run this command from your project root, or run \`npm init\` first.`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const { pkg, rootDir } = found;
|
|
75
|
+
// Step 1: pick framework — explicit flag wins over detection.
|
|
76
|
+
const framework = args.framework
|
|
77
|
+
? findFrameworkById(args.framework)
|
|
78
|
+
: detectFramework(pkg);
|
|
79
|
+
if (!framework) {
|
|
80
|
+
err(`Couldn't detect a supported bundler in package.json.`);
|
|
81
|
+
info(`Supported: ${FRAMEWORKS.map(f => f.id).join(', ')}.`);
|
|
82
|
+
info(`Force one with --vite / --astro / --nuxt / --webpack.`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
if (args.framework) {
|
|
86
|
+
info(`Using ${bold(framework.label)} (forced via --${framework.id}).`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
info(`Detected ${bold(framework.label)} project.`);
|
|
90
|
+
}
|
|
91
|
+
// Step 2: pick package manager.
|
|
92
|
+
const { pm, reason } = detectPackageManager(rootDir);
|
|
93
|
+
info(`Using package manager: ${bold(pm)} ${dim(`(${reason})`)}.`);
|
|
94
|
+
if (args.dryRun) {
|
|
95
|
+
warn(`Dry-run — not installing or modifying files.`);
|
|
96
|
+
info(`Would install: ${cyan(framework.hoverPackage)} (dev dependency, via ${pm}).`);
|
|
97
|
+
info(`Would mutate: ${cyan(framework.configCandidates[0])} (or whichever candidate exists).`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
// Step 3: install the right Hover package as a dev dependency.
|
|
101
|
+
info(`Installing ${cyan(framework.hoverPackage)} ...`);
|
|
102
|
+
const installCode = await installPackage(pm, framework.hoverPackage, rootDir);
|
|
103
|
+
if (installCode !== 0) {
|
|
104
|
+
err(`Package manager exited with code ${installCode}. Aborting.`);
|
|
105
|
+
return installCode;
|
|
106
|
+
}
|
|
107
|
+
ok(`Installed ${framework.hoverPackage}.`);
|
|
108
|
+
// Step 4: wire the plugin into the config file.
|
|
109
|
+
info(`Wiring into ${cyan(framework.configCandidates[0])} ...`);
|
|
110
|
+
const result = await mutateConfig(rootDir, framework);
|
|
111
|
+
switch (result.kind) {
|
|
112
|
+
case 'ok':
|
|
113
|
+
if (result.alreadyWired) {
|
|
114
|
+
ok(`${result.configPath} already wired — left as-is.`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
ok(`Updated ${result.configPath}.`);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case 'manual':
|
|
121
|
+
warn(`Couldn't update your config automatically: ${result.reason}.`);
|
|
122
|
+
console.log(result.instructions);
|
|
123
|
+
break;
|
|
124
|
+
case 'error':
|
|
125
|
+
warn(`Skipped config update — magicast couldn't safely mutate the file.`);
|
|
126
|
+
warn(`Reason: ${result.reason}`);
|
|
127
|
+
console.log(result.instructions);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
spark(`Done. Run your dev server and click the floating ✨.`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
async function main() {
|
|
134
|
+
const args = parseArgs(process.argv.slice(2));
|
|
135
|
+
if (args.help) {
|
|
136
|
+
printUsage();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (args.version) {
|
|
140
|
+
// Read version from our own package.json at runtime so we don't bake a
|
|
141
|
+
// string into source that drifts from package.json. Walk up from
|
|
142
|
+
// src/ or dist/ until we find the package's own package.json.
|
|
143
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
144
|
+
let dir = here;
|
|
145
|
+
while (dir !== '/' && !existsSync(join(dir, 'package.json')))
|
|
146
|
+
dir = dirname(dir);
|
|
147
|
+
const own = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
|
|
148
|
+
console.log(own.version);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (args.command === 'add') {
|
|
152
|
+
const code = await runAdd(args);
|
|
153
|
+
process.exit(code);
|
|
154
|
+
}
|
|
155
|
+
if (!args.command) {
|
|
156
|
+
printUsage();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
err(`Unknown command: ${args.command}`);
|
|
160
|
+
printUsage();
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
main().catch(e => {
|
|
164
|
+
err(e instanceof Error ? e.stack ?? e.message : String(e));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PackageManager } from './detect.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spawn the user's package manager with the install command. Inherits stdio
|
|
4
|
+
* so the user sees the PM's native progress output (it's better than
|
|
5
|
+
* anything we could synthesise). Resolves with the exit code; the caller
|
|
6
|
+
* decides whether to bail.
|
|
7
|
+
*
|
|
8
|
+
* `cwd` should be the user's project root (where package.json lives) —
|
|
9
|
+
* `detect.readUserPackageJson` returns it as `rootDir`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function installPackage(pm: PackageManager, pkg: string, cwd: string): Promise<number>;
|
|
12
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAqBlD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO5F"}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Map (package manager, install kind) → argv. Each PM has its own dialect:
|
|
4
|
+
* - `pnpm add -D`, `yarn add -D`, `bun add -d`, `npm install --save-dev`.
|
|
5
|
+
* We pin to dev-dependency installs because every Hover integration is a
|
|
6
|
+
* dev-only tool (no production runtime code).
|
|
7
|
+
*/
|
|
8
|
+
function buildInstallArgv(pm, pkg) {
|
|
9
|
+
switch (pm) {
|
|
10
|
+
case 'pnpm':
|
|
11
|
+
return ['add', '-D', pkg];
|
|
12
|
+
case 'yarn':
|
|
13
|
+
return ['add', '-D', pkg];
|
|
14
|
+
case 'bun':
|
|
15
|
+
return ['add', '-d', pkg];
|
|
16
|
+
case 'npm':
|
|
17
|
+
return ['install', '--save-dev', pkg];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Spawn the user's package manager with the install command. Inherits stdio
|
|
22
|
+
* so the user sees the PM's native progress output (it's better than
|
|
23
|
+
* anything we could synthesise). Resolves with the exit code; the caller
|
|
24
|
+
* decides whether to bail.
|
|
25
|
+
*
|
|
26
|
+
* `cwd` should be the user's project root (where package.json lives) —
|
|
27
|
+
* `detect.readUserPackageJson` returns it as `rootDir`.
|
|
28
|
+
*/
|
|
29
|
+
export function installPackage(pm, pkg, cwd) {
|
|
30
|
+
return new Promise((resolveExit, rejectExit) => {
|
|
31
|
+
const argv = buildInstallArgv(pm, pkg);
|
|
32
|
+
const child = spawn(pm, argv, { cwd, stdio: 'inherit' });
|
|
33
|
+
child.on('error', rejectExit);
|
|
34
|
+
child.on('exit', code => resolveExit(code ?? -1));
|
|
35
|
+
});
|
|
36
|
+
}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const dim: (s: string) => string;
|
|
2
|
+
export declare const bold: (s: string) => string;
|
|
3
|
+
export declare const green: (s: string) => string;
|
|
4
|
+
export declare const yellow: (s: string) => string;
|
|
5
|
+
export declare const red: (s: string) => string;
|
|
6
|
+
export declare const blue: (s: string) => string;
|
|
7
|
+
export declare const cyan: (s: string) => string;
|
|
8
|
+
export declare const info: (msg: string) => void;
|
|
9
|
+
export declare const ok: (msg: string) => void;
|
|
10
|
+
export declare const warn: (msg: string) => void;
|
|
11
|
+
export declare const err: (msg: string) => void;
|
|
12
|
+
export declare const spark: (msg: string) => void;
|
|
13
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,GAAG,GAAI,GAAG,MAAM,KAAG,MAAsB,CAAC;AACvD,eAAO,MAAM,IAAI,GAAI,GAAG,MAAM,KAAG,MAAsB,CAAC;AACxD,eAAO,MAAM,KAAK,GAAI,GAAG,MAAM,KAAG,MAAuB,CAAC;AAC1D,eAAO,MAAM,MAAM,GAAI,GAAG,MAAM,KAAG,MAAuB,CAAC;AAC3D,eAAO,MAAM,GAAG,GAAI,GAAG,MAAM,KAAG,MAAuB,CAAC;AACxD,eAAO,MAAM,IAAI,GAAI,GAAG,MAAM,KAAG,MAAuB,CAAC;AACzD,eAAO,MAAM,IAAI,GAAI,GAAG,MAAM,KAAG,MAAuB,CAAC;AAWzD,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,KAAG,IAAyC,CAAC;AAC7E,eAAO,MAAM,EAAE,GAAI,KAAK,MAAM,KAAG,IAAuC,CAAC;AACzE,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,KAAG,IAAyC,CAAC;AAC7E,eAAO,MAAM,GAAG,GAAI,KAAK,MAAM,KAAG,IAA0C,CAAC;AAC7E,eAAO,MAAM,KAAK,GAAI,KAAK,MAAM,KAAG,IAA0C,CAAC"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Minimal ANSI color + symbol helpers. We avoid pulling in chalk / picocolors
|
|
2
|
+
// to keep the CLI dependency surface tight (faster npx cold-start). Falls
|
|
3
|
+
// back to plain text if the output stream isn't a TTY (e.g. CI piping into
|
|
4
|
+
// a log file) so we don't dump escape codes into logs.
|
|
5
|
+
const isTTY = process.stdout.isTTY === true;
|
|
6
|
+
const wrap = (code, s) => (isTTY ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
7
|
+
export const dim = (s) => wrap('2', s);
|
|
8
|
+
export const bold = (s) => wrap('1', s);
|
|
9
|
+
export const green = (s) => wrap('32', s);
|
|
10
|
+
export const yellow = (s) => wrap('33', s);
|
|
11
|
+
export const red = (s) => wrap('31', s);
|
|
12
|
+
export const blue = (s) => wrap('34', s);
|
|
13
|
+
export const cyan = (s) => wrap('36', s);
|
|
14
|
+
// Status symbols mirror what tools like Vite / Astro / shadcn use.
|
|
15
|
+
const SYM = {
|
|
16
|
+
info: blue('ℹ'),
|
|
17
|
+
ok: green('✓'),
|
|
18
|
+
warn: yellow('⚠'),
|
|
19
|
+
err: red('✗'),
|
|
20
|
+
spark: cyan('✨'),
|
|
21
|
+
};
|
|
22
|
+
export const info = (msg) => console.log(`${SYM.info} ${msg}`);
|
|
23
|
+
export const ok = (msg) => console.log(`${SYM.ok} ${msg}`);
|
|
24
|
+
export const warn = (msg) => console.log(`${SYM.warn} ${msg}`);
|
|
25
|
+
export const err = (msg) => console.error(`${SYM.err} ${msg}`);
|
|
26
|
+
export const spark = (msg) => console.log(`${SYM.spark} ${msg}`);
|
package/dist/mutate.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Framework, FrameworkId } from './frameworks.js';
|
|
2
|
+
/**
|
|
3
|
+
* Result of attempting to wire Hover into the user's config file.
|
|
4
|
+
*
|
|
5
|
+
* - `ok` — config was modified (or was already wired; idempotent).
|
|
6
|
+
* - `manual` — we couldn't safely mutate (no config file, unusual shape).
|
|
7
|
+
* `instructions` contains the lines the user should paste themselves.
|
|
8
|
+
* - `error` — magicast threw; we bailed without touching the file.
|
|
9
|
+
*/
|
|
10
|
+
export type MutateResult = {
|
|
11
|
+
kind: 'ok';
|
|
12
|
+
configPath: string;
|
|
13
|
+
alreadyWired: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'manual';
|
|
16
|
+
reason: string;
|
|
17
|
+
instructions: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'error';
|
|
20
|
+
reason: string;
|
|
21
|
+
instructions: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Wire Hover into the user's config file. Picks the first existing
|
|
25
|
+
* `configCandidates` entry, dispatches to a per-framework mutator. Each
|
|
26
|
+
* mutator is responsible for being idempotent (re-running the CLI on an
|
|
27
|
+
* already-wired project should not duplicate the import or array entry).
|
|
28
|
+
*/
|
|
29
|
+
export declare function mutateConfig(rootDir: string, framework: Framework): Promise<MutateResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Plain-text fallback instructions for `kind: 'manual'` / `kind: 'error'`.
|
|
32
|
+
* Mirrors what the AST mutator would have produced, so a user with an
|
|
33
|
+
* unusual config file can hand-edit and get the same result.
|
|
34
|
+
*/
|
|
35
|
+
export declare function manualInstructions(id: FrameworkId): string;
|
|
36
|
+
//# sourceMappingURL=mutate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutate.d.ts","sourceRoot":"","sources":["../src/mutate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CA+B/F;AAoHD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,CAiC1D"}
|
package/dist/mutate.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { builders, loadFile, writeFile } from 'magicast';
|
|
4
|
+
/**
|
|
5
|
+
* Wire Hover into the user's config file. Picks the first existing
|
|
6
|
+
* `configCandidates` entry, dispatches to a per-framework mutator. Each
|
|
7
|
+
* mutator is responsible for being idempotent (re-running the CLI on an
|
|
8
|
+
* already-wired project should not duplicate the import or array entry).
|
|
9
|
+
*/
|
|
10
|
+
export async function mutateConfig(rootDir, framework) {
|
|
11
|
+
const configPath = framework.configCandidates
|
|
12
|
+
.map(name => join(rootDir, name))
|
|
13
|
+
.find(p => existsSync(p));
|
|
14
|
+
if (!configPath) {
|
|
15
|
+
return {
|
|
16
|
+
kind: 'manual',
|
|
17
|
+
reason: `no ${framework.configCandidates[0]} found`,
|
|
18
|
+
instructions: manualInstructions(framework.id),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
switch (framework.id) {
|
|
23
|
+
case 'vite':
|
|
24
|
+
return await mutateVite(configPath);
|
|
25
|
+
case 'astro':
|
|
26
|
+
return await mutateAstro(configPath);
|
|
27
|
+
case 'nuxt':
|
|
28
|
+
return await mutateNuxt(configPath);
|
|
29
|
+
case 'webpack':
|
|
30
|
+
return await mutateWebpack(configPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
+
return {
|
|
36
|
+
kind: 'error',
|
|
37
|
+
reason: msg,
|
|
38
|
+
instructions: manualInstructions(framework.id),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ─── Vite: hover() in plugins array ─────────────────────────────────────
|
|
43
|
+
async function mutateVite(configPath) {
|
|
44
|
+
const mod = await loadFile(configPath);
|
|
45
|
+
// Idempotency: bail if `hover` is already imported from vite-plugin-hover.
|
|
46
|
+
// (We could be more clever and re-push if missing-from-array but present
|
|
47
|
+
// in imports, but that's a corner case — overwriting an intentional removal
|
|
48
|
+
// would be more harmful.)
|
|
49
|
+
if (alreadyImported(mod, 'vite-plugin-hover')) {
|
|
50
|
+
return { kind: 'ok', configPath, alreadyWired: true };
|
|
51
|
+
}
|
|
52
|
+
mod.imports.$add({ from: 'vite-plugin-hover', imported: 'hover' });
|
|
53
|
+
const config = unwrapDefineConfig(mod.exports.default);
|
|
54
|
+
ensureArray(config, 'plugins');
|
|
55
|
+
config.plugins.push(builders.functionCall('hover'));
|
|
56
|
+
await writeFile(mod, configPath);
|
|
57
|
+
return { kind: 'ok', configPath, alreadyWired: false };
|
|
58
|
+
}
|
|
59
|
+
// ─── Astro: hover() in integrations array ───────────────────────────────
|
|
60
|
+
async function mutateAstro(configPath) {
|
|
61
|
+
const mod = await loadFile(configPath);
|
|
62
|
+
if (alreadyImported(mod, '@hover-dev/astro')) {
|
|
63
|
+
return { kind: 'ok', configPath, alreadyWired: true };
|
|
64
|
+
}
|
|
65
|
+
mod.imports.$add({ from: '@hover-dev/astro', imported: 'hover' });
|
|
66
|
+
const config = unwrapDefineConfig(mod.exports.default);
|
|
67
|
+
ensureArray(config, 'integrations');
|
|
68
|
+
config.integrations.push(builders.functionCall('hover'));
|
|
69
|
+
await writeFile(mod, configPath);
|
|
70
|
+
return { kind: 'ok', configPath, alreadyWired: false };
|
|
71
|
+
}
|
|
72
|
+
// ─── Nuxt: '@hover-dev/nuxt' string in modules array ────────────────────
|
|
73
|
+
async function mutateNuxt(configPath) {
|
|
74
|
+
const mod = await loadFile(configPath);
|
|
75
|
+
const config = unwrapDefineConfig(mod.exports.default);
|
|
76
|
+
ensureArray(config, 'modules');
|
|
77
|
+
// Idempotency: Nuxt modules are referenced by string, so check the array.
|
|
78
|
+
// magicast arrays are iterable proxies, not real Arrays, so don't trust
|
|
79
|
+
// `Array.isArray` — iterate instead.
|
|
80
|
+
for (const m of config.modules) {
|
|
81
|
+
if (m === '@hover-dev/nuxt') {
|
|
82
|
+
return { kind: 'ok', configPath, alreadyWired: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
config.modules.push('@hover-dev/nuxt');
|
|
86
|
+
await writeFile(mod, configPath);
|
|
87
|
+
return { kind: 'ok', configPath, alreadyWired: false };
|
|
88
|
+
}
|
|
89
|
+
// ─── Webpack: new HoverPlugin() in plugins array ────────────────────────
|
|
90
|
+
async function mutateWebpack(configPath) {
|
|
91
|
+
const mod = await loadFile(configPath);
|
|
92
|
+
if (alreadyImported(mod, 'webpack-plugin-hover')) {
|
|
93
|
+
return { kind: 'ok', configPath, alreadyWired: true };
|
|
94
|
+
}
|
|
95
|
+
mod.imports.$add({ from: 'webpack-plugin-hover', imported: 'HoverPlugin' });
|
|
96
|
+
const config = unwrapDefineConfig(mod.exports.default);
|
|
97
|
+
ensureArray(config, 'plugins');
|
|
98
|
+
config.plugins.push(builders.newExpression('HoverPlugin'));
|
|
99
|
+
await writeFile(mod, configPath);
|
|
100
|
+
return { kind: 'ok', configPath, alreadyWired: false };
|
|
101
|
+
}
|
|
102
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Check whether a module already has a named/default import from `source`.
|
|
105
|
+
* magicast's `imports` proxy is keyed by binding name; each entry exposes
|
|
106
|
+
* its source as `.from`. Defensive: returns false on any unexpected shape.
|
|
107
|
+
*/
|
|
108
|
+
function alreadyImported(mod, source) {
|
|
109
|
+
try {
|
|
110
|
+
for (const item of mod.imports.$items) {
|
|
111
|
+
if (item.from === source)
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* fall through */
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `export default defineConfig({ ... })` is the convention across Vite /
|
|
122
|
+
* Astro / Nuxt. Unwrap to the inner object so callers can mutate
|
|
123
|
+
* `plugins` / `integrations` / `modules` directly. Bare object exports
|
|
124
|
+
* (`export default { ... }`) are returned unchanged.
|
|
125
|
+
*/
|
|
126
|
+
function unwrapDefineConfig(exp) {
|
|
127
|
+
if (exp?.$type === 'function-call' && Array.isArray(exp.$args) && exp.$args[0]) {
|
|
128
|
+
return exp.$args[0];
|
|
129
|
+
}
|
|
130
|
+
return exp;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Ensure a key on the magicast-proxied config object holds an array. The
|
|
134
|
+
* obvious-looking `cfg[key] ??= []` does NOT work reliably against the
|
|
135
|
+
* proxy — the proxy returns "something" for missing keys (not undefined),
|
|
136
|
+
* so the nullish-assignment short-circuits, and subsequent pushes go into
|
|
137
|
+
* a detached array that never makes it back to the AST. Explicit `if not
|
|
138
|
+
* present then assign a real []` does work, because the proxy turns the
|
|
139
|
+
* assignment into an actual AST node.
|
|
140
|
+
*/
|
|
141
|
+
function ensureArray(config, key) {
|
|
142
|
+
if (config[key] === undefined || config[key] === null) {
|
|
143
|
+
config[key] = [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Plain-text fallback instructions for `kind: 'manual'` / `kind: 'error'`.
|
|
148
|
+
* Mirrors what the AST mutator would have produced, so a user with an
|
|
149
|
+
* unusual config file can hand-edit and get the same result.
|
|
150
|
+
*/
|
|
151
|
+
export function manualInstructions(id) {
|
|
152
|
+
switch (id) {
|
|
153
|
+
case 'vite':
|
|
154
|
+
return [
|
|
155
|
+
`Add to your vite config:`,
|
|
156
|
+
``,
|
|
157
|
+
` import { hover } from 'vite-plugin-hover';`,
|
|
158
|
+
` // ...`,
|
|
159
|
+
` plugins: [react(), hover()],`,
|
|
160
|
+
].join('\n');
|
|
161
|
+
case 'astro':
|
|
162
|
+
return [
|
|
163
|
+
`Add to your astro config:`,
|
|
164
|
+
``,
|
|
165
|
+
` import { hover } from '@hover-dev/astro';`,
|
|
166
|
+
` // ...`,
|
|
167
|
+
` integrations: [hover()],`,
|
|
168
|
+
].join('\n');
|
|
169
|
+
case 'nuxt':
|
|
170
|
+
return [
|
|
171
|
+
`Add to your nuxt config:`,
|
|
172
|
+
``,
|
|
173
|
+
` modules: ['@hover-dev/nuxt'],`,
|
|
174
|
+
].join('\n');
|
|
175
|
+
case 'webpack':
|
|
176
|
+
return [
|
|
177
|
+
`Add to your webpack config:`,
|
|
178
|
+
``,
|
|
179
|
+
` const { HoverPlugin } = require('webpack-plugin-hover');`,
|
|
180
|
+
` // ...`,
|
|
181
|
+
` plugins: [..., new HoverPlugin()],`,
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
|
184
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hover-dev/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "CLI for Hover. Detects your bundler, installs the right Hover integration package, and wires it into your config — one command.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Hyperyond",
|
|
7
|
+
"homepage": "https://github.com/Hyperyond/Hover#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Hyperyond/Hover.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/Hyperyond/Hover/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"hover",
|
|
16
|
+
"cli",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"vite",
|
|
19
|
+
"astro",
|
|
20
|
+
"nuxt",
|
|
21
|
+
"webpack",
|
|
22
|
+
"playwright",
|
|
23
|
+
"claude"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"magicast": "^0.5.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"registry": "https://registry.npmjs.org",
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
47
|
+
},
|
|
48
|
+
"types": "dist/index.d.ts",
|
|
49
|
+
"bin": {
|
|
50
|
+
"hover": "dist/index.js"
|
|
51
|
+
}
|
|
52
|
+
}
|