pstack-claude-mirror 0.9.133 → 0.9.134
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGES.md +8 -0
- package/README.md +23 -7
- package/UPSTREAM-README.md +6 -0
- package/bin/pstack-claude.mjs +278 -130
- package/docs/reference.md +4 -4
- package/package.json +27 -9
- package/plugins/pstack/.antigravity-plugin/plugin.json +1 -1
- package/plugins/pstack/.claude-plugin/plugin.json +1 -1
- package/plugins/pstack/.codex-plugin/plugin.json +1 -1
- package/plugins/pstack/.github/plugin/plugin.json +1 -1
- package/plugins/pstack/.grok-plugin/plugin.json +1 -1
- package/plugins/pstack/.muse-plugin/plugin.json +1 -1
- package/plugins/pstack/LICENSE +22 -0
- package/plugins/pstack/hooks/copilot-hooks.json +4 -2
- package/plugins/pstack/hooks/session-start.ps1 +76 -12
- package/plugins/pstack/hooks/session-start.sh +6 -0
- package/plugins/pstack/models.json +1 -1
- package/plugins/pstack/package.json +1 -1
- package/plugins/pstack/pi/slash.ts +19 -5
- package/plugins/pstack/skills/poteto-mode/SKILL.md +1 -0
- package/plugins/pstack/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/plugins/pstack/skills/poteto-mode/references/agy-tools.md +5 -4
- package/plugins/pstack/skills/poteto-mode/references/codex-tools.md +3 -2
- package/plugins/pstack/skills/poteto-mode/references/copilot-tools.md +2 -1
- package/plugins/pstack/skills/poteto-mode/references/pi-tools.md +1 -1
- package/plugins/pstack/skills/poteto-mode/scripts/.prettierignore +1 -1
- package/plugins/pstack/skills/poteto-mode/scripts/codex-session-effort.mjs +35 -7
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/github.port.test.ts +427 -0
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/github.test.ts +3 -399
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/policy.port.test.ts +708 -0
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/policy.test.ts +7 -666
- package/plugins/pstack/skills/setup-pstack/SKILL.md +6 -4
- package/plugins/pstack/skills/setup-pstack/references/runtimes.md +1 -1
- package/tools/forks.json +5 -5
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "pstack",
|
|
11
11
|
"source": "./plugins/pstack",
|
|
12
12
|
"description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Original pstack by Lauren Tan (poteto).",
|
|
13
|
-
"version": "0.9.
|
|
13
|
+
"version": "0.9.134"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
package/CHANGES.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the release changelog, with one `## <version> - <title>` entry per release, newest first. The Cursor-to-Claude rewrite rules live in [`tools/substitutions.json`](tools/substitutions.json), and the [sync boundary](CONTRIBUTING.md#the-sync-boundary) in `CONTRIBUTING.md` defines how a change to upstream's skill content is declared.
|
|
4
4
|
|
|
5
|
+
## 0.9.134 - Fable 5.2 on Pi, Pi 1.5.1 as the oldest supported Pi
|
|
6
|
+
|
|
7
|
+
On Pi, the `anthropic` provider's `fable` role runs `claude-fable-5-2`, where it ran `claude-fable-5-1`. A session on a provider that has no table of its own uses the `anthropic` table, so its `fable` role changes too. Claude Code sessions need no change, because Claude Code 2.1.321 moved its own `fable` alias to Fable 5.2. The model first appears in Pi 1.5.1's catalog. Pi 1.5.0 does not have it. It warns `Model "claude-fable-5-2" not found for provider "anthropic". Using custom model id.` and runs that ID with the context window, pricing, and thinking settings of the provider's default model. To stay on Fable 5.1, add the line `pi models: fable=anthropic/claude-fable-5-1` to the override sheet.
|
|
8
|
+
|
|
9
|
+
Pi 1.5.1 is now the oldest Pi that the port supports. CI typechecks the Pi extension against Pi 1.5.1, where it used Pi 1.1.0, and checks the model catalog, child startup, and slash expansion against it. `CONTRIBUTING.md` asks for the local Pi checks on Pi 1.5.1. No CI job runs on Pi 1.1.0 through 1.5.0 any longer. The copy of Pi that the tests load the extension against moves from 1.5.0 to 1.5.2.
|
|
10
|
+
|
|
11
|
+
Pi 1.5.1 changed how it splits the arguments of a prompt template, and the extension's copy of that code in `pi/slash.ts` now does the same. The extension uses the copy when a wake-up prompt that the agent scheduled names a prompt template. A quote opens only at the start of an argument, so `--title="fix bug"` gives the template `--title="fix` and `bug"`, where it gave `--title=fix bug`. Inside a quoted argument, a backslash before the same kind of quote keeps the quote as text. A quote that never closes is text, and the words after it split at each space, so `open "quote here` gives `open`, `"quote`, and `here`, where it gave `open` and `quote here`. On Pi 1.5.0 and older, Pi itself still splits such arguments the old way. A local `bun test tests/` reads the installed Pi, so against Pi 1.5.0 or older `tests/pi/slash.test.mjs` and `tests/pi/catalog.test.mjs` each fail one test. Set `PI_PACKAGE_DIR` to a Pi 1.5.1 package to run them.
|
|
12
|
+
|
|
5
13
|
## 0.9.133 - worktree audit ref and PR fixes, setup-pstack runtime notes
|
|
6
14
|
|
|
7
15
|
`worktree-audit.mjs` now checks a worktree's ancestry against `refs/remotes/origin/<trunk>`. git resolves the shorthand `origin/<trunk>` to a tag or local branch of that name before the remote-tracking ref. Such a ref pointing at unmerged work made the audit report the worktree merged and safe. The REMOTE column's ahead count reads `refs/remotes/origin/<branch>` for the same reason. @EthanYoQ found and fixed the trunk case in [#423](https://github.com/michael-denyer/pstack-claude/pull/423).
|
package/README.md
CHANGED
|
@@ -3,21 +3,28 @@
|
|
|
3
3
|
> **This is an unofficial mirror, not the official package.**
|
|
4
4
|
> It is an unmodified copy of the GitHub repository [michael-denyer/pstack-claude](https://github.com/michael-denyer/pstack-claude), republished on npm for networks that can reach an npm registry mirror but not GitHub.
|
|
5
5
|
> It is **not** published by or affiliated with the upstream author (Michael Denyer), or with the original pstack author (Lauren Tan / Cursor).
|
|
6
|
-
> Report bugs and feature requests [upstream](https://github.com/michael-denyer/pstack-claude/issues)
|
|
6
|
+
> Report plugin bugs and feature requests [upstream](https://github.com/michael-denyer/pstack-claude/issues). Report mirror installation and packaging problems in the mirror repository linked by this package's `repository` field.
|
|
7
7
|
|
|
8
8
|
**What the mirror changes:** nothing in the upstream files. It only adds `bin/` (the installer), this `README.md` (upstream's README is kept as `UPSTREAM-README.md`), `LICENSE-mirror`, and its own `package.json`.
|
|
9
9
|
|
|
10
10
|
| | |
|
|
11
11
|
|---|---|
|
|
12
|
-
| Package version | `0.9.
|
|
13
|
-
| Upstream
|
|
14
|
-
| Upstream
|
|
12
|
+
| Package version | `0.9.134` |
|
|
13
|
+
| Upstream plugin version | `0.9.134` |
|
|
14
|
+
| Upstream ref | `v0.9.134` |
|
|
15
|
+
| Upstream commit | `a691bd30341289b9c7eae8e5b4da28a400ea2423` (2026-11-10T17:42:12Z) |
|
|
15
16
|
| Marketplace / plugin | `pstack@pstack-claude` |
|
|
16
17
|
|
|
17
18
|
The package contains the upstream marketplace exactly as published (`.claude-plugin/marketplace.json` + `plugins/pstack/`) plus a small installer. No install scripts run on `npm install`.
|
|
18
19
|
|
|
20
|
+
Mirror-only corrections can add a `-mirror.N` suffix to the npm package version while upstream plugin versions stay unchanged. Install `@latest` to receive the current mirror revision. Stable SemVer ranges do not automatically select prerelease suffixes.
|
|
21
|
+
|
|
22
|
+
The installer depends on `cross-spawn@7.0.6` for Windows command shims. npm installs its dependencies through your configured registry. Keep those packages available in an intranet registry or local npm cache when installing without internet access.
|
|
23
|
+
|
|
19
24
|
## Install (OMP and/or Claude Code)
|
|
20
25
|
|
|
26
|
+
Use Node.js 18 or later and an installed `omp` or `claude` CLI.
|
|
27
|
+
|
|
21
28
|
```shell
|
|
22
29
|
npm install -g pstack-claude-mirror # registry = your intranet npm mirror
|
|
23
30
|
pstack-claude-mirror install # every CLI found on PATH
|
|
@@ -39,6 +46,10 @@ claude plugin install pstack@pstack-claude --scope user
|
|
|
39
46
|
|
|
40
47
|
Restart OMP / start a new Claude Code session afterwards. Use `--dry-run` to see the commands, `--scope project` for a project-scoped install.
|
|
41
48
|
|
|
49
|
+
Choose a new directory, an empty directory, or a directory already managed by this package. The installer checks its `.pstack-mirror.json` ownership marker and every selected CLI's marketplace source before replacing files. It refuses unowned nonempty directories and symbolic-link destinations. An owned directory is disposable mirror state. Do not store your own files there.
|
|
50
|
+
|
|
51
|
+
`--dry-run` reads CLI state and reports conflicts, but does not stage files or change plugin registrations.
|
|
52
|
+
|
|
42
53
|
## Update
|
|
43
54
|
|
|
44
55
|
```shell
|
|
@@ -52,10 +63,13 @@ pstack-claude-mirror update
|
|
|
52
63
|
|
|
53
64
|
```shell
|
|
54
65
|
pstack-claude-mirror uninstall
|
|
55
|
-
rm -rf ~/.pstack-claude-mirror
|
|
56
66
|
npm uninstall -g pstack-claude-mirror
|
|
57
67
|
```
|
|
58
68
|
|
|
69
|
+
Uninstall leaves the marketplace files on disk. Delete `~/.pstack-claude-mirror` only after no other CLI or project uses it. The installer retains a marketplace registration when another installation uses it. It also retains Claude registrations when the CLI does not report their declaration scope. To remove a retained registration, check the scope you used to add it, then run `claude plugin marketplace remove pstack-claude --scope user` or `--scope project` as appropriate.
|
|
70
|
+
|
|
71
|
+
External CLI failures return a nonzero exit status. A failed plugin removal does not trigger marketplace removal.
|
|
72
|
+
|
|
59
73
|
## Other harnesses
|
|
60
74
|
|
|
61
75
|
- **Pi** (untested): `pi install npm:pstack-claude-mirror` (the package keeps upstream's `pi` manifest).
|
|
@@ -68,7 +82,7 @@ npm uninstall -g pstack-claude-mirror
|
|
|
68
82
|
|
|
69
83
|
## License
|
|
70
84
|
|
|
71
|
-
|
|
85
|
+
Mirrored upstream content is redistributed unchanged under its MIT licenses, with every upstream copyright and permission notice kept as it is:
|
|
72
86
|
|
|
73
87
|
| Component | Copyright | License file |
|
|
74
88
|
|---|---|---|
|
|
@@ -78,8 +92,10 @@ Everything except the mirror's own files is redistributed unchanged under the up
|
|
|
78
92
|
| `thermos`, `thermo-nuclear-*` skills and agents | © 2026 Cursor | `LICENSE-thermos` |
|
|
79
93
|
| Mirror additions only: `bin/`, this `README.md`, `package.json` | © 2026 The pstack-claude-mirror maintainers | `LICENSE-mirror` |
|
|
80
94
|
|
|
95
|
+
The separately installed `cross-spawn` dependency is MIT-licensed. Its transitive dependencies retain their own MIT or ISC licenses in their npm packages.
|
|
96
|
+
|
|
81
97
|
The full per-directory provenance is in `NOTICE.md` (it links to `CHANGES.md`, `tools/substitutions.json`, `tools/forks.json`, `tools/upstream.json` and `docs/reference.md`, all included). A copy of the license files also ships inside the plugin at `plugins/pstack/skills/poteto-mode/references/licenses/`, so it reaches the OMP / Claude Code plugin cache.
|
|
82
98
|
|
|
83
|
-
Each
|
|
99
|
+
Each build checks upstream license metadata before packaging. A failed check stops automatic publication for review. This check is not a security audit or a substitute for reviewing license changes.
|
|
84
100
|
|
|
85
101
|
The software is provided "as is", without warranty of any kind (see the license files). The mirror does not imply any endorsement by the upstream authors.
|
package/UPSTREAM-README.md
CHANGED
|
@@ -217,6 +217,8 @@ Use poteto-mode to fix the search filter resetting when I change pages.
|
|
|
217
217
|
|
|
218
218
|
For a bug, it reproduces the failure, uses `how` and `why` to investigate, delegates the fix, then reruns the failing case. If the fix crosses a function boundary, it brings in `architect` before implementation. You receive the fix and the failing and passing evidence.
|
|
219
219
|
|
|
220
|
+
A localized bug with a demonstrated cause can use the [focused-fix exception](plugins/pstack/skills/poteto-mode/playbooks/bug-fix.md#focused-fix-exception). One delegate investigates and implements the correction when it preserves existing interfaces and state semantics and has a focused regression check. Reproduction, verification, and required independent review still apply; a small patch alone does not qualify.
|
|
221
|
+
|
|
220
222
|
[Other playbooks](plugins/pstack/skills/poteto-mode/SKILL.md#playbooks) cover planning, features, refactoring, performance issues, investigations, prototypes, PR maintenance, shipping, and longer projects.
|
|
221
223
|
|
|
222
224
|

|
|
@@ -232,6 +234,10 @@ For a bug, it reproduces the failure, uses `how` and `why` to investigate, deleg
|
|
|
232
234
|
|
|
233
235
|
pstack has no server or telemetry. Anything its skills ask your agent to read, including session transcripts, goes to your model provider. Scripts run locally, and PR tools use your GitHub CLI login.
|
|
234
236
|
|
|
237
|
+
This port has no automatic agent-diary reporting or diary opt-in setting. For a local decision trail, ask for [`show-me-your-work`](plugins/pstack/skills/show-me-your-work/SKILL.md). Longer or unattended workflows also invoke it. The trail stays uncommitted by default. If the trail is committed and pushed to a shared PR, anyone with access to the PR can read it.
|
|
238
|
+
|
|
239
|
+
To share feedback, submit a short, manually reviewed report through [GitHub issues](https://github.com/michael-denyer/pstack-claude/issues). Include the pstack version, runtime, relevant workflow, and observed result. Remove credentials, customer data, private repository links, and sensitive log or configuration values before posting; share a summary instead of raw transcripts.
|
|
240
|
+
|
|
235
241
|
## Contributing
|
|
236
242
|
|
|
237
243
|
Thanks for helping make this port better. Bug reports, documentation fixes, and runtime improvements are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the checks and where your change belongs. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
package/bin/pstack-claude.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Installs the pstack marketplace shipped in this npm package into OMP and/or Claude Code,
|
|
3
|
-
// without any GitHub access. See README.md.
|
|
4
2
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { cpSync,
|
|
3
|
+
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
6
5
|
import { homedir } from "node:os";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { stripVTControlCharacters } from "node:util";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
-
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const pkgRoot = realpathSync(resolve(dirname(fileURLToPath(import.meta.url)), ".."));
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
12
12
|
const { marketplace: MKT, plugin: PLUGIN } = pkg.pstackMirror;
|
|
13
13
|
const ID = `${PLUGIN}@${MKT}`;
|
|
14
14
|
const isWin = process.platform === "win32";
|
|
15
|
+
const spawn = isWin ? createRequire(import.meta.url)("cross-spawn").sync : spawnSync;
|
|
15
16
|
|
|
16
17
|
const HELP = `${pkg.name} ${pkg.version}: unofficial GitHub mirror of ${pkg.pstackMirror.upstream}\n(upstream ${pkg.pstackMirror.ref} ${pkg.pstackMirror.commit.slice(0, 12)}; not affiliated with the upstream author)
|
|
17
18
|
|
|
@@ -29,155 +30,302 @@ Targets default to every CLI found on PATH (omp, claude).
|
|
|
29
30
|
Options:
|
|
30
31
|
--dir <path> marketplace directory (default: $PSTACK_CLAUDE_HOME or ~/.pstack-claude-mirror/marketplace)
|
|
31
32
|
--scope <scope> plugin scope: user (default) or project
|
|
32
|
-
--dry-run print
|
|
33
|
+
--dry-run check current state and print changes without applying them
|
|
33
34
|
`;
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
-
const opts = { scope: "user", dryRun: false, dir: process.env.PSTACK_CLAUDE_HOME ? join(process.env.PSTACK_CLAUDE_HOME, "marketplace") : join(homedir(), ".pstack-claude-mirror", "marketplace") };
|
|
37
|
-
const pos = [];
|
|
38
|
-
for (let i = 0; i < argv.length; i++) {
|
|
39
|
-
const a = argv[i];
|
|
40
|
-
if (a === "--dir") opts.dir = resolve(argv[++i]);
|
|
41
|
-
else if (a === "--scope") opts.scope = argv[++i];
|
|
42
|
-
else if (a === "--dry-run") opts.dryRun = true;
|
|
43
|
-
else if (a === "-h" || a === "--help") pos.unshift("help");
|
|
44
|
-
else if (a === "-v" || a === "--version") pos.unshift("version");
|
|
45
|
-
else pos.push(a);
|
|
46
|
-
}
|
|
47
|
-
const [cmd = "help", ...targetArgs] = pos;
|
|
36
|
+
const opts = { scope: "user", dryRun: false, dir: join(process.env.PSTACK_CLAUDE_HOME || join(homedir(), ".pstack-claude-mirror"), "marketplace") };
|
|
48
37
|
|
|
49
38
|
function has(bin) {
|
|
50
|
-
const r =
|
|
39
|
+
const r = spawn(isWin ? "where" : "sh", isWin ? [bin] : ["-c", `command -v ${bin}`], { stdio: "ignore", shell: false });
|
|
51
40
|
return r.status === 0;
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
function targets() {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const found =
|
|
58
|
-
const missing =
|
|
59
|
-
if (
|
|
43
|
+
function targets(args) {
|
|
44
|
+
for (const x of args) if (!["omp", "claude", "all"].includes(x)) die(`unknown target "${x}" (expected omp, claude or all)`);
|
|
45
|
+
const requested = args.length && !args.includes("all") ? [...new Set(args)] : ["omp", "claude"];
|
|
46
|
+
const found = requested.filter(has);
|
|
47
|
+
const missing = requested.filter((x) => !found.includes(x));
|
|
48
|
+
if (args.length && !args.includes("all") && missing.length) die(`not found on PATH: ${missing.join(", ")}`);
|
|
60
49
|
if (!found.length) die("neither omp nor claude was found on PATH");
|
|
61
50
|
for (const m of missing) console.log(`- ${m} not found on PATH, skipping`);
|
|
62
51
|
return found;
|
|
63
52
|
}
|
|
64
53
|
|
|
65
|
-
function die(
|
|
66
|
-
|
|
67
|
-
process.exit(1);
|
|
54
|
+
function die(message) {
|
|
55
|
+
throw new Error(message);
|
|
68
56
|
}
|
|
69
57
|
|
|
70
|
-
function run(bin, args, {
|
|
71
|
-
console.log(`$ ${bin} ${args.join(" ")}`);
|
|
72
|
-
if (opts.dryRun) return
|
|
73
|
-
const r =
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
if (r.
|
|
77
|
-
|
|
58
|
+
function run(bin, args, { query = false } = {}) {
|
|
59
|
+
console.log(`$ ${bin} ${args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(" ")}`);
|
|
60
|
+
if (opts.dryRun && !query) return "";
|
|
61
|
+
const r = spawn(bin, args, { encoding: "utf8", shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
62
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
63
|
+
if (r.stdout && (!query || r.status !== 0)) process.stdout.write(r.stdout);
|
|
64
|
+
if (r.error) die(`could not run ${bin}: ${r.error.message}`);
|
|
65
|
+
if (r.status !== 0) die(`${bin} ${args.join(" ")} failed (${r.signal ? `signal ${r.signal}` : `exit ${r.status}`})`);
|
|
66
|
+
return r.stdout || "";
|
|
78
67
|
}
|
|
79
68
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
69
|
+
function statIfPresent(path) {
|
|
70
|
+
try {
|
|
71
|
+
return lstatSync(path);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code === "ENOENT") return null;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function canonicalPath(path, rejectFinalSymlink = true) {
|
|
79
|
+
const absolute = resolve(path);
|
|
80
|
+
const stat = statIfPresent(absolute);
|
|
81
|
+
if (stat) {
|
|
82
|
+
if (rejectFinalSymlink && stat.isSymbolicLink()) die(`refusing symlink: ${absolute}`);
|
|
83
|
+
return realpathSync(absolute);
|
|
84
|
+
}
|
|
85
|
+
const parent = dirname(absolute);
|
|
86
|
+
if (parent === absolute) die(`cannot resolve filesystem root: ${absolute}`);
|
|
87
|
+
return join(canonicalPath(parent, false), basename(absolute));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function contains(parent, child) {
|
|
91
|
+
const path = relative(parent, child);
|
|
92
|
+
return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function inspectDirectory(path) {
|
|
96
|
+
path = canonicalPath(path);
|
|
97
|
+
if (contains(pkgRoot, path) || contains(path, pkgRoot)) die(`marketplace directory overlaps this package: ${path}`);
|
|
98
|
+
const stat = statIfPresent(path);
|
|
99
|
+
if (!stat) return { kind: "absent", path, stat };
|
|
100
|
+
if (!stat.isDirectory()) die(`marketplace path is not a directory: ${path}`);
|
|
101
|
+
if (!readdirSync(path).length) return { kind: "empty", path, stat };
|
|
102
|
+
const markerPath = join(path, ".pstack-mirror.json");
|
|
103
|
+
const markerStat = statIfPresent(markerPath);
|
|
104
|
+
if (!markerStat || !markerStat.isFile() || markerStat.isSymbolicLink()) die(`refusing nonempty directory without a regular ownership marker: ${path}`);
|
|
105
|
+
let marker;
|
|
106
|
+
try {
|
|
107
|
+
marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
die(`cannot read ownership marker ${markerPath}: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
const owner = { package: pkg.name, upstream: pkg.pstackMirror.upstream, marketplace: MKT, plugin: PLUGIN };
|
|
112
|
+
if (!isRecord(marker) || Object.entries(owner).some(([key, value]) => marker[key] !== value)) die(`ownership marker does not match this mirror: ${markerPath}`);
|
|
113
|
+
return { kind: "owned", path, stat };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isRecord(value) {
|
|
117
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function text(value) {
|
|
121
|
+
return typeof value === "string" && value.trim().length > 0 && !/[\x00-\x1f\x7f]/.test(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function jsonQuery(bin, args) {
|
|
125
|
+
const output = run(bin, args, { query: true });
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(output);
|
|
128
|
+
} catch {
|
|
129
|
+
die(`invalid JSON from ${bin} ${args.join(" ")}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sameSource(target, source) {
|
|
134
|
+
if (!text(source) || !isAbsolute(source) || canonicalPath(source) !== opts.dir) {
|
|
135
|
+
die(`${target} already has marketplace "${MKT}" from another source: ${source}`);
|
|
93
136
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.log("\n== OMP");
|
|
103
|
-
const list = run("omp", ["plugin", "marketplace", "list"], { capture: true, allowFail: true }).out;
|
|
104
|
-
const line = list.split("\n").find((l) => l.trim().split(/\s+/)[0] === MKT);
|
|
105
|
-
if (!line) {
|
|
106
|
-
run("omp", ["plugin", "marketplace", "add", dir]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function inspectOmp() {
|
|
140
|
+
const output = stripVTControlCharacters(run("omp", ["plugin", "marketplace", "list"], { query: true }));
|
|
141
|
+
const lines = output.split(/\r?\n/).filter((line) => line.trim());
|
|
142
|
+
const marketplaces = new Map();
|
|
143
|
+
if (lines[0] === "No marketplaces configured") {
|
|
144
|
+
if (lines.length > 2 || (lines.length === 2 && lines[1] !== "Add one with: omp plugin marketplace add <source>")) die("unrecognized OMP marketplace listing");
|
|
107
145
|
} else {
|
|
108
|
-
if (
|
|
109
|
-
|
|
146
|
+
if (lines[0] !== "Configured Marketplaces:" || lines.length < 2) die("unrecognized OMP marketplace listing");
|
|
147
|
+
for (const line of lines.slice(1)) {
|
|
148
|
+
const row = /^\s{2}([\w.-]+)[ \t]{2,}(.+)$/.exec(line);
|
|
149
|
+
if (!row || !text(row[2]) || marketplaces.has(row[1])) die("invalid or duplicate OMP marketplace row");
|
|
150
|
+
marketplaces.set(row[1], row[2]);
|
|
110
151
|
}
|
|
111
|
-
run("omp", ["plugin", "marketplace", "update", MKT]);
|
|
112
152
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
if (marketplaces.has(MKT)) sameSource("OMP", marketplaces.get(MKT));
|
|
154
|
+
const plugins = jsonQuery("omp", ["plugin", "list", "--json"]);
|
|
155
|
+
if (!isRecord(plugins) || !Array.isArray(plugins.npm) || !Array.isArray(plugins.marketplace)) die("invalid OMP plugin listing");
|
|
156
|
+
const installed = [];
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (const plugin of plugins.marketplace) {
|
|
159
|
+
if (!isRecord(plugin) || !text(plugin.id) || !/^[^@\s]+@[^@\s]+$/.test(plugin.id) || !["user", "project"].includes(plugin.scope) || !Array.isArray(plugin.entries)) die("invalid OMP marketplace plugin");
|
|
160
|
+
const key = `${plugin.id}:${plugin.scope}`;
|
|
161
|
+
if (seen.has(key)) die("duplicate OMP marketplace plugin");
|
|
162
|
+
seen.add(key);
|
|
163
|
+
for (const entry of plugin.entries) {
|
|
164
|
+
if (!isRecord(entry) || entry.scope !== plugin.scope) die("invalid OMP plugin entry");
|
|
165
|
+
installed.push({ id: plugin.id, scope: entry.scope });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
target: "omp",
|
|
170
|
+
marketplace: marketplaces.has(MKT),
|
|
171
|
+
present: installed.some((p) => p.id === ID && p.scope === opts.scope),
|
|
172
|
+
retain: installed.some((p) => p.id.endsWith(`@${MKT}`) && (p.id !== ID || p.scope !== opts.scope)),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function inspectClaude() {
|
|
177
|
+
const marketplaces = jsonQuery("claude", ["plugin", "marketplace", "list", "--json"]);
|
|
178
|
+
if (!Array.isArray(marketplaces)) die("invalid Claude Code marketplace listing");
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const marketplace of marketplaces) {
|
|
181
|
+
if (!isRecord(marketplace) || !text(marketplace.name) || seen.has(marketplace.name)) die("invalid or duplicate Claude Code marketplace");
|
|
182
|
+
seen.add(marketplace.name);
|
|
183
|
+
const sourceField = { directory: "path", file: "path", github: "repo", git: "url", url: "url", claudeai: "marketplaceId" }[marketplace.source];
|
|
184
|
+
if (!text(marketplace.source) || typeof sourceField !== "string" || !text(marketplace[sourceField])) die("invalid Claude Code marketplace source");
|
|
185
|
+
if (["directory", "file"].includes(marketplace.source) && !isAbsolute(marketplace.path)) die("invalid Claude Code local marketplace path");
|
|
186
|
+
if (marketplace.scope !== undefined && !["user", "project", "local", "managed"].includes(marketplace.scope)) die("invalid Claude Code marketplace scope");
|
|
187
|
+
}
|
|
188
|
+
const marketplace = marketplaces.find((m) => m.name === MKT);
|
|
189
|
+
if (marketplace) {
|
|
190
|
+
if (marketplace.source !== "directory") die(`Claude Code already has marketplace "${MKT}" from another source: ${marketplace.source}`);
|
|
191
|
+
sameSource("Claude Code", marketplace.path);
|
|
192
|
+
}
|
|
193
|
+
const plugins = jsonQuery("claude", ["plugin", "list", "--json"]);
|
|
194
|
+
if (!Array.isArray(plugins)) die("invalid Claude Code plugin listing");
|
|
195
|
+
const project = canonicalPath(process.cwd(), false);
|
|
196
|
+
const installed = [];
|
|
197
|
+
seen.clear();
|
|
198
|
+
for (const plugin of plugins) {
|
|
199
|
+
if (!isRecord(plugin) || !text(plugin.id) || !/^[^@\s]+@[^@\s]+$/.test(plugin.id) || !["user", "project", "local", "managed", "session", "synced"].includes(plugin.scope)) die("invalid Claude Code plugin");
|
|
200
|
+
const scopedProject = plugin.scope === "project" || plugin.scope === "local";
|
|
201
|
+
if (scopedProject && (!text(plugin.projectPath) || !isAbsolute(plugin.projectPath))) die("invalid Claude Code plugin project path");
|
|
202
|
+
const projectPath = scopedProject ? canonicalPath(plugin.projectPath, false) : null;
|
|
203
|
+
const key = JSON.stringify([plugin.id, plugin.scope, projectPath]);
|
|
204
|
+
if (seen.has(key)) die("duplicate Claude Code plugin installation");
|
|
205
|
+
seen.add(key);
|
|
206
|
+
installed.push({ id: plugin.id, requested: plugin.scope === opts.scope && (!scopedProject || projectPath === project) });
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
target: "claude",
|
|
210
|
+
marketplace: Boolean(marketplace),
|
|
211
|
+
present: installed.some((p) => p.id === ID && p.requested),
|
|
212
|
+
retain: Boolean(marketplace && marketplace.scope !== opts.scope) || installed.some((p) => p.id.endsWith(`@${MKT}`) && (p.id !== ID || !p.requested)),
|
|
213
|
+
};
|
|
121
214
|
}
|
|
122
215
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
216
|
+
// Claude reads local marketplace content in place, so keep it outside the npm cache.
|
|
217
|
+
function stage(directory) {
|
|
218
|
+
const dir = directory.path;
|
|
219
|
+
console.log(`staging ${pkg.name}@${pkg.version} -> ${dir}`);
|
|
220
|
+
if (opts.dryRun) return;
|
|
221
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
222
|
+
const workspace = mkdtempSync(join(dirname(dir), `.${basename(dir)}.pstack-`));
|
|
223
|
+
const next = join(workspace, "next");
|
|
224
|
+
const backup = join(workspace, "previous");
|
|
225
|
+
let preserveBackup = false;
|
|
127
226
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
227
|
+
mkdirSync(next);
|
|
228
|
+
for (const name of [".claude-plugin", "plugins"]) cpSync(join(pkgRoot, name), join(next, name), { recursive: true });
|
|
229
|
+
for (const name of readdirSync(pkgRoot)) {
|
|
230
|
+
if ([".claude-plugin", "plugins", "bin", "package.json", "README.md", "node_modules", ".git", ".pstack-mirror.json"].includes(name)) continue;
|
|
231
|
+
cpSync(join(pkgRoot, name), join(next, name), { recursive: true });
|
|
232
|
+
}
|
|
233
|
+
writeFileSync(join(next, ".pstack-mirror.json"), JSON.stringify({ package: pkg.name, version: pkg.version, ...pkg.pstackMirror }, null, 2) + "\n");
|
|
234
|
+
const current = inspectDirectory(dir);
|
|
235
|
+
if (current.path !== dir || current.kind !== directory.kind || current.stat?.dev !== directory.stat?.dev || current.stat?.ino !== directory.stat?.ino) die(`marketplace directory changed during preflight: ${dir}`);
|
|
236
|
+
if (directory.kind !== "absent") renameSync(dir, backup);
|
|
237
|
+
try {
|
|
238
|
+
renameSync(next, dir);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (directory.kind !== "absent") {
|
|
241
|
+
try {
|
|
242
|
+
if (statIfPresent(dir)) die(`destination is occupied: ${dir}`);
|
|
243
|
+
renameSync(backup, dir);
|
|
244
|
+
} catch (restoreError) {
|
|
245
|
+
preserveBackup = true;
|
|
246
|
+
die(`staging failed: ${error.message}; restore failed: ${restoreError.message}; previous installation remains at ${backup}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
} finally {
|
|
252
|
+
if (!preserveBackup) rmSync(workspace, { recursive: true, force: true });
|
|
132
253
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log("\n== Claude Code");
|
|
147
|
-
run("claude", ["plugin", "uninstall", ID, "--scope", opts.scope], { allowFail: true });
|
|
148
|
-
run("claude", ["plugin", "marketplace", "remove", MKT], { allowFail: true });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
switch (cmd) {
|
|
152
|
-
case "help":
|
|
153
|
-
console.log(HELP);
|
|
154
|
-
break;
|
|
155
|
-
case "version":
|
|
156
|
-
console.log(pkg.version);
|
|
157
|
-
break;
|
|
158
|
-
case "path":
|
|
159
|
-
console.log(opts.dir);
|
|
160
|
-
break;
|
|
161
|
-
case "info":
|
|
162
|
-
console.log(JSON.stringify({ package: pkg.name, version: pkg.version, root: pkgRoot, marketplaceDir: opts.dir, ...pkg.pstackMirror }, null, 2));
|
|
163
|
-
break;
|
|
164
|
-
case "install":
|
|
165
|
-
case "update": {
|
|
166
|
-
if (!["user", "project"].includes(opts.scope)) die(`--scope must be user or project`);
|
|
167
|
-
const t = targets();
|
|
168
|
-
const dir = stage();
|
|
169
|
-
if (t.includes("omp")) installOmp(dir);
|
|
170
|
-
if (t.includes("claude")) installClaude(dir);
|
|
171
|
-
console.log(`\nDone: ${ID} ${pkg.version}`);
|
|
172
|
-
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function install(state) {
|
|
257
|
+
const { target, marketplace, present } = state;
|
|
258
|
+
console.log(`\n== ${target === "omp" ? "OMP" : "Claude Code"}`);
|
|
259
|
+
if (marketplace) run(target, ["plugin", "marketplace", "update", MKT]);
|
|
260
|
+
else run(target, ["plugin", "marketplace", "add", opts.dir, ...(target === "claude" ? ["--scope", opts.scope] : [])]);
|
|
261
|
+
if (target === "omp") {
|
|
262
|
+
run(target, ["plugin", present ? "upgrade" : "install", `--scope=${opts.scope}`, ID]);
|
|
263
|
+
console.log("Restart OMP to load pstack.");
|
|
264
|
+
} else {
|
|
265
|
+
run(target, ["plugin", "install", ID, "--scope", opts.scope]);
|
|
266
|
+
console.log("Start a new Claude Code session (or run /reload-plugins) to load pstack.");
|
|
173
267
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function uninstall(state) {
|
|
271
|
+
const { target, present, marketplace, retain } = state;
|
|
272
|
+
console.log(`\n== ${target === "omp" ? "OMP" : "Claude Code"}`);
|
|
273
|
+
if (present) run(target, ["plugin", "uninstall", ...(target === "omp" ? [`--scope=${opts.scope}`, ID] : [ID, "--scope", opts.scope])]);
|
|
274
|
+
if (marketplace && !retain) run(target, ["plugin", "marketplace", "remove", MKT, ...(target === "claude" ? ["--scope", opts.scope] : [])]);
|
|
275
|
+
if (marketplace && retain) console.log(`Keeping marketplace "${MKT}" for other installations or scopes.`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const argv = process.argv.slice(2);
|
|
280
|
+
const pos = [];
|
|
281
|
+
for (let i = 0; i < argv.length; i++) {
|
|
282
|
+
const arg = argv[i];
|
|
283
|
+
if (arg === "--dir" || arg === "--scope") {
|
|
284
|
+
const value = argv[++i];
|
|
285
|
+
if (!value || value.startsWith("-")) die(`${arg} requires a value`);
|
|
286
|
+
opts[arg === "--dir" ? "dir" : "scope"] = value;
|
|
287
|
+
} else if (arg === "--dry-run") opts.dryRun = true;
|
|
288
|
+
else if (arg === "-h" || arg === "--help") pos.unshift("help");
|
|
289
|
+
else if (arg === "-v" || arg === "--version") pos.unshift("version");
|
|
290
|
+
else if (arg.startsWith("-")) die(`unknown option "${arg}"`);
|
|
291
|
+
else pos.push(arg);
|
|
292
|
+
}
|
|
293
|
+
if (!["user", "project"].includes(opts.scope)) die("--scope must be user or project");
|
|
294
|
+
opts.dir = canonicalPath(opts.dir);
|
|
295
|
+
const [cmd = "help", ...targetArgs] = pos;
|
|
296
|
+
switch (cmd) {
|
|
297
|
+
case "help":
|
|
298
|
+
console.log(HELP);
|
|
299
|
+
break;
|
|
300
|
+
case "version":
|
|
301
|
+
console.log(pkg.version);
|
|
302
|
+
break;
|
|
303
|
+
case "path":
|
|
304
|
+
console.log(opts.dir);
|
|
305
|
+
break;
|
|
306
|
+
case "info":
|
|
307
|
+
console.log(JSON.stringify({ package: pkg.name, version: pkg.version, root: pkgRoot, marketplaceDir: opts.dir, ...pkg.pstackMirror }, null, 2));
|
|
308
|
+
break;
|
|
309
|
+
case "install":
|
|
310
|
+
case "update":
|
|
311
|
+
case "uninstall": {
|
|
312
|
+
const selected = targets(targetArgs);
|
|
313
|
+
const directory = inspectDirectory(opts.dir);
|
|
314
|
+
const states = selected.map((target) => target === "omp" ? inspectOmp() : inspectClaude());
|
|
315
|
+
if (cmd === "uninstall") {
|
|
316
|
+
for (const state of states) uninstall(state);
|
|
317
|
+
console.log(`\nThe marketplace files remain in ${opts.dir}; delete that directory if no other tool uses it.`);
|
|
318
|
+
} else {
|
|
319
|
+
stage(directory);
|
|
320
|
+
for (const state of states) install(state);
|
|
321
|
+
console.log(`\nDone: ${ID} ${pkg.version}`);
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
default:
|
|
326
|
+
die(`unknown command "${cmd}"\n\n${HELP}`);
|
|
180
327
|
}
|
|
181
|
-
|
|
182
|
-
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error(`error: ${error.message}`);
|
|
330
|
+
process.exitCode = 1;
|
|
183
331
|
}
|