boxbox-plugin 0.0.0-stage → 0.2.7
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 +178 -2
- package/bin/boxbox.mjs +121 -0
- package/package.json +17 -5
- package/payload/bundle.json +8 -0
- package/payload/claude/.claude-plugin/marketplace.json +15 -0
- package/payload/claude/bundle.json +9 -0
- package/payload/claude/claude-registration.mjs +80 -0
- package/payload/claude/codex-registration.mjs +62 -0
- package/payload/claude/install-state.mjs +46 -0
- package/payload/claude/install.mjs +150 -0
- package/payload/claude/packages/boxbox-0.2.7.tgz +0 -0
- package/payload/claude/platform.mjs +106 -0
- package/payload/claude/plugin/.claude-plugin/plugin.json +18 -0
- package/payload/claude/plugin/agents/readability-inspector.md +23 -0
- package/payload/claude/plugin/hooks/hooks.json +54 -0
- package/payload/claude/plugin/scripts/boxbox.mjs +42 -0
- package/payload/claude/plugin/skills/boxbox/SKILL.md +188 -0
- package/payload/claude/plugin/skills/boxbox/references/charter.md +72 -0
- package/payload/claude/plugin/skills/boxbox/references/grammar.md +147 -0
- package/payload/claude/plugin/skills/boxbox/references/readability-contract.md +311 -0
- package/payload/claude/plugin/skills/boxbox/references/typescript-grid-template.md +334 -0
- package/payload/claude-registration.mjs +80 -0
- package/payload/codex/.agents/plugins/marketplace.json +12 -0
- package/payload/codex/bundle.json +9 -0
- package/payload/codex/claude-registration.mjs +80 -0
- package/payload/codex/codex-registration.mjs +62 -0
- package/payload/codex/install-state.mjs +46 -0
- package/payload/codex/install.mjs +150 -0
- package/payload/codex/packages/boxbox-0.2.7.tgz +0 -0
- package/payload/codex/platform.mjs +106 -0
- package/payload/codex/plugin/.codex-plugin/plugin.json +29 -0
- package/payload/codex/plugin/hooks/hooks.json +42 -0
- package/payload/codex/plugin/scripts/boxbox.mjs +42 -0
- package/payload/codex/plugin/skills/boxbox/SKILL.md +188 -0
- package/payload/codex/plugin/skills/boxbox/references/charter.md +72 -0
- package/payload/codex/plugin/skills/boxbox/references/grammar.md +147 -0
- package/payload/codex/plugin/skills/boxbox/references/readability-contract.md +311 -0
- package/payload/codex/plugin/skills/boxbox/references/typescript-grid-template.md +334 -0
- package/payload/codex-registration.mjs +62 -0
- package/payload/install-state.mjs +46 -0
- package/payload/platform.mjs +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Woo-JongHo
|
|
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
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# BoxBox plugin installer for Codex and Claude Code
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The intended public command is **`npx boxbox-plugin`**. The npm package is `boxbox-plugin`; its single executable is `boxbox`, and
|
|
4
|
+
plugin/skill identifiers remain `boxbox`. The package is licensed under MIT and ships its `LICENSE`. Confirm the publisher's registry release before using the public command; a supplied local tarball can be tested before publication. The unrelated npm package `boxbox` is not this installer.
|
|
5
|
+
|
|
6
|
+
## Windows PowerShell prerequisites
|
|
7
|
+
|
|
8
|
+
Install Node.js **22.22.3 or newer**, npm, and at least one host CLI with plugin support: Codex or Claude Code.
|
|
9
|
+
Check `node --version`, `npm.cmd --version`, and `codex.cmd plugin --help` or `claude plugin --help` in a new PowerShell
|
|
10
|
+
terminal. Use `codex` for a native Codex executable. Standard npm-installed Windows Claude Code is detected through its
|
|
11
|
+
native `claude.exe`; no Bash, WSL, or script execution-policy change is required by this installer.
|
|
12
|
+
The core CLI requires no global install, Bun, or source checkout. Python inspection and formatting additionally require a runnable `python3`.
|
|
13
|
+
|
|
14
|
+
If `CODEX_HOME` is set for a Codex installation, it must already be an existing directory. `CLAUDE_CONFIG_DIR` is passed to
|
|
15
|
+
Claude unchanged. Existing host configuration is preserved through normal plugin-management commands. The installer never
|
|
16
|
+
edits hook trust or grants it automatically.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
After an authorized public release:
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
npx.cmd boxbox-plugin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
With no arguments, the installer detects installed Codex and Claude Code CLIs and installs for every detected host. It
|
|
27
|
+
fails with instructions if neither is found. Select a particular host explicitly when needed:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
npx.cmd boxbox-plugin --codex
|
|
31
|
+
npx.cmd boxbox-plugin --claude
|
|
32
|
+
npx.cmd boxbox-plugin --codex --claude
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To test a local candidate before publication, use the absolute installer tarball path instead of the registry:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
npm.cmd exec --yes --package 'C:\path\to\boxbox-plugin-<version>.tgz' -- boxbox
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## If you added the marketplace first
|
|
42
|
+
|
|
43
|
+
Adding this repository as a Claude or Codex marketplace installs the plugin without an engine. The installer refuses to replace a same-named marketplace from another source, so remove that registration first, then rerun the installer. Removing a marketplace also removes its plugins, so only do this for a registration that points at this repository. Run the lines for your host:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
claude plugin uninstall boxbox@boxbox-plugins --scope user
|
|
47
|
+
claude plugin marketplace remove boxbox-plugins
|
|
48
|
+
codex plugin marketplace remove boxbox-plugins
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Removing an earlier HUD
|
|
52
|
+
|
|
53
|
+
The opt-in terminal HUD (`--hud`) was removed after 0.2.4. If an earlier installation enabled it, disable it with
|
|
54
|
+
that installation's permanent plugin directory, which the new installer leaves in place:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
node '/absolute/old/installed/plugin/scripts/hud/setup.mjs' claude '/absolute/old/installed/plugin' --disable
|
|
58
|
+
node '/absolute/old/installed/plugin/scripts/hud/setup.mjs' codex '/absolute/old/installed/plugin' --disable
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Disable removes only a still-owned status-line key or launcher. Unrelated settings and later user changes are preserved.
|
|
62
|
+
|
|
63
|
+
## Updating a local Codex candidate from an earlier version
|
|
64
|
+
|
|
65
|
+
If the old installation enabled the Codex HUD, disable it first (see above), then run the new installer:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
npm.cmd exec --yes --package 'C:\path\to\boxbox-plugin-<version>.tgz' -- boxbox --codex
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use your installed OpenAI Codex CLI (`codex.cmd` is the standard Windows npm entry). For an installer-owned older
|
|
72
|
+
BoxBox marketplace, the installer verifies the previous bundle, removes its plugin and marketplace registration, and
|
|
73
|
+
registers the new versioned source. A failed transition attempts to restore the previous registration. A same-named
|
|
74
|
+
foreign marketplace is refused without removal.
|
|
75
|
+
|
|
76
|
+
The payload is copied out of npm's temporary cache into permanent, separate host directories before registration:
|
|
77
|
+
|
|
78
|
+
- Windows: `%LOCALAPPDATA%\BoxBox\Codex\<version>` and `%LOCALAPPDATA%\BoxBox\Claude\<version>`
|
|
79
|
+
- macOS/Linux: `$XDG_DATA_HOME/boxbox/<host>/<version>`, or `~/.local/share/boxbox/<host>/<version>` when unset
|
|
80
|
+
|
|
81
|
+
Each selected host installs the included private `boxbox` engine tarball with `--ignore-scripts --no-audit --no-fund`.
|
|
82
|
+
Before engine installation or host registration, each copied host payload verifies that tarball against the SHA-256 in its bundle metadata.
|
|
83
|
+
Missing, malformed or mismatched integrity metadata stops installation with instructions to reinstall the installer
|
|
84
|
+
package. This detects mixed archive bytes inside a payload; it is not a publisher signature. The installed CLI version
|
|
85
|
+
is checked separately. The nested engine never comes from the npm registry. Pinned `typescript@7.0.2` and its native platform dependencies still
|
|
86
|
+
need internet access or a primed npm cache. Installed plugins survive npm cache cleanup or deletion of the original
|
|
87
|
+
installer tarball. Rerunning the same version repairs shipped files and runtime while preserving other data in that folder.
|
|
88
|
+
If repair fails before host registration starts, the previous directory is restored. Once registration may have
|
|
89
|
+
changed, a failure retains the new directory, its `.boxbox-install-state.json` phase receipt and any previous backup;
|
|
90
|
+
the error prints their exact paths. This prevents a host registration from pointing at a deleted directory. Inspect
|
|
91
|
+
the host plugin and marketplace lists before recovery. Rerunning the exact installer deliberately retries a valid
|
|
92
|
+
receipt belonging to that same root/version/host; its replacement backup is removed only after verified success. Foreign,
|
|
93
|
+
malformed or symlink receipts are refused without replacement. Do not delete retained directories while a host still
|
|
94
|
+
references them; after removing the exact registration, restore the printed backup or reinstall. This is partial-state
|
|
95
|
+
retention, not an automatic rollback of host settings. Successful retries do not delete backups retained by earlier runs.
|
|
96
|
+
|
|
97
|
+
For an installer-owned local Claude marketplace, installation checks the exact user-scope plugin identity and
|
|
98
|
+
refreshes stale recorded versions with `claude plugin update boxbox@boxbox-plugins --scope user`. Completion requires
|
|
99
|
+
the intended marketplace root, version, cached manifest and engine paths to agree. A foreign/shared marketplace or
|
|
100
|
+
ambiguous plugin scope is refused. Older installer archives do not gain these checks: use the documented exact plugin
|
|
101
|
+
and marketplace removal commands below before reinstalling an older archive for rollback. Restart Claude after a change.
|
|
102
|
+
|
|
103
|
+
Restart each selected host. In Codex, select the BoxBox `boxbox` skill in the picker; in Claude Code, select the installed
|
|
104
|
+
BoxBox skill from its skill list. Name one TypeScript or Python file and request a read-only check first. Review the
|
|
105
|
+
three hook definitions in the host before relying on them. Codex skips untrusted plugin hooks: use `/hooks` to review and
|
|
106
|
+
explicitly trust their exact current definitions. Claude Code uses its own hook/settings review workflow.
|
|
107
|
+
|
|
108
|
+
Codex hooks use the documented Windows PowerShell override. Claude hooks use direct `command` plus `args` execution,
|
|
109
|
+
including variable expansion of `${CLAUDE_PLUGIN_ROOT}`, avoiding Windows shell parsing. Both adapters default to
|
|
110
|
+
read-only inspection, store receipts in host plugin data, and refuse a hook when the host omits its data directory.
|
|
111
|
+
An explicit `boxbox mode align` enables only approved horizontal alignment after matched PostToolUse calls, followed
|
|
112
|
+
by reinspection; `check` and `refactor` keep hooks read-only, `off` skips inspection, and Stop never writes source.
|
|
113
|
+
PostToolUse checks all session changes after matched file-edit or shell calls, including read-only shell calls;
|
|
114
|
+
it does not infer which command wrote a file. Codex exposes `exec_command` and a completed `write_stdin` through the
|
|
115
|
+
hook name `Bash`. Shell feedback is advisory: inspection failures add context while preserving the command's output
|
|
116
|
+
and exit status, so guidance, previews and tests remain readable. File-edit feedback retains its existing blocking
|
|
117
|
+
behavior. Stop checks session changes again and requests at most one continuation. Other tool paths may not match.
|
|
118
|
+
A receipt with `NO_GROUPS` means
|
|
119
|
+
the file was inspected but no supported table groups were found, rather than whole-file alignment coverage.
|
|
120
|
+
Claude's generated Windows engine commands use a Node bridge that works from Bash or PowerShell and preserves paths and
|
|
121
|
+
arguments literally, so no shell switch is needed.
|
|
122
|
+
The Claude plugin also includes the canonical read-only inspector; the Codex plugin does not include an agent.
|
|
123
|
+
Native Windows execution must still be verified on the destination computer.
|
|
124
|
+
|
|
125
|
+
The recommended preset includes approved TypeScript top-level variable declarations of calls and simple values (2+ rows) and Python
|
|
126
|
+
simple name assignments (3+ rows at one indentation). No custom formatter or approval DB is required for those exact
|
|
127
|
+
patterns. Other patterns remain read-only until approved. Use `check --json`, `guide --json`, then `format --dry-run`
|
|
128
|
+
before an explicit `format` request. A positive approved-group count does not bypass the whole-file approval gate.
|
|
129
|
+
Use `boxbox mode` to read the current project mode, or `boxbox mode check --global` to set the global default. A valid
|
|
130
|
+
`BOXBOX_MODE` overrides project and global state for the current process.
|
|
131
|
+
|
|
132
|
+
Formatters such as Prettier, Black and Ruff revert BoxBox alignment in the files they rewrite. `doctor`, `init`, `align`
|
|
133
|
+
and `format` warn when a project-root configuration shows one, distinguishing `runs` from `possible`. Configure the
|
|
134
|
+
formatter for its language and invoke BoxBox from editor, pre-commit and CI commands. `check` and `format` accept
|
|
135
|
+
repeated `--file` or bounded recursive `--directory`; Align remains single-file. Configured-formatter Check and
|
|
136
|
+
`format --dry-run` execute the configured program on working copies, keep original source bytes intact, and require
|
|
137
|
+
the same approval/parser/fixed-point gates as Format. Check compares the final formatter-then-BoxBox bytes with the
|
|
138
|
+
original, keeping source findings separate from `canonical` verification. Batch formats are atomic per file and
|
|
139
|
+
retain earlier completed writes when another file fails; per-group selective alignment remains deferred.
|
|
140
|
+
|
|
141
|
+
`boxbox update --check` requests an update check and reports a non-success result when the release query fails.
|
|
142
|
+
After registry publication, rerun `npx boxbox-plugin` to update detected hosts to the published installer. The
|
|
143
|
+
`boxbox update --source <checkout>` path is a development-only override that requires an absolute BoxBox checkout
|
|
144
|
+
and its build prerequisites; an installed launcher infers its host, while a standalone call must pass `--host`.
|
|
145
|
+
Success requires the selected host's installed engine to report the intended version, followed by a host restart.
|
|
146
|
+
Session notices never install updates themselves. `boxbox statusline --install-shim` prints an optional
|
|
147
|
+
version-independent Claude status-line command; `NO_COLOR` or `BOXBOX_COLOR=0` disables color.
|
|
148
|
+
|
|
149
|
+
Run `monitor --project --host auto --json` through the host's installed launcher/environment for an on-demand report.
|
|
150
|
+
The command requires absolute `CLAUDE_PLUGIN_DATA` or `PLUGIN_DATA` and always writes a host HTML page there, including in JSON
|
|
151
|
+
mode. `--html <absolute-path>` replaces the default destination inside that folder; project or outside-folder destinations are refused.
|
|
152
|
+
Missing plugin data produces setup guidance. `--no-paths` hides source filenames, while the returned output path stays
|
|
153
|
+
visible. No transcript text, commands, source content, model call or dollar cost belongs to this report. Legacy
|
|
154
|
+
footprints and exact Codex step attribution may be unavailable and remain labeled.
|
|
155
|
+
|
|
156
|
+
External formatters are optional. On Windows use a native executable or a Node JS entry point (`node.exe` plus script
|
|
157
|
+
and `{file}` arguments). Arbitrary `.cmd`/`.bat` formatters are refused with configuration guidance; BoxBox never
|
|
158
|
+
interpolates formatter arguments into a shell. A configured formatter failure never falls back silently to standalone mode.
|
|
159
|
+
`boxbox --help` prints usage without installing. `boxbox --engine-only` is for diagnosis: it prepares only the Codex
|
|
160
|
+
permanent local engine and skips all host checks and registration. It cannot be combined with host selection flags.
|
|
161
|
+
|
|
162
|
+
## Remove
|
|
163
|
+
|
|
164
|
+
Use the same host configuration environment as installation, and remove each installed adapter separately:
|
|
165
|
+
|
|
166
|
+
```powershell
|
|
167
|
+
codex.cmd plugin remove boxbox@boxbox-plugins
|
|
168
|
+
codex.cmd plugin marketplace remove boxbox-plugins
|
|
169
|
+
claude plugin uninstall boxbox@boxbox-plugins --scope user
|
|
170
|
+
claude plugin marketplace remove boxbox-plugins
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use `codex` for a native executable. Restart the host. Once its removal succeeds, its version directory above can be
|
|
174
|
+
deleted if desired. Keep a marketplace used by other plugins. Project `.boxbox/` approvals and personal data are not
|
|
175
|
+
removed. On macOS/Linux use `npx`, `npm exec`, `codex`, `claude`, and the corresponding data directory.
|
|
176
|
+
|
|
177
|
+
Official contracts: [Codex packaging](https://developers.openai.com/plugins/build/plugins),
|
|
178
|
+
[Codex hooks](https://learn.chatgpt.com/docs/hooks), [Claude marketplaces](https://code.claude.com/docs/en/plugin-marketplaces),
|
|
179
|
+
and [Claude hooks](https://code.claude.com/docs/en/hooks).
|
package/bin/boxbox.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process" ;
|
|
3
|
+
import {
|
|
4
|
+
cpSync ,
|
|
5
|
+
existsSync ,
|
|
6
|
+
mkdirSync ,
|
|
7
|
+
mkdtempSync ,
|
|
8
|
+
readFileSync ,
|
|
9
|
+
rmSync ,
|
|
10
|
+
statSync ,
|
|
11
|
+
} from "node:fs" ;
|
|
12
|
+
import { dirname, join } from "node:path" ;
|
|
13
|
+
import { fileURLToPath } from "node:url" ;
|
|
14
|
+
import { findCommand, installationRoot, supportedNode } from "../payload/platform.mjs" ;
|
|
15
|
+
|
|
16
|
+
import { installationState } from "../payload/install-state.mjs" ;
|
|
17
|
+
|
|
18
|
+
import { assertClaudeCatalog } from "../payload/claude-registration.mjs" ;
|
|
19
|
+
|
|
20
|
+
/** Restore only failures proven to precede host mutation; retain uncertain state for recovery. */
|
|
21
|
+
function installHost(payload, destination, engineOnly) {
|
|
22
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
23
|
+
const metadata = JSON.parse(readFileSync(join(payload, "bundle.json"), "utf8")) ;
|
|
24
|
+
const previousState = existsSync(destination) ? installationState(destination, metadata) : undefined ;
|
|
25
|
+
const retained = previousState?.read() ;
|
|
26
|
+
if (metadata.host === "claude" && existsSync(destination)) assertClaudeCatalog(destination, metadata, true);
|
|
27
|
+
const backup = existsSync(destination) ? mkdtempSync(join(dirname(destination), ".boxbox-repair-")) : undefined ;
|
|
28
|
+
if (backup) {
|
|
29
|
+
try {
|
|
30
|
+
cpSync(destination, join(backup, "previous"), { recursive: true });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
try {
|
|
33
|
+
rmSync(backup, { recursive: true, force: true });
|
|
34
|
+
} catch (cleanupError) {
|
|
35
|
+
throw new Error(`Backup preparation failed; partial backup retained at ${backup}: ${error.message}. Cleanup failed: ${cleanupError.message}`);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let succeeded = false ;
|
|
41
|
+
let launched = false ;
|
|
42
|
+
let state;
|
|
43
|
+
let exitStatus;
|
|
44
|
+
let signaled = false;
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(destination, { recursive: true });
|
|
47
|
+
if (retained) previousState.remove();
|
|
48
|
+
cpSync(payload, destination, { recursive: true });
|
|
49
|
+
state = installationState(destination, metadata);
|
|
50
|
+
state.create();
|
|
51
|
+
launched = true;
|
|
52
|
+
const child = spawnSync(process.execPath, [join(destination, "install.mjs"), ...(engineOnly ? ["--engine-only"] : [])], { stdio: "inherit" });
|
|
53
|
+
exitStatus = child.status;
|
|
54
|
+
signaled = Boolean(child.signal);
|
|
55
|
+
if (child.error) throw child.error;
|
|
56
|
+
if (child.signal) throw new Error(`Installer terminated by ${child.signal}`);
|
|
57
|
+
succeeded = child.status === 0 && state.read()?.phase === "verified";
|
|
58
|
+
if (child.status === 0 && !succeeded) throw new Error("Installer exited without verified completion.");
|
|
59
|
+
return child.status ?? 1;
|
|
60
|
+
} finally {
|
|
61
|
+
let prepared = !launched;
|
|
62
|
+
if (launched && !succeeded) {
|
|
63
|
+
try { prepared = !signaled && exitStatus !== 0 && state.read()?.phase === "prepared"; } catch { prepared = false; }
|
|
64
|
+
}
|
|
65
|
+
if (!succeeded && !prepared) {
|
|
66
|
+
console.error(`[BoxBox] Installation may have changed host registration. Retained installation: ${destination}. Receipt: ${state?.path}.${backup ? ` Previous backup retained at ${backup}.` : ""} Inspect host plugin/marketplace lists, then rerun this exact installer or remove the exact registration before manual recovery.`);
|
|
67
|
+
} else {
|
|
68
|
+
if (!succeeded) {
|
|
69
|
+
try {
|
|
70
|
+
rmSync(destination, { recursive: true, force: true });
|
|
71
|
+
if (backup) cpSync(join(backup, "previous"), destination, { recursive: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(backup ? `Repair could not restore the previous installation. Backup retained at ${backup}: ${error.message}` : `Failed installation cleanup was incomplete at ${destination}: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (succeeded) state.remove();
|
|
77
|
+
if (backup) {
|
|
78
|
+
try {
|
|
79
|
+
rmSync(backup, { recursive: true, force: true });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`[BoxBox] Previous backup could not be removed; retained at ${backup}: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const arguments_ = process.argv.slice(2) ;
|
|
89
|
+
const usage = "Usage: boxbox [--codex] [--claude] | --engine-only | --help" ;
|
|
90
|
+
if (arguments_.length === 1 && arguments_[0] === "--help") {
|
|
91
|
+
console.log(`${usage}\n\nInstall BoxBox plugins for detected Codex and Claude Code CLIs.\nUse --codex or --claude to select hosts explicitly; both flags select both.\n--engine-only prepares the Codex local engine without host checks or registration.\nRestart each host afterward and review its hooks manually.`);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
const engineOnly = arguments_.length === 1 && arguments_[0] === "--engine-only";
|
|
95
|
+
if (!engineOnly && arguments_.some(value => !["--codex", "--claude"].includes(value))) {
|
|
96
|
+
console.error(`[BoxBox] ${usage}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
if (!supportedNode(process.versions.node)) throw new Error(`Node.js >=22.22.3 is required; found ${process.versions.node}. Install a supported Node.js release and rerun the installer.`);
|
|
102
|
+
const requested = [...new Set(arguments_.map(value => value.slice(2)))] ;
|
|
103
|
+
const hosts = engineOnly ? ["codex"] : requested.length ? requested : ["codex", "claude"].filter(host => findCommand(host)) ;
|
|
104
|
+
if (hosts.length === 0) throw new Error("Neither Codex nor Claude Code was found on PATH. Install at least one CLI with plugin support, open a new terminal, then rerun the BoxBox installer.");
|
|
105
|
+
if (!engineOnly) {
|
|
106
|
+
for (const host of hosts) {
|
|
107
|
+
if (!findCommand(host)) throw new Error(`${host === "codex" ? "Codex" : "Claude Code"} was not found on PATH. Install that CLI or select another installed host with --codex or --claude.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!engineOnly && hosts.includes("codex") && process.env.CODEX_HOME && (!existsSync(process.env.CODEX_HOME) || !statSync(process.env.CODEX_HOME).isDirectory())) throw new Error("CODEX_HOME must point to an existing directory. Create that directory or unset CODEX_HOME before rerunning the installer.");
|
|
111
|
+
const payload = fileURLToPath(new URL("../payload/", import.meta.url) ) ;
|
|
112
|
+
const metadata = JSON.parse (readFileSync(join(payload, "bundle.json"), "utf8")) ;
|
|
113
|
+
for (const host of hosts) {
|
|
114
|
+
const destination = installationRoot(metadata.version, host ) ;
|
|
115
|
+
const status = installHost (join(payload, host), destination, engineOnly) ;
|
|
116
|
+
if (status !== 0) process.exit(status);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`[BoxBox] ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "boxbox-plugin",
|
|
3
|
+
"version": "0.2.7",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "BoxBox plugin installer for Codex and Claude Code",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.22.3"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"boxbox": "bin/boxbox.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/boxbox.mjs",
|
|
15
|
+
"payload/",
|
|
16
|
+
"README.md"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "boxbox-plugins",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Woo-JongHo"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name" : "boxbox",
|
|
9
|
+
"source" : "./plugin",
|
|
10
|
+
"description": "Checks TypeScript, Python, JSON and YAML syntax and tabular readability one file at a time, with human-approved whitespace alignment and a read-only inspector.",
|
|
11
|
+
"version" : "0.2.7"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"description": "BoxBox: tabular readability checks for TypeScript, Python, JSON and YAML"
|
|
15
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs" ;
|
|
2
|
+
import { dirname, join, resolve } from "node:path" ;
|
|
3
|
+
|
|
4
|
+
const UPSTREAM = /^(?:https:\/\/github\.com\/|git@github\.com:)?Woo-JongHo\/boxbox(?:\.git)?\/?$/iu;
|
|
5
|
+
|
|
6
|
+
/** Check before repair copies can erase evidence of a foreign or shared catalog. */
|
|
7
|
+
export function assertClaudeCatalog(root, metadata, allowMissing = false) {
|
|
8
|
+
const directory = join(root , ".claude-plugin" ) ;
|
|
9
|
+
const path = join(directory, "marketplace.json") ;
|
|
10
|
+
for (const [target, isDirectory] of [[directory, true], [path, false]]) {
|
|
11
|
+
let stat;
|
|
12
|
+
try { stat = lstatSync(target); } catch (error) {
|
|
13
|
+
if (allowMissing && error.code === "ENOENT") return;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
if (stat.isSymbolicLink() || (isDirectory ? !stat.isDirectory() : !stat.isFile())) throw new Error(`Refusing non-regular Claude marketplace catalog: ${target}`);
|
|
17
|
+
}
|
|
18
|
+
const catalog = JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
if (catalog.name !== metadata.marketplace || !Array.isArray(catalog.plugins) || catalog.plugins.length !== 1 || catalog.plugins[0].name !== metadata.name || catalog.plugins[0].source !== "./plugin") throw new Error("Refusing a foreign or shared Claude marketplace catalog.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Bind updates to the installer-owned local marketplace, never a same-named foreign source. */
|
|
23
|
+
export function claudeRegistration(root, metadata, invoke) {
|
|
24
|
+
const intended = realpathSync(root) ;
|
|
25
|
+
const id = `${metadata.name}@${metadata.marketplace}` ;
|
|
26
|
+
function json(args) {
|
|
27
|
+
const value = JSON.parse(invoke(args, { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }).stdout);
|
|
28
|
+
if (!Array.isArray(value)) throw new Error(`Unexpected Claude list schema for ${args.join(" ")}`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function marketplace(exact) {
|
|
32
|
+
const entries = json(["plugin", "marketplace", "list", "--json"]).filter(value => value.name === metadata.marketplace);
|
|
33
|
+
if (!entries.length && !exact) return;
|
|
34
|
+
if (!exact && entries.length === 1 && entries[0].source !== "directory") {
|
|
35
|
+
const shared = json(["plugin", "list", "--json"]).some(value => typeof value.id === "string" && value.id.endsWith(`@${metadata.marketplace}`) && value.id !== id);
|
|
36
|
+
if (UPSTREAM.test(entries[0].repo ?? entries[0].url ?? "") && !shared) throw new Error(`Claude marketplace ${metadata.marketplace} was added from this project's repository (${entries[0].source}), which has no engine. Remove it, then rerun the BoxBox installer:\n claude plugin uninstall ${id} --scope user\n claude plugin marketplace remove ${metadata.marketplace}`);
|
|
37
|
+
throw new Error(`A different Claude marketplace is already named ${metadata.marketplace}, or it has other plugins. Removing a marketplace also removes its plugins, so BoxBox will not suggest it. Inspect it with "claude plugin marketplace list" and decide before rerunning the installer.`);
|
|
38
|
+
}
|
|
39
|
+
if (entries.length !== 1 || entries[0].source !== "directory" || typeof entries[0].path !== "string" || typeof entries[0].installLocation !== "string") throw new Error("Claude marketplace is missing, ambiguous or not an owned local directory.");
|
|
40
|
+
const source = realpathSync(entries[0].path);
|
|
41
|
+
if (realpathSync(entries[0].installLocation) !== source) throw new Error("Claude marketplace installLocation does not match its source.");
|
|
42
|
+
if (exact && source !== intended) throw new Error("Claude marketplace did not select the intended installation root.");
|
|
43
|
+
if (dirname(source) !== dirname(intended)) throw new Error("Refusing to replace a foreign Claude marketplace.");
|
|
44
|
+
const bundle = JSON.parse(readFileSync(join(source, "bundle.json"), "utf8")) ;
|
|
45
|
+
const manifest = JSON.parse(readFileSync(join(source, "plugin/.claude-plugin/plugin.json"), "utf8")) ;
|
|
46
|
+
if (bundle.name !== metadata.name || bundle.host !== "claude" || bundle.marketplace !== metadata.marketplace || manifest.name !== metadata.name || manifest.version !== bundle.version || source !== realpathSync(resolve(dirname(intended), bundle.version))) throw new Error("Claude marketplace ownership could not be established.");
|
|
47
|
+
assertClaudeCatalog(source, metadata);
|
|
48
|
+
return source;
|
|
49
|
+
}
|
|
50
|
+
function plugin(required) {
|
|
51
|
+
const all = json(["plugin", "list", "--json"]);
|
|
52
|
+
if (all.some(value => typeof value.id === "string" && value.id.endsWith(`@${metadata.marketplace}`) && value.id !== id)) throw new Error("Refusing to replace a Claude marketplace shared with another plugin.");
|
|
53
|
+
const entries = all.filter(value => value.id === id);
|
|
54
|
+
if (!entries.length && !required) return;
|
|
55
|
+
if (entries.length !== 1 || entries[0].scope !== "user" || entries[0].enabled !== true || typeof entries[0].installPath !== "string") throw new Error("Claude plugin identity is missing, ambiguous, disabled or outside user scope.");
|
|
56
|
+
return entries[0];
|
|
57
|
+
}
|
|
58
|
+
function preflight() {
|
|
59
|
+
const help = invoke(["plugin", "update", "--help"], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }).stdout;
|
|
60
|
+
if (!/(?:^|\s)--scope(?:[\s=,]|$)/u.test(help)) throw new Error("Claude plugin update does not advertise --scope support; upgrade Claude before installation.");
|
|
61
|
+
const source = marketplace(false);
|
|
62
|
+
if (plugin(false) && !source) throw new Error("Existing Claude plugin has no owned marketplace registration.");
|
|
63
|
+
}
|
|
64
|
+
function verify() {
|
|
65
|
+
marketplace(true);
|
|
66
|
+
let installed = plugin(true);
|
|
67
|
+
if (installed.version !== metadata.version) {
|
|
68
|
+
invoke(["plugin", "update", id, "--scope", "user"]);
|
|
69
|
+
marketplace(true);
|
|
70
|
+
installed = plugin(true);
|
|
71
|
+
}
|
|
72
|
+
if (installed.version !== metadata.version) throw new Error("Claude retained the wrong plugin version after registration.");
|
|
73
|
+
const cached = realpathSync(installed.installPath ) ;
|
|
74
|
+
const manifest = JSON.parse (readFileSync(join(cached, ".claude-plugin/plugin.json"), "utf8")) ;
|
|
75
|
+
const runtime = JSON.parse (readFileSync(join(cached, "runtime.json"), "utf8") ) ;
|
|
76
|
+
const expected = JSON.parse (readFileSync(join(intended, "plugin/runtime.json"), "utf8") ) ;
|
|
77
|
+
if (manifest.name !== metadata.name || manifest.version !== metadata.version || realpathSync(runtime.cli) !== realpathSync(expected.cli) || realpathSync(runtime.node) !== realpathSync(expected.node)) throw new Error("Claude cached plugin identity or runtime does not match the intended installation.");
|
|
78
|
+
}
|
|
79
|
+
return { preflight, marketplace: () => marketplace(true), verify };
|
|
80
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs" ;
|
|
2
|
+
import { dirname, resolve } from "node:path" ;
|
|
3
|
+
|
|
4
|
+
function registrationSource(entry) {
|
|
5
|
+
const value = entry?.root ?? entry?.marketplaceSource?.source;
|
|
6
|
+
return typeof value === "string" ? resolve(value) : undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function codexRegistration(root, metadata, invoke) {
|
|
10
|
+
const selector = `${metadata.name}@${metadata.marketplace}`;
|
|
11
|
+
|
|
12
|
+
function json(args) {
|
|
13
|
+
const output = invoke(args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).stdout;
|
|
14
|
+
return JSON.parse(output);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function inspect() {
|
|
18
|
+
const marketplaces = json(["plugin", "marketplace", "list", "--json"]);
|
|
19
|
+
if (!Array.isArray(marketplaces?.marketplaces)) throw new Error("Unexpected Codex marketplace list schema.");
|
|
20
|
+
const matches = marketplaces.marketplaces.filter(entry => entry?.name === metadata.marketplace);
|
|
21
|
+
if (matches.length > 1) throw new Error("Multiple Codex BoxBox marketplace registrations were found; remove the duplicates before installation.");
|
|
22
|
+
if (matches.length === 0) return { previous: undefined, installed: false };
|
|
23
|
+
const previous = registrationSource(matches[0]);
|
|
24
|
+
if (!previous) throw new Error("Existing Codex BoxBox marketplace has no local source path.");
|
|
25
|
+
if (previous !== resolve(root)) {
|
|
26
|
+
if (!existsSync(previous) || dirname(realpathSync(previous)) !== realpathSync(dirname(root))) throw new Error("Existing Codex BoxBox marketplace is not owned by this installer; remove it manually before installation.");
|
|
27
|
+
const bundle = JSON.parse(readFileSync(resolve(previous, "bundle.json"), "utf8"));
|
|
28
|
+
if (bundle.host !== "codex" || bundle.name !== metadata.name || bundle.marketplace !== metadata.marketplace) throw new Error("Existing Codex BoxBox marketplace ownership metadata does not match this installer.");
|
|
29
|
+
}
|
|
30
|
+
const plugins = json(["plugin", "list", "--json"]);
|
|
31
|
+
if (!Array.isArray(plugins?.installed)) throw new Error("Unexpected Codex plugin list schema.");
|
|
32
|
+
return { previous, installed: plugins.installed.some(entry => entry?.pluginId === selector && entry?.installed === true) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let state;
|
|
36
|
+
return {
|
|
37
|
+
preflight() { state = inspect(); },
|
|
38
|
+
transition() {
|
|
39
|
+
state ??= inspect();
|
|
40
|
+
if (state.previous === resolve(root)) {
|
|
41
|
+
if (!state.installed) invoke(["plugin", "add", selector]);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (state.previous) {
|
|
45
|
+
if (state.installed) invoke(["plugin", "remove", selector]);
|
|
46
|
+
invoke(["plugin", "marketplace", "remove", metadata.marketplace]);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
invoke(["plugin", "marketplace", "add", root]);
|
|
50
|
+
invoke(["plugin", "add", selector]);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (state.previous) {
|
|
53
|
+
try {
|
|
54
|
+
invoke(["plugin", "marketplace", "add", state.previous]);
|
|
55
|
+
if (state.installed) invoke(["plugin", "add", selector]);
|
|
56
|
+
} catch { /* Preserve the original transition error; the installer receipt names manual recovery. */ }
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstatSync ,
|
|
3
|
+
readFileSync ,
|
|
4
|
+
realpathSync ,
|
|
5
|
+
renameSync ,
|
|
6
|
+
unlinkSync ,
|
|
7
|
+
writeFileSync ,
|
|
8
|
+
} from "node:fs" ;
|
|
9
|
+
import { join } from "node:path" ;
|
|
10
|
+
|
|
11
|
+
const FILE = ".boxbox-install-state.json" ;
|
|
12
|
+
const PHASES = ["prepared", "host-registration-started", "verified"] ;
|
|
13
|
+
|
|
14
|
+
/** A receipt authorizes cleanup only before any host command can change registration. */
|
|
15
|
+
export function installationState(root, metadata) {
|
|
16
|
+
const path = join(root, FILE) ;
|
|
17
|
+
const identity = { schema: 1, root: realpathSync(root), name: metadata.name, version: metadata.version, host: metadata.host, marketplace: metadata.marketplace } ;
|
|
18
|
+
function read() {
|
|
19
|
+
let stat;
|
|
20
|
+
try { stat = lstatSync(path); } catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return undefined;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Reserved installation receipt is not a regular file: ${path}`);
|
|
25
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
if (!value || Object.entries(identity).some(([key, expected]) => value[key] !== expected) || !PHASES.includes(value.phase)) throw new Error(`Invalid installation receipt: ${path}`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function create() {
|
|
30
|
+
writeFileSync(path, JSON.stringify({ ...identity, phase: "prepared" }) + "\n", { flag: "wx", mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
function advance(phase) {
|
|
33
|
+
const previous = read();
|
|
34
|
+
if (!previous || !["host-registration-started", "verified"].includes(phase) || previous.phase !== "prepared" && !(previous.phase === "host-registration-started" && phase === "verified")) throw new Error(`Invalid installation phase transition at ${path}`);
|
|
35
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
36
|
+
writeFileSync(temporary, JSON.stringify({ ...identity, phase }) + "\n", { flag: "wx", mode: 0o600 });
|
|
37
|
+
try { renameSync(temporary, path); } finally {
|
|
38
|
+
try { unlinkSync(temporary); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function remove() {
|
|
42
|
+
if (!read()) throw new Error(`Missing installation receipt: ${path}`);
|
|
43
|
+
unlinkSync(path);
|
|
44
|
+
}
|
|
45
|
+
return { path, read, create, advance, remove };
|
|
46
|
+
}
|