skillrepo 3.0.0 → 3.1.1
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 +74 -6
- package/bin/skillrepo.mjs +14 -0
- package/package.json +1 -1
- package/src/commands/init.mjs +184 -19
- package/src/commands/remove.mjs +8 -13
- package/src/commands/session-sync.mjs +152 -0
- package/src/commands/uninstall.mjs +484 -0
- package/src/commands/update.mjs +125 -8
- package/src/lib/artifact-registry.mjs +305 -0
- package/src/lib/cli-config.mjs +78 -0
- package/src/lib/config.mjs +6 -3
- package/src/lib/file-write.mjs +8 -3
- package/src/lib/fs-utils.mjs +90 -9
- package/src/lib/mergers/session-hook.mjs +378 -0
- package/src/lib/paths.mjs +21 -0
- package/src/lib/platform.mjs +124 -0
- package/src/lib/removers/claude-mcp.mjs +67 -0
- package/src/lib/removers/cursor-mcp.mjs +60 -0
- package/src/lib/removers/env-local.mjs +55 -0
- package/src/lib/removers/gitignore.mjs +108 -0
- package/src/lib/removers/settings.mjs +183 -0
- package/src/lib/removers/vscode-mcp.mjs +87 -0
- package/src/lib/removers/windsurf-mcp.mjs +65 -0
- package/src/lib/sync.mjs +26 -0
- package/src/test/commands/add.test.mjs +10 -4
- package/src/test/commands/get.test.mjs +10 -4
- package/src/test/commands/init.test.mjs +428 -4
- package/src/test/commands/list.test.mjs +10 -4
- package/src/test/commands/remove.test.mjs +10 -4
- package/src/test/commands/search.test.mjs +10 -4
- package/src/test/commands/session-sync.test.mjs +352 -0
- package/src/test/commands/uninstall.test.mjs +774 -0
- package/src/test/commands/update.test.mjs +168 -4
- package/src/test/helpers/sandbox-home.mjs +161 -0
- package/src/test/helpers/skillrepo-shim.mjs +133 -0
- package/src/test/integration/file-write.integration.test.mjs +10 -4
- package/src/test/lib/artifact-registry.test.mjs +268 -0
- package/src/test/lib/cli-config.test.mjs +126 -5
- package/src/test/lib/config.test.mjs +10 -4
- package/src/test/lib/file-write.test.mjs +24 -10
- package/src/test/lib/mcp-merge.test.mjs +10 -4
- package/src/test/lib/paths.test.mjs +10 -4
- package/src/test/lib/platform.test.mjs +135 -0
- package/src/test/lib/sync.test.mjs +20 -4
- package/src/test/mergers/session-hook.test.mjs +1175 -0
- package/src/test/mergers/uninstall-claude-mcp.test.mjs +145 -0
- package/src/test/mergers/uninstall-cursor-mcp.test.mjs +108 -0
- package/src/test/mergers/uninstall-env-local.test.mjs +144 -0
- package/src/test/mergers/uninstall-gitignore.test.mjs +209 -0
- package/src/test/mergers/uninstall-settings.test.mjs +296 -0
- package/src/test/mergers/uninstall-vscode-mcp.test.mjs +215 -0
- package/src/test/mergers/uninstall-windsurf-mcp.test.mjs +128 -0
|
@@ -20,12 +20,18 @@ import { readConfig } from "../../lib/config.mjs";
|
|
|
20
20
|
import { CliError, EXIT_AUTH, EXIT_VALIDATION } from "../../lib/errors.mjs";
|
|
21
21
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
22
22
|
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
23
|
+
import {
|
|
24
|
+
captureHome,
|
|
25
|
+
setSandboxHome,
|
|
26
|
+
restoreHome,
|
|
27
|
+
} from "../helpers/sandbox-home.mjs";
|
|
23
28
|
|
|
24
29
|
let sandbox;
|
|
25
30
|
let server;
|
|
26
31
|
let serverUrl;
|
|
27
32
|
let originalCwd;
|
|
28
|
-
|
|
33
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
34
|
+
let originalHomeEnv;
|
|
29
35
|
let stdout;
|
|
30
36
|
let stderr;
|
|
31
37
|
const VALID_KEY = "sk_live_init_test";
|
|
@@ -38,9 +44,9 @@ async function setup() {
|
|
|
38
44
|
mkdirSync(join(sandbox, "project", ".claude"), { recursive: true });
|
|
39
45
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
40
46
|
originalCwd = process.cwd();
|
|
41
|
-
|
|
47
|
+
originalHomeEnv = captureHome();
|
|
42
48
|
process.chdir(join(sandbox, "project"));
|
|
43
|
-
|
|
49
|
+
setSandboxHome(join(sandbox, "home"));
|
|
44
50
|
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
45
51
|
delete process.env.SKILLREPO_URL;
|
|
46
52
|
|
|
@@ -55,7 +61,7 @@ async function setup() {
|
|
|
55
61
|
async function teardown() {
|
|
56
62
|
if (server) await server.stop();
|
|
57
63
|
process.chdir(originalCwd);
|
|
58
|
-
|
|
64
|
+
restoreHome(originalHomeEnv);
|
|
59
65
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
60
66
|
server = null;
|
|
61
67
|
}
|
|
@@ -484,3 +490,421 @@ describe("runInit — stale-key handling", () => {
|
|
|
484
490
|
);
|
|
485
491
|
});
|
|
486
492
|
});
|
|
493
|
+
|
|
494
|
+
// ── Session-sync step 6 (#884) ────────────────────────────────────────
|
|
495
|
+
//
|
|
496
|
+
// INTENT-based coverage of the new step 6 added in v3.1.0. Tests use
|
|
497
|
+
// the PATH-shim trick from session-sync.test.mjs to make
|
|
498
|
+
// `which skillrepo` resolve deterministically: a fake `skillrepo`
|
|
499
|
+
// executable is dropped into `$HOME/bin` and prepended to PATH.
|
|
500
|
+
// Without this, the behavior of these tests would depend on whether
|
|
501
|
+
// a global install exists on the developer's machine.
|
|
502
|
+
//
|
|
503
|
+
// Lower-level installer correctness (hook shape, idempotency,
|
|
504
|
+
// round-trip with remover) is covered in session-hook.test.mjs. These
|
|
505
|
+
// init tests verify the ORCHESTRATION: --yes path, --no-session-sync
|
|
506
|
+
// opt-out, --json output shape, and the non-fatal disk-error path.
|
|
507
|
+
|
|
508
|
+
import { SESSION_HOOK_FINGERPRINT as _FINGERPRINT } from "../../lib/artifact-registry.mjs";
|
|
509
|
+
import { installShim, uninstallShim } from "../helpers/skillrepo-shim.mjs";
|
|
510
|
+
|
|
511
|
+
/** @type {ReturnType<typeof installShim> | undefined} */
|
|
512
|
+
let _shimHandle;
|
|
513
|
+
|
|
514
|
+
async function setupWithShim() {
|
|
515
|
+
await setup();
|
|
516
|
+
// Drop a cross-platform `skillrepo` shim into HOME/bin so the
|
|
517
|
+
// CLI's binary resolver (which/where) finds it rather than
|
|
518
|
+
// depending on whether the dev/CI machine has a global install.
|
|
519
|
+
_shimHandle = installShim(process.env.HOME);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function teardownWithShim() {
|
|
523
|
+
uninstallShim(_shimHandle);
|
|
524
|
+
_shimHandle = undefined;
|
|
525
|
+
await teardown();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
describe("runInit — session sync (#884)", () => {
|
|
529
|
+
beforeEach(setupWithShim);
|
|
530
|
+
afterEach(teardownWithShim);
|
|
531
|
+
|
|
532
|
+
it("--yes installs the hook at step 6 by default", async () => {
|
|
533
|
+
// INTENT: the architect-designed default for --yes mode is
|
|
534
|
+
// "install the hook." CI/onboarding scripts passing --yes should
|
|
535
|
+
// get a fully-configured project including session sync.
|
|
536
|
+
await runInit(
|
|
537
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
538
|
+
{ stdout, stderr },
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const settingsPath = join(process.cwd(), ".claude", "settings.local.json");
|
|
542
|
+
assert.ok(existsSync(settingsPath), "settings.local.json must exist");
|
|
543
|
+
|
|
544
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
545
|
+
const hasHook = parsed.hooks.SessionStart.flatMap((g) => g.hooks).some(
|
|
546
|
+
(h) => h.command.includes(_FINGERPRINT),
|
|
547
|
+
);
|
|
548
|
+
assert.ok(hasHook, "SkillRepo SessionStart hook must be installed");
|
|
549
|
+
assert.match(stdout.text(), /SessionStart hook installed/);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("--no-session-sync skips the hook install even with --yes", async () => {
|
|
553
|
+
// INTENT: the only way CI scripts that bootstrap a project
|
|
554
|
+
// without starting Claude Code sessions can opt out. Must work
|
|
555
|
+
// alongside --yes (otherwise --yes would force hook install).
|
|
556
|
+
await runInit(
|
|
557
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes", "--no-session-sync"],
|
|
558
|
+
{ stdout, stderr },
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const settingsPath = join(process.cwd(), ".claude", "settings.local.json");
|
|
562
|
+
assert.ok(
|
|
563
|
+
!existsSync(settingsPath),
|
|
564
|
+
"settings.local.json must NOT be written under --no-session-sync",
|
|
565
|
+
);
|
|
566
|
+
assert.match(stdout.text(), /Session sync skipped \(--no-session-sync\)/);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("re-running init is idempotent — exactly one hook entry", async () => {
|
|
570
|
+
// INTENT: users re-run init for many reasons (switching keys,
|
|
571
|
+
// updating the config). A duplicate hook would fire sync twice
|
|
572
|
+
// per session — waste at best, race at worst.
|
|
573
|
+
await runInit(
|
|
574
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
575
|
+
{ stdout, stderr },
|
|
576
|
+
);
|
|
577
|
+
stdout.clear();
|
|
578
|
+
await runInit(
|
|
579
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
580
|
+
{ stdout, stderr },
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const parsed = JSON.parse(
|
|
584
|
+
readFileSync(
|
|
585
|
+
join(process.cwd(), ".claude", "settings.local.json"),
|
|
586
|
+
"utf-8",
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
const skillrepoHooks = parsed.hooks.SessionStart.flatMap(
|
|
590
|
+
(g) => g.hooks,
|
|
591
|
+
).filter((h) => h.command.includes(_FINGERPRINT));
|
|
592
|
+
assert.equal(skillrepoHooks.length, 1, "exactly one SkillRepo hook");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("--json includes a sessionSync block with action + path", async () => {
|
|
596
|
+
// INTENT: automation scripts need to know whether session sync
|
|
597
|
+
// was installed, opted out, or failed. The JSON contract is the
|
|
598
|
+
// machine-readable channel for that.
|
|
599
|
+
await runInit(
|
|
600
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes", "--json"],
|
|
601
|
+
{ stdout, stderr },
|
|
602
|
+
);
|
|
603
|
+
const json = JSON.parse(stdout.text());
|
|
604
|
+
assert.ok(json.sessionSync, "sessionSync must be in --json output");
|
|
605
|
+
assert.equal(json.sessionSync.action, "installed");
|
|
606
|
+
assert.equal(json.sessionSync.path, ".claude/settings.local.json");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("--json with --no-session-sync reports action: 'opted-out'", async () => {
|
|
610
|
+
await runInit(
|
|
611
|
+
[
|
|
612
|
+
"--key",
|
|
613
|
+
VALID_KEY,
|
|
614
|
+
"--url",
|
|
615
|
+
serverUrl,
|
|
616
|
+
"--yes",
|
|
617
|
+
"--no-session-sync",
|
|
618
|
+
"--json",
|
|
619
|
+
],
|
|
620
|
+
{ stdout, stderr },
|
|
621
|
+
);
|
|
622
|
+
const json = JSON.parse(stdout.text());
|
|
623
|
+
assert.equal(json.sessionSync.action, "opted-out");
|
|
624
|
+
assert.equal(json.sessionSync.path, null);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("skips session sync entirely when only non-Claude-Code IDEs are targeted (cross-PR review fix)", async () => {
|
|
628
|
+
// Cross-PR review flagged: before this guard, a user running
|
|
629
|
+
// `skillrepo init --ide cursor` would get a Claude Code-specific
|
|
630
|
+
// SessionStart hook written to `.claude/settings.local.json`.
|
|
631
|
+
// Cursor never reads that file, so the hook was silent useless
|
|
632
|
+
// state that `skillrepo uninstall` later had to clean up.
|
|
633
|
+
//
|
|
634
|
+
// The guard in init.mjs step 6 now skips the install when
|
|
635
|
+
// `claudeCode` is not in the resolved vendors list AND
|
|
636
|
+
// `--global` is not passed. This test proves the skip fires.
|
|
637
|
+
//
|
|
638
|
+
// Use --ide cursor to force vendors = ["cursor"]. Bypass the
|
|
639
|
+
// .claude/ auto-detection by creating .cursor/ instead.
|
|
640
|
+
mkdirSync(join(process.cwd(), ".cursor"), { recursive: true });
|
|
641
|
+
rmSync(join(process.cwd(), ".claude"), { recursive: true, force: true });
|
|
642
|
+
|
|
643
|
+
await runInit(
|
|
644
|
+
[
|
|
645
|
+
"--key",
|
|
646
|
+
VALID_KEY,
|
|
647
|
+
"--url",
|
|
648
|
+
serverUrl,
|
|
649
|
+
"--yes",
|
|
650
|
+
"--ide",
|
|
651
|
+
"cursor",
|
|
652
|
+
"--json",
|
|
653
|
+
],
|
|
654
|
+
{ stdout, stderr },
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const json = JSON.parse(stdout.text());
|
|
658
|
+
assert.equal(
|
|
659
|
+
json.sessionSync.action,
|
|
660
|
+
"not-applicable",
|
|
661
|
+
"session sync must report 'not-applicable' for non-Claude-Code targets",
|
|
662
|
+
);
|
|
663
|
+
assert.equal(json.sessionSync.path, null);
|
|
664
|
+
// Critical: the settings.local.json file must NOT have been
|
|
665
|
+
// written. A Cursor user should never see this Claude-specific
|
|
666
|
+
// file materialize from `skillrepo init`.
|
|
667
|
+
assert.ok(
|
|
668
|
+
!existsSync(join(process.cwd(), ".claude", "settings.local.json")),
|
|
669
|
+
".claude/settings.local.json must NOT be written for Cursor-only init",
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("still installs session sync under --global even without claudeCode in vendors", async () => {
|
|
674
|
+
// INTENT: `--global` writes to `~/.claude/settings.local.json`,
|
|
675
|
+
// which IS Claude Code's user-wide settings path. A user who
|
|
676
|
+
// runs `skillrepo init --global` (even without `--ide claude`)
|
|
677
|
+
// is implicitly targeting Claude Code. The guard must allow
|
|
678
|
+
// this path so `--global` users still get auto-sync.
|
|
679
|
+
//
|
|
680
|
+
// Note: the setup() helper already creates `.claude/` in the
|
|
681
|
+
// project, which would normally push vendors to include
|
|
682
|
+
// claudeCode. Force vendors = ["cursor"] via --ide to exercise
|
|
683
|
+
// the "--global overrides vendors" branch.
|
|
684
|
+
await runInit(
|
|
685
|
+
[
|
|
686
|
+
"--key",
|
|
687
|
+
VALID_KEY,
|
|
688
|
+
"--url",
|
|
689
|
+
serverUrl,
|
|
690
|
+
"--yes",
|
|
691
|
+
"--global",
|
|
692
|
+
"--ide",
|
|
693
|
+
"cursor",
|
|
694
|
+
"--json",
|
|
695
|
+
],
|
|
696
|
+
{ stdout, stderr },
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const json = JSON.parse(stdout.text());
|
|
700
|
+
assert.equal(
|
|
701
|
+
json.sessionSync.action,
|
|
702
|
+
"installed",
|
|
703
|
+
"--global must install the hook even when vendors doesn't include claudeCode",
|
|
704
|
+
);
|
|
705
|
+
assert.equal(json.sessionSync.path, "~/.claude/settings.local.json");
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// ── v3.1.1 patch fixes: init UX bugs surfaced by real-world npx use ──
|
|
710
|
+
//
|
|
711
|
+
// Real-user `npx skillrepo@latest init` session surfaced four bugs in
|
|
712
|
+
// the v3.1.0 init output. The tests below lock each fix as a
|
|
713
|
+
// behavioral contract:
|
|
714
|
+
//
|
|
715
|
+
// 1. "Next steps" hardcoded `skillrepo` even under npx — would fail
|
|
716
|
+
// with "command not found" for users without a global install.
|
|
717
|
+
// 2. Session-sync hook installed under npx with a cache-temporary
|
|
718
|
+
// path that breaks on cache eviction. (Covered in
|
|
719
|
+
// session-hook.test.mjs via isNpxInvocation guard test.)
|
|
720
|
+
// 3. Step-7 zero-delta message conflated "empty library" with
|
|
721
|
+
// "nothing changed since last sync" — lied to users who had
|
|
722
|
+
// synced skills but no server-side changes since.
|
|
723
|
+
// 4. `--verbose` rejected as Unknown argument by resolveFlags.
|
|
724
|
+
// (Covered in cli-config.test.mjs.)
|
|
725
|
+
|
|
726
|
+
describe("runInit — v3.1.1 zero-delta message (bug 3)", () => {
|
|
727
|
+
beforeEach(setup);
|
|
728
|
+
afterEach(teardown);
|
|
729
|
+
|
|
730
|
+
it("reports 'Library is up to date' when a delta sync returns zero changes", async () => {
|
|
731
|
+
// Reproduces the real-user bug: a user with an existing
|
|
732
|
+
// .last-sync from a prior v3.0.0 session runs `skillrepo init`
|
|
733
|
+
// again. Init's step 7 sends `since=<prior syncedAt>`. Server
|
|
734
|
+
// returns empty skills[] because nothing changed server-side.
|
|
735
|
+
// Counters (added/updated/removed) all zero.
|
|
736
|
+
//
|
|
737
|
+
// Before the fix: init printed "No skills in library yet"
|
|
738
|
+
// regardless of whether the library was empty or fully synced.
|
|
739
|
+
// After: init distinguishes via the new `fullSync` field in
|
|
740
|
+
// SyncSummary and prints "Library is up to date (no changes
|
|
741
|
+
// since last sync)" for the delta case.
|
|
742
|
+
//
|
|
743
|
+
// Pre-seed a .last-sync file simulating the prior sync state.
|
|
744
|
+
mkdirSync(join(process.env.HOME, ".claude", "skillrepo"), { recursive: true });
|
|
745
|
+
writeFileSync(
|
|
746
|
+
join(process.env.HOME, ".claude", "skillrepo", ".last-sync"),
|
|
747
|
+
JSON.stringify({
|
|
748
|
+
schemaVersion: 1,
|
|
749
|
+
etag: '"old-v300-format-etag"',
|
|
750
|
+
syncedAt: "2026-04-15T12:00:00.000Z",
|
|
751
|
+
}),
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
// Server returns empty skills[] (no changes since last sync).
|
|
755
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "2026-04-16T12:00:00.000Z" });
|
|
756
|
+
|
|
757
|
+
await runInit(
|
|
758
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
759
|
+
{ stdout, stderr },
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
const out = stdout.text();
|
|
763
|
+
assert.match(
|
|
764
|
+
out,
|
|
765
|
+
/Library is up to date/,
|
|
766
|
+
"zero-delta on delta sync must report 'up to date', not 'no skills in library yet'",
|
|
767
|
+
);
|
|
768
|
+
assert.doesNotMatch(
|
|
769
|
+
out,
|
|
770
|
+
/No skills in library yet/,
|
|
771
|
+
"the misleading 'no skills' message must NOT fire when .last-sync existed",
|
|
772
|
+
);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("still reports 'No skills in library yet' on a true first-run full sync with zero skills", async () => {
|
|
776
|
+
// The other side of bug 3: when there's NO prior .last-sync and
|
|
777
|
+
// the server returns zero skills, the library IS genuinely
|
|
778
|
+
// empty. The message should still say so.
|
|
779
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "2026-04-16T12:00:00.000Z" });
|
|
780
|
+
|
|
781
|
+
await runInit(
|
|
782
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
783
|
+
{ stdout, stderr },
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const out = stdout.text();
|
|
787
|
+
assert.match(out, /No skills in library yet/);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("304 Not Modified reports 'Library is up to date' (neutral phrasing)", async () => {
|
|
791
|
+
// 304 path: server returned "nothing changed" at the HTTP
|
|
792
|
+
// level. Definitively up-to-date regardless of whether library
|
|
793
|
+
// is populated or empty.
|
|
794
|
+
mkdirSync(join(process.env.HOME, ".claude", "skillrepo"), { recursive: true });
|
|
795
|
+
writeFileSync(
|
|
796
|
+
join(process.env.HOME, ".claude", "skillrepo", ".last-sync"),
|
|
797
|
+
JSON.stringify({
|
|
798
|
+
schemaVersion: 1,
|
|
799
|
+
etag: '"match-me"',
|
|
800
|
+
syncedAt: "2026-04-15T12:00:00.000Z",
|
|
801
|
+
}),
|
|
802
|
+
);
|
|
803
|
+
server.setEtag('"match-me"');
|
|
804
|
+
|
|
805
|
+
await runInit(
|
|
806
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
807
|
+
{ stdout, stderr },
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
assert.match(stdout.text(), /Library is up to date/);
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
describe("runInit — v3.1.1 Next steps prefix (bug 1)", () => {
|
|
815
|
+
// Process-state isolation: the tests below depend on controlling
|
|
816
|
+
// isNpxInvocation()'s outputs. The test host's shell environment
|
|
817
|
+
// may have `_` set to some launching command (the enclosing `npm
|
|
818
|
+
// run check`, `node --test`, etc.). beforeEach force-clears the
|
|
819
|
+
// two isNpxInvocation signals (argv[1] and `_`) plus the
|
|
820
|
+
// now-unused `npm_command` — the latter is cleared defensively
|
|
821
|
+
// because older versions of this codebase treated it as an npx
|
|
822
|
+
// signal, and belt-and-suspenders cleanup costs nothing.
|
|
823
|
+
let originalArgv;
|
|
824
|
+
let originalNpmCommand;
|
|
825
|
+
let originalUnderscore;
|
|
826
|
+
|
|
827
|
+
beforeEach(async () => {
|
|
828
|
+
await setup();
|
|
829
|
+
originalArgv = process.argv;
|
|
830
|
+
originalNpmCommand = process.env.npm_command;
|
|
831
|
+
originalUnderscore = process.env._;
|
|
832
|
+
// Non-npx state:
|
|
833
|
+
process.argv = ["/usr/local/bin/node", "/usr/local/bin/skillrepo"];
|
|
834
|
+
delete process.env.npm_command;
|
|
835
|
+
delete process.env._;
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
afterEach(async () => {
|
|
839
|
+
process.argv = originalArgv;
|
|
840
|
+
if (originalNpmCommand === undefined) delete process.env.npm_command;
|
|
841
|
+
else process.env.npm_command = originalNpmCommand;
|
|
842
|
+
if (originalUnderscore === undefined) delete process.env._;
|
|
843
|
+
else process.env._ = originalUnderscore;
|
|
844
|
+
await teardown();
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("shows bare `skillrepo` prefix when running from a stable install", async () => {
|
|
848
|
+
// With all npx signals cleared (beforeEach above), isNpxInvocation
|
|
849
|
+
// returns false and the bare prefix is used in the Next Steps
|
|
850
|
+
// block.
|
|
851
|
+
//
|
|
852
|
+
// NOTE ON REGEX: "npm install -g skillrepo" appears in TWO places
|
|
853
|
+
// that look alike but come from different code paths:
|
|
854
|
+
//
|
|
855
|
+
// 1. Step 6/7 session-sync skipped message (when the binary
|
|
856
|
+
// can't be resolved — in the test environment, `which
|
|
857
|
+
// skillrepo` doesn't find anything real).
|
|
858
|
+
// 2. Next Steps tip (only shown under npx).
|
|
859
|
+
//
|
|
860
|
+
// We want to assert only #2 is absent. Match the EXACT "Tip:"
|
|
861
|
+
// prefix to scope the assertion to Next Steps.
|
|
862
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
863
|
+
await runInit(
|
|
864
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
865
|
+
{ stdout, stderr },
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const out = stdout.text();
|
|
869
|
+
// The commands in Next Steps are bare, not prefixed.
|
|
870
|
+
assert.match(out, /^ +• skillrepo list/m);
|
|
871
|
+
assert.doesNotMatch(
|
|
872
|
+
out,
|
|
873
|
+
/npx skillrepo list/,
|
|
874
|
+
"bare install must NOT show the npx-prefixed hints",
|
|
875
|
+
);
|
|
876
|
+
// The "Tip: …" line appears only under npx.
|
|
877
|
+
assert.doesNotMatch(
|
|
878
|
+
out,
|
|
879
|
+
/Tip: `npm install -g skillrepo`/,
|
|
880
|
+
"bare install must NOT show the npx-mode global-install tip",
|
|
881
|
+
);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("shows `npx skillrepo` prefix and global-install tip under npx invocation", async () => {
|
|
885
|
+
// Simulate npx by stuffing argv[1] with an _npx cache path.
|
|
886
|
+
// isNpxInvocation() returns true and the output changes shape.
|
|
887
|
+
process.argv = [
|
|
888
|
+
"/usr/local/bin/node",
|
|
889
|
+
"/Users/alice/.npm/_npx/dc129a78aca3fc9c/node_modules/.bin/skillrepo",
|
|
890
|
+
];
|
|
891
|
+
|
|
892
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
893
|
+
await runInit(
|
|
894
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes"],
|
|
895
|
+
{ stdout, stderr },
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const out = stdout.text();
|
|
899
|
+
assert.match(
|
|
900
|
+
out,
|
|
901
|
+
/npx skillrepo list/,
|
|
902
|
+
"npx invocation must prefix commands with `npx`",
|
|
903
|
+
);
|
|
904
|
+
assert.match(
|
|
905
|
+
out,
|
|
906
|
+
/Tip: `npm install -g skillrepo`/,
|
|
907
|
+
"npx invocation must show the Next-Steps global-install tip",
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
@@ -12,12 +12,18 @@ import { runList } from "../../commands/list.mjs";
|
|
|
12
12
|
import { CliError, EXIT_AUTH } from "../../lib/errors.mjs";
|
|
13
13
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
14
14
|
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
15
|
+
import {
|
|
16
|
+
captureHome,
|
|
17
|
+
setSandboxHome,
|
|
18
|
+
restoreHome,
|
|
19
|
+
} from "../helpers/sandbox-home.mjs";
|
|
15
20
|
|
|
16
21
|
let sandbox;
|
|
17
22
|
let server;
|
|
18
23
|
let serverUrl;
|
|
19
24
|
let originalCwd;
|
|
20
|
-
|
|
25
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
26
|
+
let originalHomeEnv;
|
|
21
27
|
let stdout;
|
|
22
28
|
const VALID_KEY = "sk_live_test";
|
|
23
29
|
|
|
@@ -37,9 +43,9 @@ async function setup() {
|
|
|
37
43
|
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
38
44
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
39
45
|
originalCwd = process.cwd();
|
|
40
|
-
|
|
46
|
+
originalHomeEnv = captureHome();
|
|
41
47
|
process.chdir(join(sandbox, "project"));
|
|
42
|
-
|
|
48
|
+
setSandboxHome(join(sandbox, "home"));
|
|
43
49
|
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
44
50
|
|
|
45
51
|
server = createMockServer({});
|
|
@@ -52,7 +58,7 @@ async function setup() {
|
|
|
52
58
|
async function teardown() {
|
|
53
59
|
if (server) await server.stop();
|
|
54
60
|
process.chdir(originalCwd);
|
|
55
|
-
|
|
61
|
+
restoreHome(originalHomeEnv);
|
|
56
62
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
57
63
|
server = null;
|
|
58
64
|
}
|
|
@@ -13,12 +13,18 @@ import { writeSkillDir, resolvePlacementDir } from "../../lib/file-write.mjs";
|
|
|
13
13
|
import { CliError, EXIT_VALIDATION, EXIT_AUTH, EXIT_SCOPE } from "../../lib/errors.mjs";
|
|
14
14
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
15
15
|
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
16
|
+
import {
|
|
17
|
+
captureHome,
|
|
18
|
+
setSandboxHome,
|
|
19
|
+
restoreHome,
|
|
20
|
+
} from "../helpers/sandbox-home.mjs";
|
|
16
21
|
|
|
17
22
|
let sandbox;
|
|
18
23
|
let server;
|
|
19
24
|
let serverUrl;
|
|
20
25
|
let originalCwd;
|
|
21
|
-
|
|
26
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
27
|
+
let originalHomeEnv;
|
|
22
28
|
let stdout;
|
|
23
29
|
const VALID_KEY = "sk_live_test";
|
|
24
30
|
|
|
@@ -46,9 +52,9 @@ async function setup() {
|
|
|
46
52
|
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
47
53
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
48
54
|
originalCwd = process.cwd();
|
|
49
|
-
|
|
55
|
+
originalHomeEnv = captureHome();
|
|
50
56
|
process.chdir(join(sandbox, "project"));
|
|
51
|
-
|
|
57
|
+
setSandboxHome(join(sandbox, "home"));
|
|
52
58
|
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
53
59
|
|
|
54
60
|
server = createMockServer({});
|
|
@@ -61,7 +67,7 @@ async function setup() {
|
|
|
61
67
|
async function teardown() {
|
|
62
68
|
if (server) await server.stop();
|
|
63
69
|
process.chdir(originalCwd);
|
|
64
|
-
|
|
70
|
+
restoreHome(originalHomeEnv);
|
|
65
71
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
66
72
|
server = null;
|
|
67
73
|
}
|
|
@@ -12,12 +12,18 @@ import { runSearch } from "../../commands/search.mjs";
|
|
|
12
12
|
import { CliError, EXIT_VALIDATION, EXIT_AUTH } from "../../lib/errors.mjs";
|
|
13
13
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
14
14
|
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
15
|
+
import {
|
|
16
|
+
captureHome,
|
|
17
|
+
setSandboxHome,
|
|
18
|
+
restoreHome,
|
|
19
|
+
} from "../helpers/sandbox-home.mjs";
|
|
15
20
|
|
|
16
21
|
let sandbox;
|
|
17
22
|
let server;
|
|
18
23
|
let serverUrl;
|
|
19
24
|
let originalCwd;
|
|
20
|
-
|
|
25
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
26
|
+
let originalHomeEnv;
|
|
21
27
|
let stdout;
|
|
22
28
|
let stderr;
|
|
23
29
|
const VALID_KEY = "sk_live_test";
|
|
@@ -42,9 +48,9 @@ async function setup() {
|
|
|
42
48
|
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
43
49
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
44
50
|
originalCwd = process.cwd();
|
|
45
|
-
|
|
51
|
+
originalHomeEnv = captureHome();
|
|
46
52
|
process.chdir(join(sandbox, "project"));
|
|
47
|
-
|
|
53
|
+
setSandboxHome(join(sandbox, "home"));
|
|
48
54
|
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
49
55
|
|
|
50
56
|
server = createMockServer({});
|
|
@@ -58,7 +64,7 @@ async function setup() {
|
|
|
58
64
|
async function teardown() {
|
|
59
65
|
if (server) await server.stop();
|
|
60
66
|
process.chdir(originalCwd);
|
|
61
|
-
|
|
67
|
+
restoreHome(originalHomeEnv);
|
|
62
68
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
63
69
|
server = null;
|
|
64
70
|
}
|