facult 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/facult.cjs +302 -0
- package/package.json +78 -0
- package/src/adapters/claude-cli.ts +18 -0
- package/src/adapters/claude-desktop.ts +15 -0
- package/src/adapters/clawdbot.ts +18 -0
- package/src/adapters/codex.ts +19 -0
- package/src/adapters/cursor.ts +18 -0
- package/src/adapters/index.ts +69 -0
- package/src/adapters/mcp.ts +270 -0
- package/src/adapters/reference.ts +9 -0
- package/src/adapters/skills.ts +47 -0
- package/src/adapters/types.ts +42 -0
- package/src/adapters/version.ts +18 -0
- package/src/audit/agent.ts +1071 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/static.ts +1130 -0
- package/src/audit/tui.ts +704 -0
- package/src/audit/types.ts +68 -0
- package/src/audit/update-index.ts +115 -0
- package/src/conflicts.ts +135 -0
- package/src/consolidate-conflict-action.ts +57 -0
- package/src/consolidate.ts +1637 -0
- package/src/enable-disable.ts +349 -0
- package/src/index-builder.ts +562 -0
- package/src/index.ts +589 -0
- package/src/manage.ts +894 -0
- package/src/migrate.ts +272 -0
- package/src/paths.ts +238 -0
- package/src/quarantine.ts +217 -0
- package/src/query.ts +186 -0
- package/src/remote-manifest-integrity.ts +367 -0
- package/src/remote-providers.ts +905 -0
- package/src/remote-source-policy.ts +237 -0
- package/src/remote-sources.ts +162 -0
- package/src/remote-types.ts +136 -0
- package/src/remote.ts +1970 -0
- package/src/scan.ts +2427 -0
- package/src/schema.ts +39 -0
- package/src/self-update.ts +408 -0
- package/src/snippets-cli.ts +293 -0
- package/src/snippets.ts +706 -0
- package/src/source-trust.ts +203 -0
- package/src/trust-list.ts +232 -0
- package/src/trust.ts +170 -0
- package/src/tui.ts +118 -0
- package/src/util/codex-toml.ts +126 -0
- package/src/util/json.ts +32 -0
- package/src/util/skills.ts +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hack Dance
|
|
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/README.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# facult
|
|
2
|
+
|
|
3
|
+
`facult` is a CLI for managing coding-agent skills and MCP configs across tools.
|
|
4
|
+
|
|
5
|
+
It helps you:
|
|
6
|
+
- discover what is installed on your machine
|
|
7
|
+
- consolidate everything into one canonical store
|
|
8
|
+
- review trust/security before installing remote content
|
|
9
|
+
- enable a curated skill set across Codex, Cursor, and Claude
|
|
10
|
+
|
|
11
|
+
## What facult Is
|
|
12
|
+
|
|
13
|
+
If your agent setup feels scattered (`~/.codex`, `~/.agents`, tool-specific MCP JSON/TOML), `facult` gives you one place to manage it safely.
|
|
14
|
+
|
|
15
|
+
Think of it as:
|
|
16
|
+
- inventory + auditing for agent assets
|
|
17
|
+
- package manager interface for skill/MCP catalogs
|
|
18
|
+
- sync layer that applies your chosen setup to each tool
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Install facult
|
|
23
|
+
|
|
24
|
+
Recommended global install:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g facult
|
|
28
|
+
# or
|
|
29
|
+
bun add -g facult
|
|
30
|
+
facult --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
One-off usage without global install:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx facult --help
|
|
37
|
+
bunx facult --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Direct binary install from GitHub Releases (macOS/Linux):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -fsSL https://github.com/hack-dance/facult/releases/latest/download/facult-install.sh | bash
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If release assets are private, set a token first:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export FACULT_GITHUB_TOKEN="<github-token>"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Windows and manual installs can download the correct binary from each release page:
|
|
53
|
+
`facult-<version>-<platform>-<arch>`.
|
|
54
|
+
|
|
55
|
+
Update later with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
facult self-update
|
|
59
|
+
# or
|
|
60
|
+
facult update --self
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Pin to a specific version:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
facult self-update --version 0.0.1
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Import existing skills/configs
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
facult consolidate --auto keep-current --from ~/.codex/skills --from ~/.agents/skills
|
|
73
|
+
facult index
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Why `keep-current`: it is deterministic and non-interactive for duplicate sources.
|
|
77
|
+
|
|
78
|
+
Default canonical store: `~/agents/.facult`. You can change it later with `FACULT_ROOT_DIR` or `~/.facult/config.json`.
|
|
79
|
+
|
|
80
|
+
### 3. Inspect what you have
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
facult list skills
|
|
84
|
+
facult list mcp
|
|
85
|
+
facult show requesting-code-review
|
|
86
|
+
facult show mcp:github
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. Enable managed mode for your tools
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
facult manage codex
|
|
93
|
+
facult manage cursor
|
|
94
|
+
facult manage claude
|
|
95
|
+
|
|
96
|
+
facult enable requesting-code-review receiving-code-review brainstorming systematic-debugging --for codex,cursor,claude
|
|
97
|
+
facult sync
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
At this point, your selected skills are actively synced to all managed tools.
|
|
101
|
+
|
|
102
|
+
### 5. Turn on source trust and strict install flow
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
facult sources list
|
|
106
|
+
facult verify-source skills.sh --json
|
|
107
|
+
facult sources trust skills.sh --note "reviewed"
|
|
108
|
+
|
|
109
|
+
facult install skills.sh:code-review --as code-review-skills-sh --strict-source-trust
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Use facult from your agents
|
|
113
|
+
|
|
114
|
+
`facult` is CLI-first. The practical setup is:
|
|
115
|
+
1. Install `facult` globally so any agent runtime can execute it.
|
|
116
|
+
2. Put allowed `facult` workflows in your agent instructions/skills.
|
|
117
|
+
3. Optionally scaffold MCP wrappers if you want an MCP entry that delegates to `facult`.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Scaffold reusable templates in the canonical store
|
|
121
|
+
facult templates init agents
|
|
122
|
+
facult templates init claude
|
|
123
|
+
facult templates init skill facult-manager
|
|
124
|
+
|
|
125
|
+
# Enable that skill for managed tools
|
|
126
|
+
facult manage codex
|
|
127
|
+
facult manage cursor
|
|
128
|
+
facult manage claude
|
|
129
|
+
facult enable facult-manager --for codex,cursor,claude
|
|
130
|
+
facult sync
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Optional MCP scaffold:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
facult templates init mcp facult-cli
|
|
137
|
+
facult enable mcp:facult-cli --for codex,cursor,claude
|
|
138
|
+
facult sync
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Note: `templates init mcp ...` is a scaffold, not a running server by itself.
|
|
142
|
+
|
|
143
|
+
## Security and Trust
|
|
144
|
+
|
|
145
|
+
`facult` has two trust layers:
|
|
146
|
+
- Item trust: `facult trust <name>` / `facult untrust <name>`
|
|
147
|
+
- Source trust: `facult sources ...` with levels `trusted`, `review`, `blocked`
|
|
148
|
+
|
|
149
|
+
`facult` also supports two audit modes:
|
|
150
|
+
|
|
151
|
+
1. Interactive audit workflow:
|
|
152
|
+
```bash
|
|
153
|
+
facult audit
|
|
154
|
+
```
|
|
155
|
+
2. Static audit rules (deterministic pattern checks):
|
|
156
|
+
```bash
|
|
157
|
+
facult audit --non-interactive --severity high
|
|
158
|
+
facult audit --non-interactive mcp:github --severity medium --json
|
|
159
|
+
```
|
|
160
|
+
3. Agent-based audit (Claude/Codex review pass):
|
|
161
|
+
```bash
|
|
162
|
+
facult audit --non-interactive --with claude --max-items 50
|
|
163
|
+
facult audit --non-interactive --with codex --max-items all --json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Recommended security flow:
|
|
167
|
+
1. `facult verify-source <source>`
|
|
168
|
+
2. `facult sources trust <source>` only after review
|
|
169
|
+
3. use `--strict-source-trust` for `install`/`update`
|
|
170
|
+
4. run both static and agent audits on a schedule
|
|
171
|
+
|
|
172
|
+
## Comprehensive Reference
|
|
173
|
+
|
|
174
|
+
### Capability categories
|
|
175
|
+
|
|
176
|
+
- Inventory: discover local skills, MCP configs, hooks, and instruction files
|
|
177
|
+
- Management: consolidate, index, manage/unmanage tools, enable/disable entries
|
|
178
|
+
- Security: static audit, agent audit, item trust, source trust, source verification
|
|
179
|
+
- Distribution: search/install/update from catalogs and verified manifests
|
|
180
|
+
- DX: scaffold templates and sync snippets into instruction/config files
|
|
181
|
+
|
|
182
|
+
### Command categories
|
|
183
|
+
|
|
184
|
+
- Inventory and discovery
|
|
185
|
+
```bash
|
|
186
|
+
facult scan [--from <path>] [--json] [--show-duplicates]
|
|
187
|
+
facult list [skills|mcp|agents|snippets] [--enabled-for <tool>] [--untrusted] [--flagged] [--pending]
|
|
188
|
+
facult show <name>
|
|
189
|
+
facult show mcp:<name> [--show-secrets]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- Canonical store and migration
|
|
193
|
+
```bash
|
|
194
|
+
facult consolidate [--auto keep-current|keep-incoming|keep-newest] [--from <path> ...]
|
|
195
|
+
facult index [--force]
|
|
196
|
+
facult migrate [--from <path>] [--dry-run] [--move] [--write-config]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- Managed mode and rollout
|
|
200
|
+
```bash
|
|
201
|
+
facult manage <tool>
|
|
202
|
+
facult unmanage <tool>
|
|
203
|
+
facult managed
|
|
204
|
+
facult enable <name> [--for <tool1,tool2,...>]
|
|
205
|
+
facult enable mcp:<name> [--for <tool1,tool2,...>]
|
|
206
|
+
facult disable <name> [--for <tool1,tool2,...>]
|
|
207
|
+
facult sync [tool] [--dry-run]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- Remote catalogs and policies
|
|
211
|
+
```bash
|
|
212
|
+
facult search <query> [--index <name>] [--limit <n>]
|
|
213
|
+
facult install <index:item> [--as <name>] [--force] [--strict-source-trust]
|
|
214
|
+
facult update [--apply] [--strict-source-trust]
|
|
215
|
+
facult verify-source <name> [--json]
|
|
216
|
+
facult sources list
|
|
217
|
+
facult sources trust <source> [--note <text>]
|
|
218
|
+
facult sources review <source> [--note <text>]
|
|
219
|
+
facult sources block <source> [--note <text>]
|
|
220
|
+
facult sources clear <source>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- Templates and snippets
|
|
224
|
+
```bash
|
|
225
|
+
facult templates list
|
|
226
|
+
facult templates init skill <name>
|
|
227
|
+
facult templates init mcp <name>
|
|
228
|
+
facult templates init snippet <marker>
|
|
229
|
+
facult templates init agents
|
|
230
|
+
facult templates init claude
|
|
231
|
+
|
|
232
|
+
facult snippets list
|
|
233
|
+
facult snippets show <marker>
|
|
234
|
+
facult snippets create <marker>
|
|
235
|
+
facult snippets edit <marker>
|
|
236
|
+
facult snippets sync [--dry-run] [file...]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For full flags and exact usage:
|
|
240
|
+
```bash
|
|
241
|
+
facult --help
|
|
242
|
+
facult <command> --help
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Root resolution
|
|
246
|
+
|
|
247
|
+
`facult` resolves the canonical root in this order:
|
|
248
|
+
1. `FACULT_ROOT_DIR`
|
|
249
|
+
2. `~/.facult/config.json` (`rootDir`)
|
|
250
|
+
3. `~/agents/.facult` (or a detected legacy store under `~/agents/`)
|
|
251
|
+
|
|
252
|
+
### Runtime env vars
|
|
253
|
+
|
|
254
|
+
- `FACULT_ROOT_DIR`: override canonical store location
|
|
255
|
+
- `FACULT_GITHUB_TOKEN`: auth token for private GitHub release asset downloads
|
|
256
|
+
- `GITHUB_TOKEN` / `GH_TOKEN`: fallback tokens for release asset downloads
|
|
257
|
+
- `FACULT_VERSION`: version selector for `scripts/install.sh` (`latest` by default)
|
|
258
|
+
- `FACULT_INSTALL_DIR`: install target dir for `scripts/install.sh` (`~/.facult/bin` by default)
|
|
259
|
+
- `FACULT_INSTALL_PM`: force package manager detection for npm bootstrap launcher (`npm` or `bun`)
|
|
260
|
+
|
|
261
|
+
### State and report files
|
|
262
|
+
|
|
263
|
+
Under `~/.facult/`:
|
|
264
|
+
- `sources.json` (latest inventory scan state)
|
|
265
|
+
- `consolidated.json` (consolidation state)
|
|
266
|
+
- `managed.json` (managed tool state)
|
|
267
|
+
- `audit/static-latest.json` (latest static audit report)
|
|
268
|
+
- `audit/agent-latest.json` (latest agent audit report)
|
|
269
|
+
- `trust/sources.json` (source trust policy state)
|
|
270
|
+
|
|
271
|
+
### Config reference
|
|
272
|
+
|
|
273
|
+
`~/.facult/config.json` supports:
|
|
274
|
+
- `rootDir`
|
|
275
|
+
- `scanFrom`
|
|
276
|
+
- `scanFromIgnore`
|
|
277
|
+
- `scanFromNoDefaultIgnore`
|
|
278
|
+
- `scanFromMaxVisits`
|
|
279
|
+
- `scanFromMaxResults`
|
|
280
|
+
|
|
281
|
+
`scanFrom*` settings are used by `scan`/`audit` unless `--no-config-from` is passed.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"rootDir": "~/agents/.facult",
|
|
287
|
+
"scanFrom": ["~/dev", "~/work"],
|
|
288
|
+
"scanFromIgnore": ["vendor", ".venv"],
|
|
289
|
+
"scanFromNoDefaultIgnore": false,
|
|
290
|
+
"scanFromMaxVisits": 20000,
|
|
291
|
+
"scanFromMaxResults": 5000
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Source aliases and custom indices
|
|
296
|
+
|
|
297
|
+
Default source aliases:
|
|
298
|
+
- `facult` (builtin templates)
|
|
299
|
+
- `smithery`
|
|
300
|
+
- `glama`
|
|
301
|
+
- `skills.sh`
|
|
302
|
+
- `clawhub`
|
|
303
|
+
|
|
304
|
+
Custom remote sources can be defined in `~/.facult/indices.json` (manifest URL, optional integrity, optional signature keys/signature verification settings).
|
|
305
|
+
|
|
306
|
+
## Local Install Modes
|
|
307
|
+
|
|
308
|
+
For local CLI setup (outside npm global install), use:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
bun run install:dev
|
|
312
|
+
bun run install:bin
|
|
313
|
+
bun run install:status
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Default install path is `~/.facult/bin/facult`. You can pass a custom target dir via `--dir=/path`.
|
|
317
|
+
|
|
318
|
+
## CI and Release Automation
|
|
319
|
+
|
|
320
|
+
- CI workflow: `.github/workflows/ci.yml`
|
|
321
|
+
- Release workflow: `.github/workflows/release.yml`
|
|
322
|
+
- Semantic-release config: `.releaserc.json`
|
|
323
|
+
|
|
324
|
+
Release behavior:
|
|
325
|
+
1. Every push to `main` runs full checks.
|
|
326
|
+
2. `semantic-release` creates the version/tag and GitHub release (npm publish is disabled in this phase).
|
|
327
|
+
3. The same release workflow then builds platform binaries and uploads them to that GitHub release.
|
|
328
|
+
4. npm publish runs only after binary asset upload succeeds (`publish-npm` depends on `publish-assets`).
|
|
329
|
+
5. Published release assets include platform binaries, `facult-install.sh`, and `SHA256SUMS`.
|
|
330
|
+
6. The npm package launcher resolves your platform, downloads the matching release binary, caches it under `~/.facult/runtime/<version>/<platform-arch>/`, and runs it.
|
|
331
|
+
|
|
332
|
+
Current prebuilt binary targets:
|
|
333
|
+
- `darwin-x64`
|
|
334
|
+
- `darwin-arm64`
|
|
335
|
+
- `linux-x64`
|
|
336
|
+
- `windows-x64`
|
|
337
|
+
|
|
338
|
+
Self-update behavior:
|
|
339
|
+
1. npm/bun global install: updates via package manager (`npm install -g facult@...` or `bun add -g facult@...`).
|
|
340
|
+
2. Direct binary install (release script/local binary path): downloads and replaces the binary in place.
|
|
341
|
+
3. Use `facult self-update` (or `facult update --self`).
|
|
342
|
+
|
|
343
|
+
Required secrets for publish:
|
|
344
|
+
- `NPM_TOKEN`
|
|
345
|
+
|
|
346
|
+
Local semantic-release dry-runs require a supported Node runtime (`>=24.10`).
|
|
347
|
+
|
|
348
|
+
Recommended one-time bootstrap before first auto release:
|
|
349
|
+
```bash
|
|
350
|
+
git tag v0.0.0
|
|
351
|
+
git push origin v0.0.0
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
This makes the first semantic-release increment land at `0.0.1` for patch-level changes.
|
|
355
|
+
|
|
356
|
+
## Commit Hygiene
|
|
357
|
+
|
|
358
|
+
Some MCP config files can contain secrets. Keep local generated artifacts and secret-bearing config files ignored and out of commits.
|
|
359
|
+
|
|
360
|
+
## Local Development
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
bun run install:status
|
|
364
|
+
bun run install:dev
|
|
365
|
+
bun run install:bin
|
|
366
|
+
bun run build
|
|
367
|
+
bun run build:verify
|
|
368
|
+
bun run type-check
|
|
369
|
+
bun run test:ci
|
|
370
|
+
bun test
|
|
371
|
+
bun run check
|
|
372
|
+
bun run fix
|
|
373
|
+
bun run pack:dry-run
|
|
374
|
+
bun run release:dry-run
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## FAQ
|
|
378
|
+
|
|
379
|
+
### Does facult run its own MCP server today?
|
|
380
|
+
|
|
381
|
+
Not as a first-party `facult mcp serve` runtime.
|
|
382
|
+
|
|
383
|
+
`facult` currently focuses on inventory, trust/audit, install/update, and managed sync of skills/MCP configs.
|
package/bin/facult.cjs
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const fsp = require("node:fs/promises");
|
|
6
|
+
const https = require("node:https");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
|
+
|
|
11
|
+
const pkg = require("../package.json");
|
|
12
|
+
|
|
13
|
+
const REPO_OWNER = "hack-dance";
|
|
14
|
+
const REPO_NAME = "facult";
|
|
15
|
+
const DOWNLOAD_RETRIES = 12;
|
|
16
|
+
const DOWNLOAD_RETRY_DELAY_MS = 5000;
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const resolved = resolveTarget();
|
|
20
|
+
if (!resolved.ok) {
|
|
21
|
+
console.error(resolved.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const version = String(pkg.version || "").trim();
|
|
26
|
+
if (!version) {
|
|
27
|
+
console.error("Invalid package version.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const home = os.homedir();
|
|
32
|
+
const cacheRoot = path.join(home, ".facult", "runtime");
|
|
33
|
+
const installDir = path.join(
|
|
34
|
+
cacheRoot,
|
|
35
|
+
version,
|
|
36
|
+
`${resolved.platform}-${resolved.arch}`
|
|
37
|
+
);
|
|
38
|
+
const binaryName = resolved.platform === "windows" ? "facult.exe" : "facult";
|
|
39
|
+
const binaryPath = path.join(installDir, binaryName);
|
|
40
|
+
const githubToken = resolveGitHubToken();
|
|
41
|
+
|
|
42
|
+
if (!(await fileExists(binaryPath))) {
|
|
43
|
+
const tag = `v${version}`;
|
|
44
|
+
const assetName = `facult-${version}-${resolved.platform}-${resolved.arch}${resolved.ext}`;
|
|
45
|
+
const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
|
|
46
|
+
|
|
47
|
+
await fsp.mkdir(installDir, { recursive: true });
|
|
48
|
+
const tmpPath = `${binaryPath}.tmp-${Date.now()}`;
|
|
49
|
+
try {
|
|
50
|
+
await downloadWithRetry(url, tmpPath, {
|
|
51
|
+
attempts: DOWNLOAD_RETRIES,
|
|
52
|
+
delayMs: DOWNLOAD_RETRY_DELAY_MS,
|
|
53
|
+
token: githubToken,
|
|
54
|
+
});
|
|
55
|
+
if (resolved.platform !== "windows") {
|
|
56
|
+
await fsp.chmod(tmpPath, 0o755);
|
|
57
|
+
}
|
|
58
|
+
await fsp.rename(tmpPath, binaryPath);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
await safeUnlink(tmpPath);
|
|
61
|
+
const message =
|
|
62
|
+
error instanceof Error ? error.message : String(error ?? "");
|
|
63
|
+
console.error(
|
|
64
|
+
[
|
|
65
|
+
"Unable to download the facult binary for this platform.",
|
|
66
|
+
`Expected asset: ${assetName}`,
|
|
67
|
+
`URL: ${url}`,
|
|
68
|
+
`Reason: ${message}`,
|
|
69
|
+
"",
|
|
70
|
+
"Try installing directly from releases:",
|
|
71
|
+
"https://github.com/hack-dance/facult/releases",
|
|
72
|
+
].join("\n")
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const packageManager = detectPackageManager();
|
|
79
|
+
await writeInstallState({
|
|
80
|
+
method: "npm-binary-cache",
|
|
81
|
+
version,
|
|
82
|
+
binaryPath,
|
|
83
|
+
packageManager,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const args = process.argv.slice(2);
|
|
87
|
+
const result = spawnSync(binaryPath, args, {
|
|
88
|
+
stdio: "inherit",
|
|
89
|
+
env: {
|
|
90
|
+
...process.env,
|
|
91
|
+
FACULT_INSTALL_METHOD: "npm-binary-cache",
|
|
92
|
+
FACULT_NPM_PACKAGE_VERSION: version,
|
|
93
|
+
FACULT_RUNTIME_BINARY: binaryPath,
|
|
94
|
+
FACULT_INSTALL_PM: packageManager,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (typeof result.status === "number") {
|
|
99
|
+
process.exit(result.status);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveTarget() {
|
|
105
|
+
const platform = process.platform;
|
|
106
|
+
const arch = process.arch;
|
|
107
|
+
|
|
108
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
109
|
+
return { ok: true, platform: "darwin", arch: "arm64", ext: "" };
|
|
110
|
+
}
|
|
111
|
+
if (platform === "darwin" && arch === "x64") {
|
|
112
|
+
return { ok: true, platform: "darwin", arch: "x64", ext: "" };
|
|
113
|
+
}
|
|
114
|
+
if (platform === "linux" && arch === "x64") {
|
|
115
|
+
return { ok: true, platform: "linux", arch: "x64", ext: "" };
|
|
116
|
+
}
|
|
117
|
+
if (platform === "win32" && arch === "x64") {
|
|
118
|
+
return { ok: true, platform: "windows", arch: "x64", ext: ".exe" };
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
message: [
|
|
123
|
+
`Unsupported platform/arch: ${platform}/${arch}`,
|
|
124
|
+
"Prebuilt binaries are currently available for:",
|
|
125
|
+
" - darwin/x64",
|
|
126
|
+
" - darwin/arm64",
|
|
127
|
+
" - linux/x64",
|
|
128
|
+
" - windows/x64",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function detectPackageManager() {
|
|
134
|
+
const forced = String(process.env.FACULT_INSTALL_PM || "").trim();
|
|
135
|
+
if (forced === "bun" || forced === "npm") {
|
|
136
|
+
return forced;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const userAgent = String(process.env.npm_config_user_agent || "");
|
|
140
|
+
if (userAgent.startsWith("bun/")) {
|
|
141
|
+
return "bun";
|
|
142
|
+
}
|
|
143
|
+
if (userAgent.startsWith("npm/")) {
|
|
144
|
+
return "npm";
|
|
145
|
+
}
|
|
146
|
+
if (__filename.includes(`${path.sep}.bun${path.sep}`)) {
|
|
147
|
+
return "bun";
|
|
148
|
+
}
|
|
149
|
+
return "npm";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveGitHubToken() {
|
|
153
|
+
const tokenCandidates = [
|
|
154
|
+
process.env.FACULT_GITHUB_TOKEN,
|
|
155
|
+
process.env.GITHUB_TOKEN,
|
|
156
|
+
process.env.GH_TOKEN,
|
|
157
|
+
];
|
|
158
|
+
for (const candidate of tokenCandidates) {
|
|
159
|
+
const token = String(candidate || "").trim();
|
|
160
|
+
if (token) {
|
|
161
|
+
return token;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildRequestHeaders(url, token) {
|
|
168
|
+
const headers = {
|
|
169
|
+
"user-agent": "facult-installer",
|
|
170
|
+
accept: "application/octet-stream",
|
|
171
|
+
};
|
|
172
|
+
if (!token) {
|
|
173
|
+
return headers;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
178
|
+
if (hostname === "github.com" || hostname === "api.github.com") {
|
|
179
|
+
headers.authorization = `Bearer ${token}`;
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Keep default headers if URL parsing fails.
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return headers;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function download(url, destinationPath, options = {}) {
|
|
189
|
+
await new Promise((resolve, reject) => {
|
|
190
|
+
const request = https.get(
|
|
191
|
+
url,
|
|
192
|
+
{
|
|
193
|
+
headers: buildRequestHeaders(url, options.token),
|
|
194
|
+
},
|
|
195
|
+
(response) => {
|
|
196
|
+
if (
|
|
197
|
+
response.statusCode &&
|
|
198
|
+
response.statusCode >= 300 &&
|
|
199
|
+
response.statusCode < 400 &&
|
|
200
|
+
response.headers.location
|
|
201
|
+
) {
|
|
202
|
+
response.resume();
|
|
203
|
+
download(response.headers.location, destinationPath, options)
|
|
204
|
+
.then(resolve)
|
|
205
|
+
.catch(reject);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (response.statusCode !== 200) {
|
|
210
|
+
response.resume();
|
|
211
|
+
reject(
|
|
212
|
+
new Error(
|
|
213
|
+
`HTTP ${response.statusCode ?? "unknown"} while downloading`
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const file = fs.createWriteStream(destinationPath);
|
|
220
|
+
response.pipe(file);
|
|
221
|
+
file.on("finish", () => {
|
|
222
|
+
file.close((err) => {
|
|
223
|
+
if (err) {
|
|
224
|
+
reject(err);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
resolve(undefined);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
file.on("error", (err) => reject(err));
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
request.on("error", (err) => reject(err));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function downloadWithRetry(url, destinationPath, options) {
|
|
239
|
+
let lastError = null;
|
|
240
|
+
for (let attempt = 1; attempt <= options.attempts; attempt += 1) {
|
|
241
|
+
try {
|
|
242
|
+
await safeUnlink(destinationPath);
|
|
243
|
+
await download(url, destinationPath, { token: options.token });
|
|
244
|
+
return;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
lastError = error;
|
|
247
|
+
if (attempt >= options.attempts) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
await sleep(options.delayMs);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw lastError instanceof Error
|
|
254
|
+
? lastError
|
|
255
|
+
: new Error(String(lastError ?? ""));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function sleep(ms) {
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
setTimeout(resolve, ms);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function writeInstallState(state) {
|
|
265
|
+
const home = os.homedir();
|
|
266
|
+
const dir = path.join(home, ".facult");
|
|
267
|
+
const installPath = path.join(dir, "install.json");
|
|
268
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
269
|
+
const payload = {
|
|
270
|
+
version: 1,
|
|
271
|
+
method: state.method,
|
|
272
|
+
installedAt: new Date().toISOString(),
|
|
273
|
+
packageVersion: state.version,
|
|
274
|
+
binaryPath: state.binaryPath,
|
|
275
|
+
source: "npm",
|
|
276
|
+
packageManager: state.packageManager,
|
|
277
|
+
};
|
|
278
|
+
await fsp.writeFile(installPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function fileExists(filePath) {
|
|
282
|
+
try {
|
|
283
|
+
await fsp.stat(filePath);
|
|
284
|
+
return true;
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function safeUnlink(filePath) {
|
|
291
|
+
try {
|
|
292
|
+
await fsp.unlink(filePath);
|
|
293
|
+
} catch {
|
|
294
|
+
// ignore
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
main().catch((error) => {
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
300
|
+
console.error(message);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|