@anytio/pspm 0.12.0 → 0.13.0
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/CHANGELOG.md +12 -1
- package/CLI_GUIDE.md +241 -6
- package/README.md +1 -1
- package/dist/index.js +199 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to the PSPM CLI will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Client-side encryption for private packages**: Encrypt skill packages before publishing with AES-256-GCM encryption
|
|
13
|
+
- `pspm config set-encryption-key` — Set an encryption key for a scope (`@user/x` or `@org/x`)
|
|
14
|
+
- `pspm config get-encryption-key` — Check if an encryption key is set for a scope
|
|
15
|
+
- `pspm config remove-encryption-key` — Remove an encryption key for a scope
|
|
16
|
+
- Private packages are automatically encrypted on publish and decrypted on install when a key is configured
|
|
17
|
+
- Uses scrypt key derivation for secure key management
|
|
18
|
+
|
|
8
19
|
## [0.12.0] - 2026-03-19
|
|
9
20
|
|
|
10
21
|
### Added
|
|
@@ -103,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
103
114
|
- **`audit` command**: Verify integrity of installed skills
|
|
104
115
|
- Checks for missing packages, deprecated versions, corrupted installations
|
|
105
116
|
- `--json` flag for CI integration
|
|
106
|
-
- **Expanded agent support**: From 6 to 41 supported AI
|
|
117
|
+
- **Expanded agent support**: From 6 to 41 supported AI agents
|
|
107
118
|
- Added Windsurf, Amp, Augment, Cline, Continue, Goose, Kilo Code, Kiro CLI, OpenCode, OpenHands, Replit, Roo Code, Trae, and 22 more
|
|
108
119
|
|
|
109
120
|
### Changed
|
package/CLI_GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PSPM CLI Guide
|
|
2
2
|
|
|
3
|
-
PSPM is a package manager for AI agent skills. It provides commands for authentication, configuration, skill management, and publishing across AI
|
|
3
|
+
PSPM is a package manager for AI agent skills. It provides commands for authentication, configuration, skill management, and publishing across AI agents.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ Options:
|
|
|
24
24
|
-h, --help display help for command
|
|
25
25
|
|
|
26
26
|
Commands:
|
|
27
|
-
config Manage PSPM configuration
|
|
27
|
+
config Manage PSPM configuration (show, init, set-encryption-key, get-encryption-key, remove-encryption-key)
|
|
28
28
|
login [options] Log in via browser or with an API key
|
|
29
29
|
logout Log out and clear stored credentials
|
|
30
30
|
whoami Show current user information
|
|
@@ -36,11 +36,17 @@ Commands:
|
|
|
36
36
|
install|i [options] [specifiers...] Install skills from lockfile, or add and install specific packages
|
|
37
37
|
link [options] Recreate agent symlinks without reinstalling
|
|
38
38
|
update [options] Update all skills to latest compatible versions
|
|
39
|
+
search|find [options] [query] Search and discover skills from the registry
|
|
40
|
+
audit [options] Verify integrity of installed skills
|
|
41
|
+
outdated [options] [packages...] Check for outdated skills
|
|
39
42
|
version <bump> Bump package version (major, minor, patch)
|
|
40
43
|
publish [options] Publish current directory as a skill
|
|
41
44
|
unpublish [options] <specifier> Remove a published skill version (only within 72 hours of publishing)
|
|
42
45
|
access [options] [specifier] Change package visibility (public/private)
|
|
43
46
|
deprecate [options] <specifier> [message] Mark a skill version as deprecated (alternative to unpublish after 72 hours)
|
|
47
|
+
skill-list Manage skill lists (list, create, show, update, delete, add-skill, remove-skill, install)
|
|
48
|
+
notebook Manage notebooks (upload, list, download, delete)
|
|
49
|
+
upgrade Update pspm itself to the latest version
|
|
44
50
|
help [command] display help for command
|
|
45
51
|
```
|
|
46
52
|
|
|
@@ -171,6 +177,7 @@ pspm add @user/alice/skill1 @user/bob/skill2
|
|
|
171
177
|
pspm add @user/skill --agent claude-code,cursor # Link to multiple agents
|
|
172
178
|
pspm add github:owner/repo --agent none # Skip symlink creation
|
|
173
179
|
pspm add @user/skill -y # Skip agent selection prompt
|
|
180
|
+
pspm add @user/skill -g # Install to user home directory
|
|
174
181
|
```
|
|
175
182
|
|
|
176
183
|
### Remove Skill
|
|
@@ -193,6 +200,9 @@ pspm ls
|
|
|
193
200
|
# JSON output for scripting
|
|
194
201
|
pspm list --json
|
|
195
202
|
|
|
203
|
+
# List global skills
|
|
204
|
+
pspm list -g
|
|
205
|
+
|
|
196
206
|
# Example output:
|
|
197
207
|
# Installed skills:
|
|
198
208
|
#
|
|
@@ -219,6 +229,7 @@ pspm install --dir ./vendor/skills # Install to specific directory
|
|
|
219
229
|
pspm install --agent claude-code,cursor # Link to multiple agents
|
|
220
230
|
pspm install --agent none # Skip symlink creation
|
|
221
231
|
pspm install -y # Skip agent selection prompt
|
|
232
|
+
pspm install -g # Install to user home directory
|
|
222
233
|
|
|
223
234
|
# Install specific packages (like npm):
|
|
224
235
|
pspm install @user/alice/skill1 github:org/repo
|
|
@@ -236,6 +247,7 @@ Recreate agent symlinks without reinstalling (useful after adding agents):
|
|
|
236
247
|
pspm link
|
|
237
248
|
pspm link --agent claude-code,cursor # Link to specific agents
|
|
238
249
|
pspm link -y # Skip agent selection prompt
|
|
250
|
+
pspm link -g # Recreate global agent symlinks
|
|
239
251
|
```
|
|
240
252
|
|
|
241
253
|
### Update Skills
|
|
@@ -245,6 +257,39 @@ pspm update
|
|
|
245
257
|
pspm update --dry-run # Preview updates without applying
|
|
246
258
|
```
|
|
247
259
|
|
|
260
|
+
### Search Skills
|
|
261
|
+
|
|
262
|
+
Search and discover skills from the registry:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
pspm search typescript # Search by keyword
|
|
266
|
+
pspm find react # Alias for search
|
|
267
|
+
pspm search react --json # JSON output
|
|
268
|
+
pspm search --sort recent --limit 10
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Check Outdated Skills
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
pspm outdated # Check all packages
|
|
275
|
+
pspm outdated code-review # Check specific package
|
|
276
|
+
pspm outdated --json # JSON output
|
|
277
|
+
pspm outdated --all # Include up-to-date packages
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Exits with code `1` if any packages are outdated.
|
|
281
|
+
|
|
282
|
+
### Audit Skills
|
|
283
|
+
|
|
284
|
+
Verify integrity of installed skills:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
pspm audit # Human-readable output
|
|
288
|
+
pspm audit --json # JSON output (for CI)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Checks for: missing packages, deprecated versions, corrupted installations.
|
|
292
|
+
|
|
248
293
|
## Versioning
|
|
249
294
|
|
|
250
295
|
### Bump Version
|
|
@@ -276,12 +321,15 @@ The command:
|
|
|
276
321
|
Publish the current directory as a skill:
|
|
277
322
|
|
|
278
323
|
```bash
|
|
279
|
-
pspm publish
|
|
280
|
-
pspm publish --
|
|
281
|
-
pspm publish --
|
|
282
|
-
pspm publish --access public #
|
|
324
|
+
pspm publish --access public # Publish as public
|
|
325
|
+
pspm publish --access private # Publish as private
|
|
326
|
+
pspm publish --access team --org myorg # Publish under org
|
|
327
|
+
pspm publish --access public --bump patch # Auto-bump version
|
|
328
|
+
pspm publish --access public --bump minor --tag beta
|
|
283
329
|
```
|
|
284
330
|
|
|
331
|
+
The `--access` flag is required and must be `public`, `private`, or `team`.
|
|
332
|
+
|
|
285
333
|
**Required `pspm.json` fields:**
|
|
286
334
|
- `name` - Skill name (e.g., `@user/username/skillname`)
|
|
287
335
|
- `version` - Semver version
|
|
@@ -338,6 +386,88 @@ pspm access @user/bsheng/vite_slides --public
|
|
|
338
386
|
- **Private packages** (default): Require authentication to download
|
|
339
387
|
- **Public packages**: Anyone can download without authentication
|
|
340
388
|
|
|
389
|
+
## Client-Side Encryption
|
|
390
|
+
|
|
391
|
+
Private packages can be encrypted before upload so that the PSPM server and storage (R2) only ever see ciphertext. The encryption key never leaves your machine.
|
|
392
|
+
|
|
393
|
+
### How It Works
|
|
394
|
+
|
|
395
|
+
- **Publish:** If an encryption key is set for the package scope, the CLI encrypts the tarball with AES-256-GCM before uploading. The server stores only ciphertext.
|
|
396
|
+
- **Install:** The CLI checks the package manifest for encryption metadata. If present, it decrypts the tarball locally before extracting.
|
|
397
|
+
- **Public packages** are never encrypted — encryption only applies to `private` and `team` visibility.
|
|
398
|
+
|
|
399
|
+
### Set an Encryption Key
|
|
400
|
+
|
|
401
|
+
Each scope (`@user/yourname` or `@org/orgname`) has one encryption key. All private packages under that scope use the same key.
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# Set encryption key for your user scope
|
|
405
|
+
pspm config set-encryption-key @user/yourname my-secret-passphrase
|
|
406
|
+
|
|
407
|
+
# Set encryption key for an organization
|
|
408
|
+
pspm config set-encryption-key @org/myorg shared-team-secret
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Or use environment variables:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
export PSPM_ENCRYPTION_KEY_USER_YOURNAME="my-secret-passphrase"
|
|
415
|
+
export PSPM_ENCRYPTION_KEY_ORG_MYORG="shared-team-secret"
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Manage Encryption Keys
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
# Check if a key is set
|
|
422
|
+
pspm config get-encryption-key @user/yourname
|
|
423
|
+
|
|
424
|
+
# Remove a key
|
|
425
|
+
pspm config remove-encryption-key @user/yourname
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Publish with Encryption
|
|
429
|
+
|
|
430
|
+
When you publish a private package and an encryption key is configured for the scope, the CLI automatically encrypts:
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
pspm config set-encryption-key @user/yourname my-secret
|
|
434
|
+
pspm publish --access private
|
|
435
|
+
# Output: pspm notice Encrypting package (scope: @user/yourname)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
If no encryption key is set, the package is uploaded unencrypted with a warning.
|
|
439
|
+
|
|
440
|
+
### Install Encrypted Packages
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
# Set the same key used during publish
|
|
444
|
+
pspm config set-encryption-key @user/yourname my-secret
|
|
445
|
+
|
|
446
|
+
# Install as usual — decryption is automatic
|
|
447
|
+
pspm install
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
If you don't have the key, the CLI will show an error with instructions:
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
Error: Package @user/yourname/my-skill is encrypted.
|
|
454
|
+
Set the key: pspm config set-encryption-key @user/yourname <passphrase>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Team Sharing
|
|
458
|
+
|
|
459
|
+
For organization packages, share the encryption key with team members through a secure channel (e.g., a password manager). Each team member adds it to their local config:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
pspm config set-encryption-key @org/myorg shared-team-secret
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Important Notes
|
|
466
|
+
|
|
467
|
+
- **Key loss = data loss.** If you lose your encryption key, encrypted packages cannot be recovered. Back up your keys.
|
|
468
|
+
- The server stores encryption metadata (algorithm, salt, IV) alongside the package — these are not secrets and are safe to store publicly.
|
|
469
|
+
- Encryption is opt-in. If no key is configured, private packages are uploaded unencrypted.
|
|
470
|
+
|
|
341
471
|
## Configuration Files
|
|
342
472
|
|
|
343
473
|
### User Config: `~/.pspmrc`
|
|
@@ -357,6 +487,10 @@ username = myuser
|
|
|
357
487
|
; Multi-registry: Per-registry tokens (optional)
|
|
358
488
|
//pspm.dev:authToken = sk_public_token
|
|
359
489
|
//corp.pspm.io:authToken = sk_corp_token
|
|
490
|
+
|
|
491
|
+
; Encryption keys (optional)
|
|
492
|
+
encryption-key:@user/yourname = my-secret-passphrase
|
|
493
|
+
encryption-key:@org/myorg = shared-team-secret
|
|
360
494
|
```
|
|
361
495
|
|
|
362
496
|
### Project Config: `.pspmrc`
|
|
@@ -441,6 +575,7 @@ Configuration is resolved in priority order:
|
|
|
441
575
|
| `PSPM_API_KEY` | Override API key |
|
|
442
576
|
| `PSPM_DEBUG` | Enable debug logging |
|
|
443
577
|
| `GITHUB_TOKEN` | GitHub token for private repos and higher rate limits |
|
|
578
|
+
| `PSPM_ENCRYPTION_KEY_<SCOPE>` | Encryption key for a scope (e.g., `PSPM_ENCRYPTION_KEY_USER_ALICE`) |
|
|
444
579
|
|
|
445
580
|
## Directory Structure
|
|
446
581
|
|
|
@@ -517,6 +652,106 @@ pspm init
|
|
|
517
652
|
pspm publish --bump patch
|
|
518
653
|
```
|
|
519
654
|
|
|
655
|
+
## Skill Lists
|
|
656
|
+
|
|
657
|
+
### List Skill Lists
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
pspm skill-list list # Your lists
|
|
661
|
+
pspm skill-list list --org myorg # Organization's lists
|
|
662
|
+
pspm skill-list list --json # JSON output
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Create Skill List
|
|
666
|
+
|
|
667
|
+
```bash
|
|
668
|
+
pspm skill-list create my-favorites
|
|
669
|
+
pspm skill-list create my-favorites --visibility public
|
|
670
|
+
pspm skill-list create team-tools --org myorg -d "Our team's tools"
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Show Skill List
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
pspm skill-list show @user/alice/my-favorites
|
|
677
|
+
pspm skill-list show @org/myorg/team-tools --json
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Update Skill List
|
|
681
|
+
|
|
682
|
+
```bash
|
|
683
|
+
pspm skill-list update @user/alice/my-favorites --description "Updated desc"
|
|
684
|
+
pspm skill-list update @user/alice/my-favorites --visibility public
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Delete Skill List
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
pspm skill-list delete @user/alice/my-favorites
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Add Skill to List
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
pspm skill-list add-skill @user/alice/my-favorites @user/bob/code-review
|
|
697
|
+
pspm skill-list add-skill @user/alice/my-favorites @user/bob/lint --note "Great for CI"
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Remove Skill from List
|
|
701
|
+
|
|
702
|
+
```bash
|
|
703
|
+
pspm skill-list remove-skill @user/alice/my-favorites @user/bob/code-review
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Install from Skill List
|
|
707
|
+
|
|
708
|
+
```bash
|
|
709
|
+
pspm skill-list install @user/alice/my-favorites
|
|
710
|
+
pspm skill-list install @org/myorg/team-tools --agent claude-code
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
## Notebook Management
|
|
714
|
+
|
|
715
|
+
### Upload Notebook
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
pspm notebook upload notebook.anyt.md
|
|
719
|
+
pspm notebook upload notebook.anyt.md --visibility public
|
|
720
|
+
pspm notebook upload notebook.anyt.md --org myorg
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### List Notebooks
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
pspm notebook list
|
|
727
|
+
pspm notebook list --org myorg
|
|
728
|
+
pspm notebook list --json
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Download Notebook
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
pspm notebook download <id>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Delete Notebook
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
pspm notebook delete <id>
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
## Self-Update
|
|
744
|
+
|
|
745
|
+
### Upgrade PSPM
|
|
746
|
+
|
|
747
|
+
Update pspm itself to the latest version:
|
|
748
|
+
|
|
749
|
+
```bash
|
|
750
|
+
pspm upgrade
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
Auto-detects your package manager (pnpm, npm, yarn, bun). The CLI also checks for updates every 24 hours and notifies you when a newer version is available.
|
|
754
|
+
|
|
520
755
|
## Troubleshooting
|
|
521
756
|
|
|
522
757
|
| Error | Solution |
|
package/README.md
CHANGED
|
@@ -445,7 +445,7 @@ project/
|
|
|
445
445
|
| | +-- _local/ # Local skill symlinks
|
|
446
446
|
| +-- cache/ # Tarball cache
|
|
447
447
|
+-- .claude/
|
|
448
|
-
| +-- skills/ # Symlinks for Claude Code
|
|
448
|
+
| +-- skills/ # Symlinks for Claude Code (and other agents)
|
|
449
449
|
+-- .cursor/
|
|
450
450
|
+-- skills/ # Symlinks for Cursor (if configured)
|
|
451
451
|
```
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { stat, writeFile, readdir, mkdir, rm, rename, access as access$1, readFi
|
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { join, dirname, basename, resolve, relative } from 'path';
|
|
5
5
|
import * as ini from 'ini';
|
|
6
|
+
import { createHash, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
6
7
|
import ignore from 'ignore';
|
|
7
|
-
import { createHash, randomBytes } from 'crypto';
|
|
8
8
|
import * as semver2 from 'semver';
|
|
9
9
|
import semver2__default from 'semver';
|
|
10
10
|
import { checkbox } from '@inquirer/prompts';
|
|
@@ -603,6 +603,8 @@ __export(config_exports, {
|
|
|
603
603
|
findProjectConfig: () => findProjectConfig,
|
|
604
604
|
getCacheDir: () => getCacheDir,
|
|
605
605
|
getConfigPath: () => getConfigPath,
|
|
606
|
+
getEncryptionKey: () => getEncryptionKey,
|
|
607
|
+
getEncryptionScope: () => getEncryptionScope,
|
|
606
608
|
getLegacyLockfilePath: () => getLegacyLockfilePath,
|
|
607
609
|
getLegacySkillsDir: () => getLegacySkillsDir,
|
|
608
610
|
getLockfilePath: () => getLockfilePath,
|
|
@@ -613,9 +615,11 @@ __export(config_exports, {
|
|
|
613
615
|
isGlobalMode: () => isGlobalMode,
|
|
614
616
|
isLoggedIn: () => isLoggedIn,
|
|
615
617
|
readUserConfig: () => readUserConfig,
|
|
618
|
+
removeEncryptionKey: () => removeEncryptionKey,
|
|
616
619
|
requireApiKey: () => requireApiKey,
|
|
617
620
|
resolveConfig: () => resolveConfig,
|
|
618
621
|
setCredentials: () => setCredentials,
|
|
622
|
+
setEncryptionKey: () => setEncryptionKey,
|
|
619
623
|
setGlobalMode: () => setGlobalMode,
|
|
620
624
|
writeUserConfig: () => writeUserConfig
|
|
621
625
|
});
|
|
@@ -685,12 +689,21 @@ async function readUserConfig() {
|
|
|
685
689
|
}
|
|
686
690
|
}
|
|
687
691
|
}
|
|
692
|
+
const encryptionKeys = {};
|
|
693
|
+
for (const key of Object.keys(parsed)) {
|
|
694
|
+
const encKeyMatch = key.match(/^encryption-key:(.+)$/);
|
|
695
|
+
if (encKeyMatch) {
|
|
696
|
+
const scope = encKeyMatch[1];
|
|
697
|
+
encryptionKeys[scope] = parsed[key];
|
|
698
|
+
}
|
|
699
|
+
}
|
|
688
700
|
return {
|
|
689
701
|
registry: parsed.registry,
|
|
690
702
|
authToken: parsed.authToken,
|
|
691
703
|
username: parsed.username,
|
|
692
704
|
scopedRegistries: Object.keys(scopedRegistries).length > 0 ? scopedRegistries : void 0,
|
|
693
|
-
registryTokens: Object.keys(registryTokens).length > 0 ? registryTokens : void 0
|
|
705
|
+
registryTokens: Object.keys(registryTokens).length > 0 ? registryTokens : void 0,
|
|
706
|
+
encryptionKeys: Object.keys(encryptionKeys).length > 0 ? encryptionKeys : void 0
|
|
694
707
|
};
|
|
695
708
|
} catch (error) {
|
|
696
709
|
if (process.env.PSPM_DEBUG) {
|
|
@@ -713,6 +726,12 @@ async function writeUserConfig(config2) {
|
|
|
713
726
|
if (config2.username) {
|
|
714
727
|
lines.push(`username = ${config2.username}`);
|
|
715
728
|
}
|
|
729
|
+
if (config2.encryptionKeys && Object.keys(config2.encryptionKeys).length > 0) {
|
|
730
|
+
lines.push("; Encryption keys (scope -> passphrase)");
|
|
731
|
+
for (const [scope, key] of Object.entries(config2.encryptionKeys)) {
|
|
732
|
+
lines.push(`encryption-key:${scope} = ${key}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
716
735
|
lines.push("");
|
|
717
736
|
await mkdir(dirname(configPath), { recursive: true });
|
|
718
737
|
await writeFile(configPath, lines.join("\n"));
|
|
@@ -896,6 +915,36 @@ async function getRegistryUrl() {
|
|
|
896
915
|
const resolved = await resolveConfig();
|
|
897
916
|
return resolved.registryUrl;
|
|
898
917
|
}
|
|
918
|
+
async function getEncryptionKey(scope) {
|
|
919
|
+
const envSuffix = scope.replace(/^@/, "").replace(/\//g, "_").toUpperCase();
|
|
920
|
+
const envKey = process.env[`PSPM_ENCRYPTION_KEY_${envSuffix}`];
|
|
921
|
+
if (envKey) {
|
|
922
|
+
return envKey;
|
|
923
|
+
}
|
|
924
|
+
const config2 = await readUserConfig();
|
|
925
|
+
return config2.encryptionKeys?.[scope];
|
|
926
|
+
}
|
|
927
|
+
async function setEncryptionKey(scope, key) {
|
|
928
|
+
const config2 = await readUserConfig();
|
|
929
|
+
if (!config2.encryptionKeys) {
|
|
930
|
+
config2.encryptionKeys = {};
|
|
931
|
+
}
|
|
932
|
+
config2.encryptionKeys[scope] = key;
|
|
933
|
+
await writeUserConfig(config2);
|
|
934
|
+
}
|
|
935
|
+
async function removeEncryptionKey(scope) {
|
|
936
|
+
const config2 = await readUserConfig();
|
|
937
|
+
if (config2.encryptionKeys) {
|
|
938
|
+
delete config2.encryptionKeys[scope];
|
|
939
|
+
if (Object.keys(config2.encryptionKeys).length === 0) {
|
|
940
|
+
config2.encryptionKeys = void 0;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
await writeUserConfig(config2);
|
|
944
|
+
}
|
|
945
|
+
function getEncryptionScope(namespace, owner) {
|
|
946
|
+
return `@${namespace}/${owner}`;
|
|
947
|
+
}
|
|
899
948
|
var DEFAULT_REGISTRY_URL, _globalMode;
|
|
900
949
|
var init_config = __esm({
|
|
901
950
|
"src/config.ts"() {
|
|
@@ -904,6 +953,62 @@ var init_config = __esm({
|
|
|
904
953
|
_globalMode = false;
|
|
905
954
|
}
|
|
906
955
|
});
|
|
956
|
+
function deriveKey(passphrase, salt) {
|
|
957
|
+
return scryptSync(passphrase, salt, KEY_LENGTH, {
|
|
958
|
+
N: SCRYPT_COST,
|
|
959
|
+
r: SCRYPT_BLOCK_SIZE,
|
|
960
|
+
p: SCRYPT_PARALLELISM
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
function encryptBuffer(data, passphrase, scope) {
|
|
964
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
965
|
+
const iv = randomBytes(IV_LENGTH);
|
|
966
|
+
const key = deriveKey(passphrase, salt);
|
|
967
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, {
|
|
968
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
969
|
+
});
|
|
970
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
971
|
+
const authTag = cipher.getAuthTag();
|
|
972
|
+
return {
|
|
973
|
+
encrypted,
|
|
974
|
+
metadata: {
|
|
975
|
+
algorithm: ALGORITHM,
|
|
976
|
+
kdf: "scrypt",
|
|
977
|
+
salt: salt.toString("hex"),
|
|
978
|
+
iv: iv.toString("hex"),
|
|
979
|
+
authTag: authTag.toString("hex"),
|
|
980
|
+
scope
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function decryptBuffer(encrypted, passphrase, metadata) {
|
|
985
|
+
const salt = Buffer.from(metadata.salt, "hex");
|
|
986
|
+
const iv = Buffer.from(metadata.iv, "hex");
|
|
987
|
+
const authTag = Buffer.from(metadata.authTag, "hex");
|
|
988
|
+
const key = deriveKey(passphrase, salt);
|
|
989
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, {
|
|
990
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
991
|
+
});
|
|
992
|
+
decipher.setAuthTag(authTag);
|
|
993
|
+
const decrypted = Buffer.concat([
|
|
994
|
+
decipher.update(encrypted),
|
|
995
|
+
decipher.final()
|
|
996
|
+
]);
|
|
997
|
+
return decrypted;
|
|
998
|
+
}
|
|
999
|
+
var ALGORITHM, KEY_LENGTH, IV_LENGTH, SALT_LENGTH, SCRYPT_COST, SCRYPT_BLOCK_SIZE, SCRYPT_PARALLELISM, AUTH_TAG_LENGTH;
|
|
1000
|
+
var init_encryption = __esm({
|
|
1001
|
+
"src/lib/encryption.ts"() {
|
|
1002
|
+
ALGORITHM = "aes-256-gcm";
|
|
1003
|
+
KEY_LENGTH = 32;
|
|
1004
|
+
IV_LENGTH = 16;
|
|
1005
|
+
SALT_LENGTH = 32;
|
|
1006
|
+
SCRYPT_COST = 16384;
|
|
1007
|
+
SCRYPT_BLOCK_SIZE = 8;
|
|
1008
|
+
SCRYPT_PARALLELISM = 1;
|
|
1009
|
+
AUTH_TAG_LENGTH = 16;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
907
1012
|
async function loadIgnorePatterns(cwd = process.cwd()) {
|
|
908
1013
|
const ig = ignore();
|
|
909
1014
|
ig.add(ALWAYS_IGNORED);
|
|
@@ -1626,6 +1731,7 @@ var init_specifier2 = __esm({
|
|
|
1626
1731
|
// src/lib/index.ts
|
|
1627
1732
|
var init_lib = __esm({
|
|
1628
1733
|
"src/lib/index.ts"() {
|
|
1734
|
+
init_encryption();
|
|
1629
1735
|
init_ignore();
|
|
1630
1736
|
init_integrity();
|
|
1631
1737
|
init_lockfile();
|
|
@@ -3539,11 +3645,15 @@ async function installFromLockfile(options) {
|
|
|
3539
3645
|
}
|
|
3540
3646
|
const tarballBuffer = Buffer.from(await tarballResponse.arrayBuffer());
|
|
3541
3647
|
const integrity = calculateIntegrity(tarballBuffer);
|
|
3542
|
-
|
|
3648
|
+
const lockfileEntry = {
|
|
3543
3649
|
version: resolved,
|
|
3544
3650
|
resolved: versionInfo.downloadUrl,
|
|
3545
3651
|
integrity
|
|
3546
|
-
}
|
|
3652
|
+
};
|
|
3653
|
+
if (versionInfo.manifest?.encryption) {
|
|
3654
|
+
lockfileEntry.encryption = versionInfo.manifest.encryption;
|
|
3655
|
+
}
|
|
3656
|
+
await addToLockfile(fullName, lockfileEntry);
|
|
3547
3657
|
await writeToCache(cacheDir, integrity, tarballBuffer);
|
|
3548
3658
|
console.log(` Resolved ${fullName}@${resolved}`);
|
|
3549
3659
|
}
|
|
@@ -3686,6 +3796,28 @@ Installing ${packageCount} registry skill(s)...
|
|
|
3686
3796
|
}
|
|
3687
3797
|
await writeToCache(cacheDir, entry.integrity, tarballBuffer);
|
|
3688
3798
|
}
|
|
3799
|
+
if (entry.encryption) {
|
|
3800
|
+
const scope = entry.encryption.scope;
|
|
3801
|
+
const encKey = await getEncryptionKey(scope);
|
|
3802
|
+
if (!encKey) {
|
|
3803
|
+
console.error(
|
|
3804
|
+
` Error: Package ${fullName} is encrypted. Set the key: pspm config set-encryption-key ${scope} <passphrase>`
|
|
3805
|
+
);
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
try {
|
|
3809
|
+
tarballBuffer = decryptBuffer(
|
|
3810
|
+
tarballBuffer,
|
|
3811
|
+
encKey,
|
|
3812
|
+
entry.encryption
|
|
3813
|
+
);
|
|
3814
|
+
} catch {
|
|
3815
|
+
console.error(
|
|
3816
|
+
` Error: Failed to decrypt ${fullName}. Wrong encryption key for scope "${scope}".`
|
|
3817
|
+
);
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3689
3821
|
const effectiveSkillName = subname ?? name;
|
|
3690
3822
|
let destDir;
|
|
3691
3823
|
if (ns === "org") {
|
|
@@ -3836,6 +3968,7 @@ var init_install = __esm({
|
|
|
3836
3968
|
init_config();
|
|
3837
3969
|
init_errors();
|
|
3838
3970
|
init_github();
|
|
3971
|
+
init_encryption();
|
|
3839
3972
|
init_lib();
|
|
3840
3973
|
init_lockfile3();
|
|
3841
3974
|
init_manifest3();
|
|
@@ -5453,6 +5586,7 @@ function printTable(results) {
|
|
|
5453
5586
|
init_api_client();
|
|
5454
5587
|
init_config();
|
|
5455
5588
|
init_errors();
|
|
5589
|
+
init_encryption();
|
|
5456
5590
|
init_lib();
|
|
5457
5591
|
var exec = promisify(exec$1);
|
|
5458
5592
|
function confirm(question) {
|
|
@@ -5693,19 +5827,53 @@ async function publishCommand(options) {
|
|
|
5693
5827
|
process.exit(0);
|
|
5694
5828
|
}
|
|
5695
5829
|
console.log("");
|
|
5830
|
+
let finalTarballBase64 = tarballBase64;
|
|
5831
|
+
if (options.access === "private" || options.access === "team") {
|
|
5832
|
+
const config2 = await Promise.resolve().then(() => (init_config(), config_exports)).then((m) => m.resolveConfig());
|
|
5833
|
+
const namespace2 = options.org ? "org" : "user";
|
|
5834
|
+
const owner2 = options.org ?? config2.username;
|
|
5835
|
+
if (!owner2) {
|
|
5836
|
+
console.error(
|
|
5837
|
+
"Error: Cannot determine package owner. Run 'pspm login' first."
|
|
5838
|
+
);
|
|
5839
|
+
process.exit(1);
|
|
5840
|
+
}
|
|
5841
|
+
const scope = getEncryptionScope(namespace2, owner2);
|
|
5842
|
+
const encryptionKey = await getEncryptionKey(scope);
|
|
5843
|
+
if (encryptionKey) {
|
|
5844
|
+
console.log(`pspm notice Encrypting package (scope: ${scope})`);
|
|
5845
|
+
const { encrypted, metadata } = encryptBuffer(
|
|
5846
|
+
tarballBuffer,
|
|
5847
|
+
encryptionKey,
|
|
5848
|
+
scope
|
|
5849
|
+
);
|
|
5850
|
+
finalTarballBase64 = encrypted.toString("base64");
|
|
5851
|
+
packageJson2.encryption = metadata;
|
|
5852
|
+
console.log(
|
|
5853
|
+
"pspm notice Package encrypted with client-side encryption"
|
|
5854
|
+
);
|
|
5855
|
+
} else {
|
|
5856
|
+
console.log(
|
|
5857
|
+
`pspm warn No encryption key found for scope "${scope}". Publishing without encryption.`
|
|
5858
|
+
);
|
|
5859
|
+
console.log(
|
|
5860
|
+
`pspm warn To encrypt, run: pspm config set-encryption-key ${scope} <your-passphrase>`
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5696
5864
|
console.log(`pspm notice Publishing to ${registryUrl} with tag latest`);
|
|
5697
5865
|
configure2({ registryUrl, apiKey });
|
|
5698
5866
|
let response;
|
|
5699
5867
|
if (options.org) {
|
|
5700
5868
|
response = await publishOrgSkill(options.org, {
|
|
5701
5869
|
manifest: packageJson2,
|
|
5702
|
-
tarballBase64,
|
|
5870
|
+
tarballBase64: finalTarballBase64,
|
|
5703
5871
|
visibility: options.access
|
|
5704
5872
|
});
|
|
5705
5873
|
} else {
|
|
5706
5874
|
response = await publishSkill({
|
|
5707
5875
|
manifest: packageJson2,
|
|
5708
|
-
tarballBase64,
|
|
5876
|
+
tarballBase64: finalTarballBase64,
|
|
5709
5877
|
visibility: options.access
|
|
5710
5878
|
});
|
|
5711
5879
|
}
|
|
@@ -5731,7 +5899,9 @@ async function publishCommand(options) {
|
|
|
5731
5899
|
`+ @${namespace}/${owner}/${result.skill.name}@${result.version.version}`
|
|
5732
5900
|
);
|
|
5733
5901
|
console.log(`Checksum: ${result.version.checksum}`);
|
|
5734
|
-
console.log(
|
|
5902
|
+
console.log(
|
|
5903
|
+
`Visibility: ${visibilityIcon} ${visibility}${packageJson2.encryption ? " (encrypted)" : ""}`
|
|
5904
|
+
);
|
|
5735
5905
|
if (visibility === "public") {
|
|
5736
5906
|
console.log(
|
|
5737
5907
|
"Note: Public packages cannot be made private. This is irreversible."
|
|
@@ -6729,6 +6899,28 @@ configCmd.command("init").description("Create a .pspmrc file in the current dire
|
|
|
6729
6899
|
registry: options.registry
|
|
6730
6900
|
});
|
|
6731
6901
|
});
|
|
6902
|
+
configCmd.command("set-encryption-key <scope> <passphrase>").description(
|
|
6903
|
+
"Set encryption key for a scope (e.g., pspm config set-encryption-key @user/alice my-secret)"
|
|
6904
|
+
).action(async (scope, passphrase) => {
|
|
6905
|
+
const { setEncryptionKey: setEncryptionKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
6906
|
+
await setEncryptionKey2(scope, passphrase);
|
|
6907
|
+
console.log(`Encryption key set for scope "${scope}"`);
|
|
6908
|
+
});
|
|
6909
|
+
configCmd.command("remove-encryption-key <scope>").description("Remove encryption key for a scope").action(async (scope) => {
|
|
6910
|
+
const { removeEncryptionKey: removeEncryptionKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
6911
|
+
await removeEncryptionKey2(scope);
|
|
6912
|
+
console.log(`Encryption key removed for scope "${scope}"`);
|
|
6913
|
+
});
|
|
6914
|
+
configCmd.command("get-encryption-key <scope>").description("Check if an encryption key is set for a scope").action(async (scope) => {
|
|
6915
|
+
const { getEncryptionKey: getEncryptionKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
6916
|
+
const key = await getEncryptionKey2(scope);
|
|
6917
|
+
if (key) {
|
|
6918
|
+
const masked = `${key.substring(0, 4)}***`;
|
|
6919
|
+
console.log(`Encryption key for "${scope}": ${masked} (set)`);
|
|
6920
|
+
} else {
|
|
6921
|
+
console.log(`No encryption key set for "${scope}"`);
|
|
6922
|
+
}
|
|
6923
|
+
});
|
|
6732
6924
|
program.command("upgrade").description("Upgrade pspm to the latest version").action(async () => {
|
|
6733
6925
|
await upgrade();
|
|
6734
6926
|
});
|