@fusion.dev/helix-cli 0.0.9
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 +21 -0
- package/bin.mjs +165 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fusion Development Group
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin.mjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `helix` for a caller that reaches it through npm rather than through cargo.
|
|
4
|
+
*
|
|
5
|
+
* **A near-copy of `fusion/cli/npm/bin.mjs`, deliberately.** Sharing it would
|
|
6
|
+
* put a file in the fusion domain on the publish path of a helix package, and
|
|
7
|
+
* per-domain lockstep exists so a release of either does not wait on the other.
|
|
8
|
+
* It is the same trade the two `Rule` types make, for the same reason.
|
|
9
|
+
*
|
|
10
|
+
* **The binary is Rust and half this repository is not**, so the checks that
|
|
11
|
+
* call it are `bun run` lines in a manifest and the people running them have
|
|
12
|
+
* node on their path and may not have cargo. This resolves a built binary,
|
|
13
|
+
* builds one if it can, and says exactly what is missing if it cannot.
|
|
14
|
+
*
|
|
15
|
+
* **Resolution order, and each step is there for a reason.** `HELIX_BIN` first,
|
|
16
|
+
* because a caller that knows better should not have to argue. Then the cargo
|
|
17
|
+
* target directory, release before debug, honoring `CARGO_TARGET_DIR` because a
|
|
18
|
+
* shared target is common and guessing `./target` would miss it. Then whatever
|
|
19
|
+
* is on `PATH`, which is what an installed binary looks like. Only then a
|
|
20
|
+
* build, and only when cargo is present.
|
|
21
|
+
*
|
|
22
|
+
* **A build is a fallback and not the plan.** It happens once, prints that it
|
|
23
|
+
* is happening, and a CI pipeline that has already run `cargo build` never
|
|
24
|
+
* reaches it. What it replaces is the failure this shim exists to prevent: a
|
|
25
|
+
* check that dies with `spawn fusion ENOENT` and tells a reader nothing about
|
|
26
|
+
* which of the four possible causes they have.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process'
|
|
29
|
+
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
30
|
+
import { delimiter, dirname, join, resolve } from 'node:path'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
|
|
33
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
34
|
+
const CRATE = resolve(HERE, '..')
|
|
35
|
+
const WORKSPACE = resolve(CRATE, '../..')
|
|
36
|
+
const NAME = process.platform === 'win32' ? 'helix.exe' : 'helix'
|
|
37
|
+
|
|
38
|
+
const target = process.env.CARGO_TARGET_DIR
|
|
39
|
+
? resolve(process.env.CARGO_TARGET_DIR)
|
|
40
|
+
: join(WORKSPACE, 'target')
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The most recently built binary, or undefined.
|
|
44
|
+
*
|
|
45
|
+
* **By modification time, not by profile.** Preferring release outright is the
|
|
46
|
+
* obvious rule and it is a trap on a development machine: one `cargo build
|
|
47
|
+
* --release` leaves an artifact that shadows every `cargo build` after it, so a
|
|
48
|
+
* rule added an hour ago is not there and the command reports a smaller, older
|
|
49
|
+
* truth without saying anything is wrong. Measured: a release binary two commits
|
|
50
|
+
* behind reported nine rules against the debug build's twelve, and one of them
|
|
51
|
+
* as counting nothing. On a CI runner only one profile exists and this is the
|
|
52
|
+
* same answer either way.
|
|
53
|
+
*/
|
|
54
|
+
const built = () =>
|
|
55
|
+
[join(target, 'release', NAME), join(target, 'debug', NAME)]
|
|
56
|
+
.filter((path) => existsSync(path))
|
|
57
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
58
|
+
.at(0)
|
|
59
|
+
|
|
60
|
+
/** Whether cargo is on the path, asked once and cheaply. */
|
|
61
|
+
const hasCargo = () => spawnSync('cargo', ['--version'], { stdio: 'ignore' }).status === 0
|
|
62
|
+
|
|
63
|
+
/** This file, resolved, which is what the `PATH` entry has to not be. */
|
|
64
|
+
const SELF = realpathSync(fileURLToPath(import.meta.url))
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The first `helix` on `PATH`, resolved through its symlinks, or undefined.
|
|
68
|
+
*
|
|
69
|
+
* **Walked rather than spawned, because the spawn is the bug.** Probing with
|
|
70
|
+
* `spawnSync('helix', ['--version'])` asks `PATH` to answer, and under
|
|
71
|
+
* `bun run` the first answer is `node_modules/.bin/helix`, which is a symlink
|
|
72
|
+
* to this file: the probe ran this shim, which had no built binary either, so
|
|
73
|
+
* it probed `PATH` again. Nothing returns and nothing errors, because every
|
|
74
|
+
* level is waiting on the level below it. Measured on a checkout with no
|
|
75
|
+
* `target/`: 4,871 node processes before the machine stopped forking.
|
|
76
|
+
*
|
|
77
|
+
* `built()` answering first is why this was never seen. Any tree that has run
|
|
78
|
+
* `cargo build` once takes the branch above and never reaches here, so the
|
|
79
|
+
* trap is set for a fresh clone, which is what CI and a new contributor both
|
|
80
|
+
* are.
|
|
81
|
+
*/
|
|
82
|
+
const onPath = () => {
|
|
83
|
+
const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : ['']
|
|
84
|
+
for (const directory of (process.env.PATH ?? '').split(delimiter)) {
|
|
85
|
+
if (directory === '') {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
for (const extension of extensions) {
|
|
89
|
+
const candidate = join(directory, NAME + extension)
|
|
90
|
+
if (!existsSync(candidate)) {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
/* Through the symlink, so `node_modules/.bin/helix` is recognized as
|
|
94
|
+
this file rather than as an installed binary that happens to share a
|
|
95
|
+
name. A path that cannot be resolved is not one to hand to `spawn`. */
|
|
96
|
+
let real
|
|
97
|
+
try {
|
|
98
|
+
real = realpathSync(candidate)
|
|
99
|
+
} catch {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
/* Skipped and not given up on: an entry earlier in `PATH` being this file
|
|
103
|
+
says nothing about a real one later in it, and returning here would
|
|
104
|
+
make an installed binary invisible whenever a checkout was in front of
|
|
105
|
+
it. */
|
|
106
|
+
if (real === SELF) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
return real
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const run = (binary) => {
|
|
116
|
+
const { status, error } = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' })
|
|
117
|
+
if (error) {
|
|
118
|
+
console.error(`helix: could not run \`${binary}\`: ${error.message}`)
|
|
119
|
+
process.exit(70)
|
|
120
|
+
}
|
|
121
|
+
process.exit(status ?? 70)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const found = process.env.HELIX_BIN ?? built()
|
|
125
|
+
if (found && existsSync(found)) {
|
|
126
|
+
run(found)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* `PATH`, which is what an installed binary looks like. Resolved rather than
|
|
130
|
+
probed: see `onPath`, where the probe this used to do is what recursed. */
|
|
131
|
+
const installed = onPath()
|
|
132
|
+
if (installed) {
|
|
133
|
+
run(installed)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!hasCargo()) {
|
|
137
|
+
console.error(
|
|
138
|
+
'helix: no binary and no cargo.\n\n' +
|
|
139
|
+
` Looked at: $HELIX_BIN, ${join(target, 'release', NAME)},\n` +
|
|
140
|
+
` ${join(target, 'debug', NAME)}, and \`${NAME}\` on PATH.\n\n` +
|
|
141
|
+
' Either install the binary, point `HELIX_BIN` at one, or install a Rust\n' +
|
|
142
|
+
' toolchain so this can build it.',
|
|
143
|
+
)
|
|
144
|
+
process.exit(69)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.error('helix: no binary yet, building it once (cargo build -p fusion-helix-cli)')
|
|
148
|
+
const build = spawnSync('cargo', ['build', '-p', 'fusion-helix-cli'], {
|
|
149
|
+
cwd: WORKSPACE,
|
|
150
|
+
stdio: 'inherit',
|
|
151
|
+
})
|
|
152
|
+
if (build.status !== 0) {
|
|
153
|
+
console.error('helix: the build failed, so there is nothing to run.')
|
|
154
|
+
process.exit(build.status ?? 70)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const after = built()
|
|
158
|
+
if (!after) {
|
|
159
|
+
console.error(
|
|
160
|
+
`helix: cargo reported success and produced no \`${NAME}\` under ${target}.\n` +
|
|
161
|
+
' That usually means a different target directory. Try `CARGO_TARGET_DIR`.',
|
|
162
|
+
)
|
|
163
|
+
process.exit(70)
|
|
164
|
+
}
|
|
165
|
+
run(after)
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fusion.dev/helix-cli",
|
|
3
|
+
"description": "The Helix command line, for a workspace that reaches it through npm.",
|
|
4
|
+
"version": "0.0.9",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"directory": "helix/cli/npm",
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/project-fusion/fusion.git"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"bin.mjs"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"helix": "./bin.mjs"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "bun test"
|
|
21
|
+
},
|
|
22
|
+
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
23
|
+
"homepage": "https://fusion.dev",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"helix",
|
|
26
|
+
"cli",
|
|
27
|
+
"design-system",
|
|
28
|
+
"scaffold",
|
|
29
|
+
"theme"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "restricted"
|
|
33
|
+
}
|
|
34
|
+
}
|