pi-codex-marketplace 0.1.4 → 0.1.6
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/README.md +13 -9
- package/extensions/pi/installation.ts +3 -2
- package/package.json +1 -1
- package/src/cache/source-cache.ts +26 -1
- package/src/installation/flow.ts +10 -7
- package/src/installation/inspection.ts +64 -7
- package/src/projection/project.ts +1 -1
- package/src/registration/contained.ts +7 -2
package/README.md
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Bridge Package for Codex Marketplace compatibility in Pi (`Pi 0.84.2`).
|
|
4
4
|
|
|
5
|
-
> **One-line:** `pi install pi-codex-marketplace` → `/codex-marketplace` shows Global / Project partitioned Bridge State and lifecycle controls with Validation Disclosure, dual Confirmation, and three-orthogonal Receipt reporting.
|
|
5
|
+
> **One-line:** `pi install npm:pi-codex-marketplace` → `/codex-marketplace` shows Global / Project partitioned Bridge State and lifecycle controls with Validation Disclosure, dual Confirmation, and three-orthogonal Receipt reporting.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
pi install pi-codex-marketplace
|
|
11
|
-
pi install
|
|
12
|
-
pi install
|
|
13
|
-
pi
|
|
14
|
-
pi
|
|
15
|
-
pi -
|
|
10
|
+
pi install npm:pi-codex-marketplace # Global Scope (writes to ~/.pi/agent/settings.json)
|
|
11
|
+
pi install npm:pi-codex-marketplace -l # Project Scope (.pi/settings.json)
|
|
12
|
+
pi install ./path/to/pi-codex-marketplace # Local path (try without publishing)
|
|
13
|
+
pi install ./path/to/pi-codex-marketplace -l # Local path, Project Scope
|
|
14
|
+
pi -e npm:pi-codex-marketplace # Ephemeral try without installing (temporary)
|
|
15
|
+
pi update npm:pi-codex-marketplace # Update one package
|
|
16
|
+
pi update --all # Update pi + all packages
|
|
17
|
+
pi remove npm:pi-codex-marketplace # Remove package
|
|
18
|
+
pi list # List installed packages
|
|
19
|
+
pi config # Enable/disable resources (Tab switches scope)
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
Source types follow `docs/packages.md`: `npm:` for registry, `git:`/`https://` for git, and absolute/relative paths for local. Ephemeral runs use `pi -e <source>` (not `pi install -e`). This package declares a single `pi` extension entry (`extensions/pi/index.ts`) loaded via `jiti` and requires no build step.
|
|
19
23
|
|
|
20
24
|
Requirements: **Pi 0.84.2**, **Node >=22.19.0**, **macOS / Linux** (Windows not supported).
|
|
21
25
|
|
|
@@ -78,7 +82,7 @@ Project Scope mutations and Effective-State participation require Pi's `Project
|
|
|
78
82
|
| OS | **macOS**, **Linux** | Windows not supported (path containment, symlink, `flock` semantics are POSIX-only) |
|
|
79
83
|
| Node | **>=22.19.0** | `engines.node` enforced; `npm-shrinkwrap.json` pins Pi 0.84.2 host |
|
|
80
84
|
| Pi host | **0.84.2** | `peerDependencies` exact `0.84.2`; expected compatible range `^0.84.2` (devDeps). `pi-ai`/`pi-tui` peers `*` per Pi extension docs. |
|
|
81
|
-
| Semantics | `pi install` / `pi
|
|
85
|
+
| Semantics | `pi install` / `pi -e` / `pi install -l` / `pi update` / `pi remove` / `pi list` / `pi config` | Single `pi` extension package; `files` ships `extensions/`, `src/`, `README.md`, `LICENSE` only |
|
|
82
86
|
|
|
83
87
|
Peer declaration (dual): **精確 `0.84.2`** in `peerDependencies` (exact host that this version was validated against) + **預期 `^0.84.2`** in `devDependencies` (range expected to remain compatible). `pi-ai` and `pi-tui` remain `*` because they are bundled by Pi.
|
|
84
88
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type InstallationOutcome,
|
|
14
14
|
} from '../../src/installation/flow.js';
|
|
15
15
|
import { inspectMarketplaceEntries } from '../../src/installation/inspection.js';
|
|
16
|
+
import type { SourceCache } from '../../src/cache/source-cache.js';
|
|
16
17
|
import type { Registration, Scope } from '../../src/bridge-state/types.js';
|
|
17
18
|
import { reportOutcome } from './registration.js';
|
|
18
19
|
|
|
@@ -23,9 +24,9 @@ function labelText(value: string): string { return JSON.stringify(value); }
|
|
|
23
24
|
export async function entryChoices(
|
|
24
25
|
registration: Registration,
|
|
25
26
|
scope: Scope,
|
|
26
|
-
|
|
27
|
+
opts: { cwd?: string; agentDir?: string; projectTrusted?: boolean; cache?: SourceCache } = {},
|
|
27
28
|
): Promise<EntryChoice[]> {
|
|
28
|
-
const inspection = inspectMarketplaceEntries(registration, scope);
|
|
29
|
+
const inspection = inspectMarketplaceEntries(registration, scope, { agentDir: opts.agentDir, cache: opts.cache });
|
|
29
30
|
if (!inspection.marketplaceId) return [{ label: `${labelText(registration.alias ?? registration.id)} — Unavailable (${labelText(inspection.findings[0]?.outcome ?? 'Marketplace Catalog cannot be read')})` }];
|
|
30
31
|
return inspection.entries.map((item) => {
|
|
31
32
|
const status = item.unavailableReason ? `Unavailable (${item.unavailableReason})` : '可安裝';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-codex-marketplace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Bridge Package for Codex Marketplace compatibility in Pi — Global/Project Bridge State, atomic persistence, and /codex-marketplace TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { cpSync } from 'node:fs';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
|
|
30
|
-
import { acquireLock, releaseLock, atomicWriteFile } from '../bridge-state/atomic.js';
|
|
30
|
+
import { acquireLock, acquireLockSync, releaseLock, atomicWriteFile } from '../bridge-state/atomic.js';
|
|
31
31
|
import { readBridgeState } from '../bridge-state/store.js';
|
|
32
32
|
import type { BridgeState } from '../bridge-state/types.js';
|
|
33
33
|
import type { Scope } from '../bridge-state/types.js';
|
|
@@ -123,6 +123,19 @@ export class SourceCache {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Synchronous per-fingerprint mutual exclusion around store/prune/touch for that entry.
|
|
128
|
+
*/
|
|
129
|
+
withFingerprintLockSync<T>(fingerprint: string, fn: () => T, timeoutMs = 5000): T {
|
|
130
|
+
const lockPath = this.lockPath(fingerprint);
|
|
131
|
+
const fd = acquireLockSync(lockPath, timeoutMs);
|
|
132
|
+
try {
|
|
133
|
+
return fn();
|
|
134
|
+
} finally {
|
|
135
|
+
releaseLock(fd, lockPath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
/** Register an in-flight pin (reference-counted); returns its release function. In-flight entries are never evicted. */
|
|
127
140
|
pinInFlight(fingerprint: string): () => void {
|
|
128
141
|
this.inFlight.set(fingerprint, (this.inFlight.get(fingerprint) ?? 0) + 1);
|
|
@@ -148,6 +161,18 @@ export class SourceCache {
|
|
|
148
161
|
return { path: dir, fingerprint };
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
/** Synchronous exact fingerprint hit: entry directory + matching meta must exist. Touches lastAccess. */
|
|
165
|
+
hitExactSync(fingerprint: string): CacheHit | null {
|
|
166
|
+
if (!isSafeFingerprint(fingerprint)) return null;
|
|
167
|
+
const dir = this.entryPath(fingerprint);
|
|
168
|
+
const meta = this.readMeta(fingerprint);
|
|
169
|
+
if (!existsSync(dir) || !meta || meta.fingerprint !== fingerprint) return null;
|
|
170
|
+
this.withFingerprintLockSync(fingerprint, () => {
|
|
171
|
+
this.touchMeta(fingerprint);
|
|
172
|
+
});
|
|
173
|
+
return { path: dir, fingerprint };
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
/**
|
|
152
177
|
* Store an acquired source tree under its fingerprint. The stored fingerprint is pinned
|
|
153
178
|
* for the duration of the store ("in flight"), so its own prune can never evict it.
|
package/src/installation/flow.ts
CHANGED
|
@@ -13,12 +13,14 @@ import { CODE, RULE, blocking, hasBlocking, sortFindings, type ValidationFinding
|
|
|
13
13
|
import { acquireAttemptFence, type AttemptFenceHandle } from '../registration/fence.js';
|
|
14
14
|
import { createReceipt, type AttemptReceipt } from '../registration/receipt.js';
|
|
15
15
|
import { appendReceipt } from '../journal/journal.js';
|
|
16
|
+
import type { SourceCache } from '../cache/source-cache.js';
|
|
16
17
|
import type { ValidationSnapshot } from '../registration/snapshot.js';
|
|
17
18
|
import { inspectMarketplaceEntries } from './inspection.js';
|
|
18
19
|
|
|
19
20
|
export interface InstallationFlowOptions {
|
|
20
21
|
cwd?: string;
|
|
21
22
|
agentDir?: string;
|
|
23
|
+
cache?: SourceCache;
|
|
22
24
|
projectTrusted?: boolean;
|
|
23
25
|
fenceTimeoutMs?: number;
|
|
24
26
|
/** Integration synchronization seam; production callers leave this undefined. */
|
|
@@ -148,18 +150,16 @@ async function makePreflight(
|
|
|
148
150
|
operationFinding(scope, CODE.INSTALLATION_NOT_FOUND, RULE.INSTALLATION_NOT_FOUND, `Registration '${registrationId}' is not in ${scope} Bridge State`, 'registration'),
|
|
149
151
|
], null, opts);
|
|
150
152
|
}
|
|
151
|
-
if (registration.sourceKind !== 'local' || !registration.source) {
|
|
152
|
-
return blocked(operation, scope, registrationId, entryPointer, state.stateRevision, [
|
|
153
|
-
operationFinding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'This Registration has no retained local Validation Snapshot tree; Git Installation waits for the Source Cache lifecycle', 'registration'),
|
|
154
|
-
], null, opts);
|
|
155
|
-
}
|
|
156
153
|
|
|
157
154
|
const fenceResult = await acquireAttemptFence(scope, { cwd: opts.cwd, agentDir: opts.agentDir, fenceTimeoutMs: opts.fenceTimeoutMs, projectTrusted: opts.projectTrusted });
|
|
158
155
|
if (!fenceResult.ok) return blocked(operation, scope, registrationId, entryPointer, state.stateRevision, [fenceResult.finding!], null, opts);
|
|
159
156
|
const fence = fenceResult.handle!;
|
|
160
157
|
|
|
161
158
|
try {
|
|
162
|
-
const inspection = inspectMarketplaceEntries(registration, scope
|
|
159
|
+
const inspection = inspectMarketplaceEntries(registration, scope, {
|
|
160
|
+
agentDir: opts.agentDir,
|
|
161
|
+
cache: opts.cache,
|
|
162
|
+
});
|
|
163
163
|
const inspected = inspection.entries.find((item) => item.entry.entryId === entryPointer);
|
|
164
164
|
const rejectedFindings = inspected?.findings ?? (inspection.findings.length > 0 ? inspection.findings : [
|
|
165
165
|
operationFinding(scope, CODE.INSTALLATION_NOT_FOUND, RULE.INSTALLATION_NOT_FOUND, `Marketplace Entry '${entryPointer}' is Unavailable`, 'entry'),
|
|
@@ -315,7 +315,10 @@ export async function confirmPluginInstallation(
|
|
|
315
315
|
if (current.state!.stateRevision !== preflight.stateRevision) {
|
|
316
316
|
return rejectedAsStale(preflight, 'State Revision changed since Installation disclosure; re-run preflight and confirmation', opts);
|
|
317
317
|
}
|
|
318
|
-
const fresh = inspectMarketplaceEntries(preflight.registration, preflight.scope
|
|
318
|
+
const fresh = inspectMarketplaceEntries(preflight.registration, preflight.scope, {
|
|
319
|
+
agentDir: opts.agentDir,
|
|
320
|
+
cache: opts.cache,
|
|
321
|
+
});
|
|
319
322
|
if (!fresh.snapshot || fresh.snapshot.fingerprint !== preflight.snapshot.fingerprint) {
|
|
320
323
|
return rejectedAsStale(preflight, 'Validation Snapshot changed since Installation disclosure; re-run preflight and confirmation', opts);
|
|
321
324
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import { lstatSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
12
|
import { classifyPlugin, type CompatiblePlugin } from '../compatibility/profile.js';
|
|
@@ -15,8 +15,9 @@ import type { MarketplaceEntry } from '../registration/catalog.js';
|
|
|
15
15
|
import { parseCatalog } from '../registration/catalog.js';
|
|
16
16
|
import { resolveContained } from '../registration/contained.js';
|
|
17
17
|
import { CODE, RULE, blocking, hasBlocking, sortFindings, type ValidationFinding } from '../registration/findings.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { SourceCache } from '../cache/source-cache.js';
|
|
19
|
+
import { localSourceKey, type SourceKey } from '../registration/source-key.js';
|
|
20
|
+
import { bindCapturedMaterial, buildGitSnapshot, buildLocalSnapshot, type ValidationSnapshot } from '../registration/snapshot.js';
|
|
20
21
|
import { BUDGET } from '../registration/budget.js';
|
|
21
22
|
|
|
22
23
|
export interface InspectedMarketplaceEntry {
|
|
@@ -43,6 +44,8 @@ export interface InspectionOptions {
|
|
|
43
44
|
baseSnapshot?: ValidationSnapshot;
|
|
44
45
|
/** Suppress the recorded-snapshot drift Blocking Finding — Refresh compares fingerprints explicitly instead. */
|
|
45
46
|
ignoreRecordedDrift?: boolean;
|
|
47
|
+
agentDir?: string;
|
|
48
|
+
cache?: SourceCache;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
function inspectionFinding(scope: Scope, code: string, rule: string, target: ValidationFinding['target'], outcome: string): ValidationFinding {
|
|
@@ -55,11 +58,60 @@ export function inspectMarketplaceEntries(registration: Registration, scope: Sco
|
|
|
55
58
|
let root: string;
|
|
56
59
|
let snapshotResult: { ok: boolean; snapshot?: ValidationSnapshot; findings: ValidationFinding[] };
|
|
57
60
|
if (override) {
|
|
58
|
-
|
|
61
|
+
try {
|
|
62
|
+
root = realpathSync.native(opts.root!);
|
|
63
|
+
} catch {
|
|
64
|
+
root = opts.root!;
|
|
65
|
+
}
|
|
59
66
|
snapshotResult = { ok: true, snapshot: opts.baseSnapshot, findings: [] };
|
|
60
|
-
} else {
|
|
61
|
-
if (
|
|
62
|
-
return {
|
|
67
|
+
} else if (registration.sourceKind === 'git') {
|
|
68
|
+
if (!registration.validationSnapshot) {
|
|
69
|
+
return {
|
|
70
|
+
entries: [],
|
|
71
|
+
findings: [inspectionFinding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', 'Git Registration has no retained Validation Snapshot; re-registration or Marketplace Refresh is required')],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const cache = opts.cache ?? new SourceCache({ agentDir: opts.agentDir });
|
|
75
|
+
const hit = cache.hitExactSync(registration.validationSnapshot);
|
|
76
|
+
if (!hit) {
|
|
77
|
+
return {
|
|
78
|
+
entries: [],
|
|
79
|
+
findings: [inspectionFinding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', `Git Source Cache miss: Validation Snapshot '${registration.validationSnapshot.slice(0, 16)}…' is not retained in Source Cache; Marketplace Refresh or re-acquisition is required`)],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
root = realpathSync.native(hit.path);
|
|
84
|
+
} catch {
|
|
85
|
+
root = hit.path;
|
|
86
|
+
}
|
|
87
|
+
const canonicalLocator = registration.canonicalLocator ?? registration.source ?? '';
|
|
88
|
+
const selectorCanonical = registration.gitSelector?.canonical ?? '';
|
|
89
|
+
const resolvedRevision = registration.resolvedRevision ?? '';
|
|
90
|
+
const sourceKey: SourceKey = registration.sourceKey ?? {
|
|
91
|
+
kind: 'git',
|
|
92
|
+
key: `git:${canonicalLocator}#${selectorCanonical}`,
|
|
93
|
+
canonicalUrl: canonicalLocator,
|
|
94
|
+
selector: selectorCanonical,
|
|
95
|
+
resolvedRevision,
|
|
96
|
+
};
|
|
97
|
+
const snapResult = buildGitSnapshot(root, sourceKey, scope, {
|
|
98
|
+
canonicalLocator,
|
|
99
|
+
resolvedRevision,
|
|
100
|
+
selectorCanonical,
|
|
101
|
+
});
|
|
102
|
+
if (!snapResult.ok || !snapResult.snapshot) {
|
|
103
|
+
return { entries: [], findings: snapResult.findings };
|
|
104
|
+
}
|
|
105
|
+
if (snapResult.snapshot.fingerprint !== registration.validationSnapshot) {
|
|
106
|
+
return {
|
|
107
|
+
entries: [],
|
|
108
|
+
findings: [inspectionFinding(scope, CODE.SOURCE_DRIFT, RULE.SOURCE_DRIFT, 'registration', `Source Drift: cached tree at fingerprint ${registration.validationSnapshot.slice(0, 16)}… no longer hashes to the recorded Validation Snapshot; Marketplace Refresh is required`)],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
snapshotResult = snapResult;
|
|
112
|
+
} else if (registration.sourceKind === 'local' || (!registration.sourceKind && registration.source && !registration.canonicalLocator)) {
|
|
113
|
+
if (!registration.source) {
|
|
114
|
+
return { entries: [], findings: [inspectionFinding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', 'Local Registration has no source path')] };
|
|
63
115
|
}
|
|
64
116
|
const key = localSourceKey(registration.source);
|
|
65
117
|
if (!key.ok) return { entries: [], findings: [inspectionFinding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', key.error ?? 'Marketplace Root cannot be revalidated')] };
|
|
@@ -74,6 +126,11 @@ export function inspectMarketplaceEntries(registration: Registration, scope: Sco
|
|
|
74
126
|
}
|
|
75
127
|
snapshotResult = buildLocalSnapshot(root, key.sourceKey!, scope);
|
|
76
128
|
if (!snapshotResult.ok) return { entries: [], findings: snapshotResult.findings };
|
|
129
|
+
} else {
|
|
130
|
+
return {
|
|
131
|
+
entries: [],
|
|
132
|
+
findings: [inspectionFinding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', `Unknown or unsupported sourceKind '${registration.sourceKind}'`)],
|
|
133
|
+
};
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
const catalogPath = join(root, '.agents', 'plugins', 'marketplace.json');
|
|
@@ -125,7 +125,7 @@ export function projectEffectiveState(
|
|
|
125
125
|
const findings: ValidationFinding[] = [];
|
|
126
126
|
|
|
127
127
|
const inspections = new Map<string, MarketplaceInspection>();
|
|
128
|
-
const inspect = opts.inspectRegistration ?? ((registration: EffectiveRegistration) => inspectMarketplaceEntries(registration, registration.sourceScope));
|
|
128
|
+
const inspect = opts.inspectRegistration ?? ((registration: EffectiveRegistration) => inspectMarketplaceEntries(registration, registration.sourceScope, { agentDir: opts.agentDir }));
|
|
129
129
|
const inspectionFor = (registration: EffectiveRegistration): MarketplaceInspection => {
|
|
130
130
|
const key = `${registration.sourceScope}:${registration.id}`;
|
|
131
131
|
let value = inspections.get(key);
|
|
@@ -69,7 +69,12 @@ export function resolveContained(root: string, relPath: string, checkType: 'any'
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
let canonicalRoot = root;
|
|
73
|
+
try {
|
|
74
|
+
canonicalRoot = realpathSync.native(root);
|
|
75
|
+
} catch {}
|
|
76
|
+
|
|
77
|
+
const abs = canonicalRoot + sep + relPath.slice(2);
|
|
73
78
|
let canonical: string;
|
|
74
79
|
try {
|
|
75
80
|
canonical = realpathSync.native(abs);
|
|
@@ -80,7 +85,7 @@ export function resolveContained(root: string, relPath: string, checkType: 'any'
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
// containment check
|
|
83
|
-
if (!isWithin(
|
|
88
|
+
if (!isWithin(canonicalRoot, canonical)) {
|
|
84
89
|
return {
|
|
85
90
|
outcome: { kind: 'blocking', blockClass: 'path', reason: `canonical target '${canonical}' escapes owning root '${root}'` },
|
|
86
91
|
type: 'special',
|