boxbox-plugin 0.0.0-stage → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -2
  3. package/bin/boxbox.mjs +121 -0
  4. package/package.json +17 -5
  5. package/payload/bundle.json +8 -0
  6. package/payload/claude/.claude-plugin/marketplace.json +15 -0
  7. package/payload/claude/bundle.json +9 -0
  8. package/payload/claude/claude-registration.mjs +80 -0
  9. package/payload/claude/codex-registration.mjs +62 -0
  10. package/payload/claude/install-state.mjs +46 -0
  11. package/payload/claude/install.mjs +150 -0
  12. package/payload/claude/packages/boxbox-0.2.8.tgz +0 -0
  13. package/payload/claude/platform.mjs +107 -0
  14. package/payload/claude/plugin/.claude-plugin/plugin.json +18 -0
  15. package/payload/claude/plugin/agents/readability-inspector.md +23 -0
  16. package/payload/claude/plugin/hooks/hooks.json +54 -0
  17. package/payload/claude/plugin/scripts/boxbox.mjs +42 -0
  18. package/payload/claude/plugin/skills/boxbox/SKILL.md +188 -0
  19. package/payload/claude/plugin/skills/boxbox/references/charter.md +72 -0
  20. package/payload/claude/plugin/skills/boxbox/references/grammar.md +147 -0
  21. package/payload/claude/plugin/skills/boxbox/references/readability-contract.md +311 -0
  22. package/payload/claude/plugin/skills/boxbox/references/typescript-grid-template.md +334 -0
  23. package/payload/claude-registration.mjs +80 -0
  24. package/payload/codex/.agents/plugins/marketplace.json +12 -0
  25. package/payload/codex/bundle.json +9 -0
  26. package/payload/codex/claude-registration.mjs +80 -0
  27. package/payload/codex/codex-registration.mjs +62 -0
  28. package/payload/codex/install-state.mjs +46 -0
  29. package/payload/codex/install.mjs +150 -0
  30. package/payload/codex/packages/boxbox-0.2.8.tgz +0 -0
  31. package/payload/codex/platform.mjs +107 -0
  32. package/payload/codex/plugin/.codex-plugin/plugin.json +29 -0
  33. package/payload/codex/plugin/hooks/hooks.json +42 -0
  34. package/payload/codex/plugin/scripts/boxbox.mjs +42 -0
  35. package/payload/codex/plugin/skills/boxbox/SKILL.md +188 -0
  36. package/payload/codex/plugin/skills/boxbox/references/charter.md +72 -0
  37. package/payload/codex/plugin/skills/boxbox/references/grammar.md +147 -0
  38. package/payload/codex/plugin/skills/boxbox/references/readability-contract.md +311 -0
  39. package/payload/codex/plugin/skills/boxbox/references/typescript-grid-template.md +334 -0
  40. package/payload/codex-registration.mjs +62 -0
  41. package/payload/install-state.mjs +46 -0
  42. package/payload/platform.mjs +107 -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,180 @@
1
- # Temporary Holding Version
1
+ # BoxBox plugin installer for Codex and Claude Code
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The public command is **`npx boxbox-plugin`**. The npm package is [`boxbox-plugin`](https://www.npmjs.com/package/boxbox-plugin);
4
+ its single executable is `boxbox`, and plugin/skill identifiers remain `boxbox`. The package is licensed under MIT
5
+ and ships its `LICENSE`. The unrelated npm package `boxbox` is not this installer.
6
+
7
+ ## Windows PowerShell prerequisites
8
+
9
+ Install Node.js **22.22.3 or newer**, npm, and at least one host CLI with plugin support: Codex or Claude Code.
10
+ Check `node --version`, `npm.cmd --version`, and `codex.cmd plugin --help` or `claude plugin --help` in a new PowerShell
11
+ terminal. Use `codex` for a native Codex executable. Standard npm-installed Windows Claude Code is detected through its
12
+ native `claude.exe`; no Bash, WSL, or script execution-policy change is required by this installer.
13
+ The core CLI requires no global install, Bun, or source checkout. Python inspection and formatting additionally require a runnable `python3`.
14
+
15
+ If `CODEX_HOME` is set for a Codex installation, it must already be an existing directory. `CLAUDE_CONFIG_DIR` is passed to
16
+ Claude unchanged. Existing host configuration is preserved through normal plugin-management commands. The installer never
17
+ edits hook trust or grants it automatically.
18
+
19
+ ## Install
20
+
21
+ Install the published release:
22
+
23
+ ```powershell
24
+ npx.cmd boxbox-plugin
25
+ ```
26
+
27
+ With no arguments, the installer detects installed Codex and Claude Code CLIs and installs for every detected host. It
28
+ fails with instructions if neither is found. Select a particular host explicitly when needed:
29
+
30
+ ```powershell
31
+ npx.cmd boxbox-plugin --codex
32
+ npx.cmd boxbox-plugin --claude
33
+ npx.cmd boxbox-plugin --codex --claude
34
+ ```
35
+
36
+ To test a local candidate before publication, use the absolute installer tarball path instead of the registry:
37
+
38
+ ```powershell
39
+ npm.cmd exec --yes --package 'C:\path\to\boxbox-plugin-<version>.tgz' -- boxbox
40
+ ```
41
+
42
+ ## If you added the marketplace first
43
+
44
+ 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:
45
+
46
+ ```sh
47
+ claude plugin uninstall boxbox@boxbox-plugins --scope user
48
+ claude plugin marketplace remove boxbox-plugins
49
+ codex plugin marketplace remove boxbox-plugins
50
+ ```
51
+
52
+ ## Removing an earlier HUD
53
+
54
+ The opt-in terminal HUD (`--hud`) was removed after 0.2.4. If an earlier installation enabled it, disable it with
55
+ that installation's permanent plugin directory, which the new installer leaves in place:
56
+
57
+ ```sh
58
+ node '/absolute/old/installed/plugin/scripts/hud/setup.mjs' claude '/absolute/old/installed/plugin' --disable
59
+ node '/absolute/old/installed/plugin/scripts/hud/setup.mjs' codex '/absolute/old/installed/plugin' --disable
60
+ ```
61
+
62
+ Disable removes only a still-owned status-line key or launcher. Unrelated settings and later user changes are preserved.
63
+
64
+ ## Updating a local Codex candidate from an earlier version
65
+
66
+ If the old installation enabled the Codex HUD, disable it first (see above), then run the new installer:
67
+
68
+ ```powershell
69
+ npm.cmd exec --yes --package 'C:\path\to\boxbox-plugin-<version>.tgz' -- boxbox --codex
70
+ ```
71
+
72
+ Use your installed OpenAI Codex CLI (`codex.cmd` is the standard Windows npm entry). For an installer-owned older
73
+ BoxBox marketplace, the installer verifies the previous bundle, removes its plugin and marketplace registration, and
74
+ registers the new versioned source. A failed transition attempts to restore the previous registration. A same-named
75
+ foreign marketplace is refused without removal.
76
+
77
+ The payload is copied out of npm's temporary cache into permanent, separate host directories before registration:
78
+
79
+ - Windows: `%LOCALAPPDATA%\BoxBox\Codex\<version>` and `%LOCALAPPDATA%\BoxBox\Claude\<version>`
80
+ - macOS/Linux: `$XDG_DATA_HOME/boxbox/<host>/<version>`, or `~/.local/share/boxbox/<host>/<version>` when unset
81
+
82
+ Each selected host installs the included private `boxbox` engine tarball with `--ignore-scripts --no-audit --no-fund`.
83
+ Before engine installation or host registration, each copied host payload verifies that tarball against the SHA-256 in its bundle metadata.
84
+ Missing, malformed or mismatched integrity metadata stops installation with instructions to reinstall the installer
85
+ package. This detects mixed archive bytes inside a payload; it is not a publisher signature. The installed CLI version
86
+ is checked separately. The nested engine never comes from the npm registry. Pinned `typescript@7.0.2` and its native platform dependencies still
87
+ need internet access or a primed npm cache. Installed plugins survive npm cache cleanup or deletion of the original
88
+ installer tarball. Rerunning the same version repairs shipped files and runtime while preserving other data in that folder.
89
+ If repair fails before host registration starts, the previous directory is restored. Once registration may have
90
+ changed, a failure retains the new directory, its `.boxbox-install-state.json` phase receipt and any previous backup;
91
+ the error prints their exact paths. This prevents a host registration from pointing at a deleted directory. Inspect
92
+ the host plugin and marketplace lists before recovery. Rerunning the exact installer deliberately retries a valid
93
+ receipt belonging to that same root/version/host; its replacement backup is removed only after verified success. Foreign,
94
+ malformed or symlink receipts are refused without replacement. Do not delete retained directories while a host still
95
+ references them; after removing the exact registration, restore the printed backup or reinstall. This is partial-state
96
+ retention, not an automatic rollback of host settings. Successful retries do not delete backups retained by earlier runs.
97
+
98
+ For an installer-owned local Claude marketplace, installation checks the exact user-scope plugin identity and
99
+ refreshes stale recorded versions with `claude plugin update boxbox@boxbox-plugins --scope user`. Completion requires
100
+ the intended marketplace root, version, cached manifest and engine paths to agree. A foreign/shared marketplace or
101
+ ambiguous plugin scope is refused. Older installer archives do not gain these checks: use the documented exact plugin
102
+ and marketplace removal commands below before reinstalling an older archive for rollback. Restart Claude after a change.
103
+
104
+ Restart each selected host. In Codex, select the BoxBox `boxbox` skill in the picker; in Claude Code, select the installed
105
+ BoxBox skill from its skill list. Name one TypeScript or Python file and request a read-only check first. Review the
106
+ three hook definitions in the host before relying on them. Codex skips untrusted plugin hooks: use `/hooks` to review and
107
+ explicitly trust their exact current definitions. Claude Code uses its own hook/settings review workflow.
108
+
109
+ Codex hooks use the documented Windows PowerShell override. Claude hooks use direct `command` plus `args` execution,
110
+ including variable expansion of `${CLAUDE_PLUGIN_ROOT}`, avoiding Windows shell parsing. Both adapters default to
111
+ read-only inspection, store receipts in host plugin data, and refuse a hook when the host omits its data directory.
112
+ An explicit `boxbox mode align` enables only approved horizontal alignment after matched PostToolUse calls, followed
113
+ by reinspection; `check` and `refactor` keep hooks read-only, `off` skips inspection, and Stop never writes source.
114
+ PostToolUse checks all session changes after matched file-edit or shell calls, including read-only shell calls;
115
+ it does not infer which command wrote a file. Codex exposes `exec_command` and a completed `write_stdin` through the
116
+ hook name `Bash`. Shell feedback is advisory: inspection failures add context while preserving the command's output
117
+ and exit status, so guidance, previews and tests remain readable. File-edit feedback retains its existing blocking
118
+ behavior. Stop checks session changes again and requests at most one continuation. Other tool paths may not match.
119
+ A receipt with `NO_GROUPS` means
120
+ the file was inspected but no supported table groups were found, rather than whole-file alignment coverage.
121
+ Claude's generated Windows engine commands use a Node bridge that works from Bash or PowerShell and preserves paths and
122
+ arguments literally, so no shell switch is needed.
123
+ The Claude plugin also includes the canonical read-only inspector; the Codex plugin does not include an agent.
124
+ Native Windows execution must still be verified on the destination computer.
125
+
126
+ The recommended preset includes approved TypeScript top-level variable declarations of calls and simple values (2+ rows) and Python
127
+ simple name assignments (3+ rows at one indentation). No custom formatter or approval DB is required for those exact
128
+ patterns. Other patterns remain read-only until approved. Use `check --json`, `guide --json`, then `format --dry-run`
129
+ before an explicit `format` request. A positive approved-group count does not bypass the whole-file approval gate.
130
+ Use `boxbox mode` to read the current project mode, or `boxbox mode check --global` to set the global default. A valid
131
+ `BOXBOX_MODE` overrides project and global state for the current process.
132
+
133
+ Formatters such as Prettier, Black and Ruff revert BoxBox alignment in the files they rewrite. `doctor`, `init`, `align`
134
+ and `format` warn when a project-root configuration shows one, distinguishing `runs` from `possible`. Configure the
135
+ formatter for its language and invoke BoxBox from editor, pre-commit and CI commands. `check` and `format` accept
136
+ repeated `--file` or bounded recursive `--directory`; Align remains single-file. Configured-formatter Check and
137
+ `format --dry-run` execute the configured program on working copies, keep original source bytes intact, and require
138
+ the same approval/parser/fixed-point gates as Format. Check compares the final formatter-then-BoxBox bytes with the
139
+ original, keeping source findings separate from `canonical` verification. Batch formats are atomic per file and
140
+ retain earlier completed writes when another file fails; per-group selective alignment remains deferred.
141
+
142
+ `boxbox update --check` requests an update check and reports a non-success result when the release query fails.
143
+ After registry publication, rerun `npx boxbox-plugin` to update detected hosts to the published installer. The
144
+ `boxbox update --source <checkout>` path is a development-only override that requires an absolute BoxBox checkout
145
+ and its build prerequisites; an installed launcher infers its host, while a standalone call must pass `--host`.
146
+ Success requires the selected host's installed engine to report the intended version, followed by a host restart.
147
+ Session notices never install updates themselves. `boxbox statusline --install-shim` prints an optional
148
+ version-independent Claude status-line command; `NO_COLOR` or `BOXBOX_COLOR=0` disables color.
149
+
150
+ Run `monitor --project --host auto --json` through the host's installed launcher/environment for an on-demand report.
151
+ The command requires absolute `CLAUDE_PLUGIN_DATA` or `PLUGIN_DATA` and always writes a host HTML page there, including in JSON
152
+ mode. `--html <absolute-path>` replaces the default destination inside that folder; project or outside-folder destinations are refused.
153
+ Missing plugin data produces setup guidance. `--no-paths` hides source filenames, while the returned output path stays
154
+ visible. No transcript text, commands, source content, model call or dollar cost belongs to this report. Legacy
155
+ footprints and exact Codex step attribution may be unavailable and remain labeled.
156
+
157
+ External formatters are optional. On Windows use a native executable or a Node JS entry point (`node.exe` plus script
158
+ and `{file}` arguments). Arbitrary `.cmd`/`.bat` formatters are refused with configuration guidance; BoxBox never
159
+ interpolates formatter arguments into a shell. A configured formatter failure never falls back silently to standalone mode.
160
+ `boxbox --help` prints usage without installing. `boxbox --engine-only` is for diagnosis: it prepares only the Codex
161
+ permanent local engine and skips all host checks and registration. It cannot be combined with host selection flags.
162
+
163
+ ## Remove
164
+
165
+ Use the same host configuration environment as installation, and remove each installed adapter separately:
166
+
167
+ ```powershell
168
+ codex.cmd plugin remove boxbox@boxbox-plugins
169
+ codex.cmd plugin marketplace remove boxbox-plugins
170
+ claude plugin uninstall boxbox@boxbox-plugins --scope user
171
+ claude plugin marketplace remove boxbox-plugins
172
+ ```
173
+
174
+ Use `codex` for a native executable. Restart the host. Once its removal succeeds, its version directory above can be
175
+ deleted if desired. Keep a marketplace used by other plugins. Project `.boxbox/` approvals and personal data are not
176
+ removed. On macOS/Linux use `npx`, `npm exec`, `codex`, `claude`, and the corresponding data directory.
177
+
178
+ Official contracts: [Codex packaging](https://developers.openai.com/plugins/build/plugins),
179
+ [Codex hooks](https://learn.chatgpt.com/docs/hooks), [Claude marketplaces](https://code.claude.com/docs/en/plugin-marketplaces),
180
+ 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
- "name": "boxbox-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "boxbox-plugin",
3
+ "version": "0.2.8",
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,8 @@
1
+ {
2
+ "name": "boxbox",
3
+ "npmName": "boxbox-plugin",
4
+ "version": "0.2.8",
5
+ "displayName": "BoxBox",
6
+ "marketplace": "boxbox-plugins",
7
+ "engineSha256": "ff2070541cbf621f0789ecbd4a41279e0f9e6f6f929a224357b87f5126e43f02"
8
+ }
@@ -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.8"
12
+ }
13
+ ],
14
+ "description": "BoxBox: tabular readability checks for TypeScript, Python, JSON and YAML"
15
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "boxbox",
3
+ "npmName": "boxbox-plugin",
4
+ "version": "0.2.8",
5
+ "displayName": "BoxBox",
6
+ "marketplace": "boxbox-plugins",
7
+ "engineSha256": "ff2070541cbf621f0789ecbd4a41279e0f9e6f6f929a224357b87f5126e43f02",
8
+ "host": "claude"
9
+ }
@@ -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
+ }