akm-cli 0.9.0-beta.5 → 0.9.0-beta.51
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 +711 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +38 -10
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +27 -11
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +33 -26
- package/dist/commands/env/secret-cli.js +36 -22
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +49 -1
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +138 -7
- package/dist/commands/improve/improve-cli.js +36 -61
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +14 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +850 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +6 -3
- package/dist/commands/read/search-cli.js +9 -4
- package/dist/commands/read/search.js +10 -6
- package/dist/commands/read/show.js +86 -7
- package/dist/commands/sources/init.js +49 -17
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +43 -45
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +18 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-migration.js +12 -11
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +19 -6
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +17 -18
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +74 -2
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2617 -1961
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
- package/dist/setup/setup.js +29 -8
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +3 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +13 -11
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
package/dist/output/renderers.js
CHANGED
|
@@ -377,7 +377,7 @@ const envFileRenderer = {
|
|
|
377
377
|
type: "env",
|
|
378
378
|
name,
|
|
379
379
|
path: ctx.absPath,
|
|
380
|
-
action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected
|
|
380
|
+
action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected; prefer `--clean` to minimize inherited parent env. AKM itself does not print values, but child stdout/stderr is not redacted. `akm env export <ref> --out <file>` writes a sourceable script to a file. Never `source` the raw file. Values stay on disk and are never written to akm's stdout.",
|
|
381
381
|
description: comments.length > 0 ? comments.join("\n") : undefined,
|
|
382
382
|
keys,
|
|
383
383
|
comments,
|
|
@@ -468,6 +468,44 @@ const sessionMdRenderer = {
|
|
|
468
468
|
};
|
|
469
469
|
},
|
|
470
470
|
};
|
|
471
|
+
// ── 9. fact-md ───────────────────────────────────────────────────────────────
|
|
472
|
+
/**
|
|
473
|
+
* Renderer for the `fact` asset type. A fact is durable stash-level semantic
|
|
474
|
+
* knowledge (personal/team/project details, coding conventions, stash-meta).
|
|
475
|
+
* It carries `category` (personal|team|project|convention|meta) and an
|
|
476
|
+
* optional `pinned` flag marking it as part of the always-injected core. The
|
|
477
|
+
* renderer surfaces a one-liner (category + pinned marker) so an agent can tell
|
|
478
|
+
* at a glance what kind of fact it is and whether it is core context.
|
|
479
|
+
*/
|
|
480
|
+
const factMdRenderer = {
|
|
481
|
+
name: "fact-md",
|
|
482
|
+
buildShowResponse(ctx) {
|
|
483
|
+
const name = deriveName(ctx);
|
|
484
|
+
const parsed = parseFrontmatter(ctx.content());
|
|
485
|
+
const fm = parsed.data;
|
|
486
|
+
const category = asNonEmptyString(fm.category);
|
|
487
|
+
const description = asNonEmptyString(fm.description);
|
|
488
|
+
const pinned = fm.pinned === true;
|
|
489
|
+
const headerParts = [
|
|
490
|
+
category ? `category: ${category}` : undefined,
|
|
491
|
+
pinned ? "pinned (core context)" : undefined,
|
|
492
|
+
].filter((p) => !!p);
|
|
493
|
+
const action = [
|
|
494
|
+
"Durable stash fact — apply it as background context.",
|
|
495
|
+
headerParts.length > 0 ? headerParts.join(" ") : undefined,
|
|
496
|
+
]
|
|
497
|
+
.filter((p) => !!p)
|
|
498
|
+
.join("\n");
|
|
499
|
+
return {
|
|
500
|
+
type: "fact",
|
|
501
|
+
name,
|
|
502
|
+
path: ctx.absPath,
|
|
503
|
+
action,
|
|
504
|
+
description,
|
|
505
|
+
content: parsed.content,
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
};
|
|
471
509
|
function applySessionMetadata(entry, ctx) {
|
|
472
510
|
try {
|
|
473
511
|
const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
|
|
@@ -501,6 +539,34 @@ function applyTocMetadata(entry, ctx) {
|
|
|
501
539
|
// Non-fatal: skip TOC if file can't be read
|
|
502
540
|
}
|
|
503
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Fact metadata: surface `category` and the `pinned` core marker as tags +
|
|
544
|
+
* search hints (no dedicated DB columns — same encoding pattern as session /
|
|
545
|
+
* task). `pinned` is mirrored to both a `pinned` tag and a `pinned` search
|
|
546
|
+
* hint so the ranking contributor can detect it and queries can target it.
|
|
547
|
+
*/
|
|
548
|
+
function applyFactMetadata(entry, ctx) {
|
|
549
|
+
try {
|
|
550
|
+
const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
|
|
551
|
+
const tags = new Set([...(entry.tags ?? []), "fact"]);
|
|
552
|
+
const hints = new Set(entry.searchHints ?? []);
|
|
553
|
+
const category = asNonEmptyString(fm.category);
|
|
554
|
+
if (category) {
|
|
555
|
+
tags.add(category);
|
|
556
|
+
hints.add(`category:${category}`);
|
|
557
|
+
}
|
|
558
|
+
if (fm.pinned === true) {
|
|
559
|
+
tags.add("pinned");
|
|
560
|
+
hints.add("pinned");
|
|
561
|
+
}
|
|
562
|
+
entry.tags = Array.from(tags).filter(Boolean);
|
|
563
|
+
if (hints.size > 0)
|
|
564
|
+
entry.searchHints = Array.from(hints).filter(Boolean);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
// Non-fatal: skip metadata extraction on parse error
|
|
568
|
+
}
|
|
569
|
+
}
|
|
504
570
|
/**
|
|
505
571
|
* Parse frontmatter, apply description (if not already set) and merge tags
|
|
506
572
|
* into `entry`. Returns the raw frontmatter data object so callers can access
|
|
@@ -660,6 +726,11 @@ registerMetadataContributor({
|
|
|
660
726
|
appliesTo: ({ rendererName }) => rendererName === "session-md",
|
|
661
727
|
contribute: (entry, ctx) => applySessionMetadata(entry, ctx.renderContext),
|
|
662
728
|
});
|
|
729
|
+
registerMetadataContributor({
|
|
730
|
+
name: "fact-md-metadata",
|
|
731
|
+
appliesTo: ({ rendererName }) => rendererName === "fact-md",
|
|
732
|
+
contribute: (entry, ctx) => applyFactMetadata(entry, ctx.renderContext),
|
|
733
|
+
});
|
|
663
734
|
// ── Registration ─────────────────────────────────────────────────────────────
|
|
664
735
|
/** All built-in renderers. */
|
|
665
736
|
const builtinRenderers = [
|
|
@@ -676,6 +747,7 @@ const builtinRenderers = [
|
|
|
676
747
|
secretFileRenderer,
|
|
677
748
|
taskMdRenderer,
|
|
678
749
|
sessionMdRenderer,
|
|
750
|
+
factMdRenderer,
|
|
679
751
|
];
|
|
680
752
|
/**
|
|
681
753
|
* Register all built-in renderers with the file-context registry.
|
|
@@ -687,4 +759,4 @@ export function registerBuiltinRenderers() {
|
|
|
687
759
|
}
|
|
688
760
|
}
|
|
689
761
|
// ── Named exports for testing ────────────────────────────────────────────────
|
|
690
|
-
export { agentMdRenderer, commandMdRenderer, envFileRenderer, INTERPRETER_MAP, knowledgeMdRenderer, lessonMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, secretFileRenderer, skillMdRenderer, wikiMdRenderer, workflowMdRenderer, };
|
|
762
|
+
export { agentMdRenderer, commandMdRenderer, envFileRenderer, factMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, lessonMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, secretFileRenderer, skillMdRenderer, wikiMdRenderer, workflowMdRenderer, };
|
|
@@ -5,7 +5,7 @@ import { capDescription, NORMAL_DESCRIPTION_LIMIT, pickFields } from "./helpers.
|
|
|
5
5
|
// Curation is a small, high-signal top-N. Even at `brief` we keep `followUp`
|
|
6
6
|
// (the actionable `akm show <ref>` command) and `reason` (why this asset was
|
|
7
7
|
// selected) — these are the point of curate, unlike a bulk search listing.
|
|
8
|
-
const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "followUp", "reason"];
|
|
8
|
+
const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "supportRefs", "followUp", "reason"];
|
|
9
9
|
const NORMAL_FIELDS = [
|
|
10
10
|
"source",
|
|
11
11
|
"type",
|
|
@@ -17,12 +17,24 @@ const NORMAL_FIELDS = [
|
|
|
17
17
|
"keys",
|
|
18
18
|
"parameters",
|
|
19
19
|
"run",
|
|
20
|
+
"supportRefs",
|
|
20
21
|
"followUp",
|
|
21
22
|
"reason",
|
|
22
23
|
"score",
|
|
23
24
|
];
|
|
24
25
|
// Agent shape: the minimal field set an LLM needs to decide and act.
|
|
25
|
-
const AGENT_FIELDS = [
|
|
26
|
+
const AGENT_FIELDS = [
|
|
27
|
+
"source",
|
|
28
|
+
"type",
|
|
29
|
+
"name",
|
|
30
|
+
"ref",
|
|
31
|
+
"id",
|
|
32
|
+
"description",
|
|
33
|
+
"supportRefs",
|
|
34
|
+
"followUp",
|
|
35
|
+
"reason",
|
|
36
|
+
"score",
|
|
37
|
+
];
|
|
26
38
|
function shapeCurateItem(item, detail, shape) {
|
|
27
39
|
if (shape === "agent") {
|
|
28
40
|
return capDescription(pickFields(item, AGENT_FIELDS), NORMAL_DESCRIPTION_LIMIT);
|
|
@@ -1005,6 +1005,15 @@ export function formatCuratePlain(r, detail) {
|
|
|
1005
1005
|
lines.push(` run: ${String(item.run)}`);
|
|
1006
1006
|
if (item.followUp)
|
|
1007
1007
|
lines.push(` show: ${String(item.followUp)}`);
|
|
1008
|
+
if (Array.isArray(item.supportRefs) && item.supportRefs.length > 0) {
|
|
1009
|
+
for (const support of item.supportRefs) {
|
|
1010
|
+
if (!support.ref)
|
|
1011
|
+
continue;
|
|
1012
|
+
const label = typeof support.type === "string" ? `[${support.type}] ` : "";
|
|
1013
|
+
const why = typeof support.reason === "string" ? ` — ${support.reason}` : "";
|
|
1014
|
+
lines.push(` support: ${label}${String(support.ref)}${why}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1008
1017
|
if (detail !== "brief" && item.reason)
|
|
1009
1018
|
lines.push(` why: ${String(item.reason)}`);
|
|
1010
1019
|
}
|
|
@@ -1026,8 +1035,14 @@ export function formatCuratePlain(r, detail) {
|
|
|
1026
1035
|
}
|
|
1027
1036
|
export function formatInitPlain(r) {
|
|
1028
1037
|
let out = `Stash initialized at ${r.stashDir ?? "unknown"}`;
|
|
1029
|
-
|
|
1038
|
+
// When --dir scaffolded a secondary stash but the default was deliberately
|
|
1039
|
+
// left untouched, tell the user instead of silently repointing their default.
|
|
1040
|
+
if (r.defaultStashUpdated === false && typeof r.previousStashDir === "string" && r.previousStashDir) {
|
|
1041
|
+
out += `\nYour default stash is unchanged (${r.previousStashDir}). Re-run with --set-default to make ${r.stashDir} the default.`;
|
|
1042
|
+
}
|
|
1043
|
+
else if (r.configPath) {
|
|
1030
1044
|
out += `\nConfig saved to ${r.configPath}`;
|
|
1045
|
+
}
|
|
1031
1046
|
return out;
|
|
1032
1047
|
}
|
|
1033
1048
|
export function formatIndexPlain(r) {
|
|
@@ -2,46 +2,12 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { fetchWithRetry } from "../../core/common.js";
|
|
5
|
-
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
6
|
-
import { closeDatabase, getRegistryIndexCache, openDatabase, upsertRegistryIndexCache } from "../../indexer/db/db.js";
|
|
7
5
|
import { md5Hex } from "../../runtime.js";
|
|
6
|
+
import { fetchCachedJson } from "../../storage/repositories/registry-cache.js";
|
|
8
7
|
import { registerProvider } from "../factory.js";
|
|
9
8
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
10
9
|
/** Per-query cache TTL in milliseconds (15 minutes). */
|
|
11
10
|
const QUERY_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
12
|
-
// ── Cache DB lifecycle ────────────────────────────────────────────────────────
|
|
13
|
-
/**
|
|
14
|
-
* RAII-style lifecycle helper for the registry cache DB. Opens the DB (treating
|
|
15
|
-
* a failed open exactly like the legacy fall-through: the bun-test isolation
|
|
16
|
-
* guard is re-thrown, any other failure yields `db = undefined`), runs `fn`,
|
|
17
|
-
* and guarantees the DB is closed in a `finally` after `fn` has fully settled
|
|
18
|
-
* (the await is required: the callbacks are async, and closing before they
|
|
19
|
-
* settle would tear the DB down mid-write).
|
|
20
|
-
*/
|
|
21
|
-
async function withRegistryCacheDb(fn) {
|
|
22
|
-
let db;
|
|
23
|
-
try {
|
|
24
|
-
db = openDatabase();
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
// Never mask the bun-test isolation guard as "DB unavailable".
|
|
28
|
-
rethrowIfTestIsolationError(err);
|
|
29
|
-
db = undefined;
|
|
30
|
-
}
|
|
31
|
-
try {
|
|
32
|
-
return await fn(db);
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
if (db) {
|
|
36
|
-
try {
|
|
37
|
-
closeDatabase(db);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
/* ignore */
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
11
|
// ── Provider class ──────────────────────────────────────────────────────────
|
|
46
12
|
class SkillsShProvider {
|
|
47
13
|
type = "skills-sh";
|
|
@@ -66,131 +32,39 @@ class SkillsShProvider {
|
|
|
66
32
|
return { hits: [], warnings: [`Registry ${label}: ${message}`] };
|
|
67
33
|
}
|
|
68
34
|
}
|
|
69
|
-
// ── v1-spec §3.1 surface ────────────────────────────────────────────────
|
|
70
|
-
async searchKits(q) {
|
|
71
|
-
const result = await this.search({
|
|
72
|
-
query: q.text,
|
|
73
|
-
limit: q.limit ?? 20,
|
|
74
|
-
includeAssets: false,
|
|
75
|
-
});
|
|
76
|
-
return result.hits.map((hit) => ({
|
|
77
|
-
id: hit.id,
|
|
78
|
-
title: hit.title,
|
|
79
|
-
summary: hit.description,
|
|
80
|
-
installRef: hit.installRef,
|
|
81
|
-
score: hit.score,
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
async searchAssets(q) {
|
|
85
|
-
const result = await this.search({
|
|
86
|
-
query: q.text,
|
|
87
|
-
limit: q.limit ?? 20,
|
|
88
|
-
includeAssets: true,
|
|
89
|
-
});
|
|
90
|
-
return (result.assetHits ?? []).map((hit) => ({
|
|
91
|
-
kitId: hit.stash.id,
|
|
92
|
-
type: hit.assetType,
|
|
93
|
-
name: hit.assetName,
|
|
94
|
-
summary: hit.description,
|
|
95
|
-
cloneRef: hit.action.replace(/^akm add\s+/, ""),
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* skills.sh has no `getKit` API — every entry corresponds to a GitHub
|
|
100
|
-
* repository whose metadata we already include in the search result. We
|
|
101
|
-
* synthesize a manifest from the search hit when the caller knows the stash
|
|
102
|
-
* id; if not present in the most recent results, return null.
|
|
103
|
-
*/
|
|
104
|
-
async getKit(id) {
|
|
105
|
-
if (!id.startsWith("skills-sh:"))
|
|
106
|
-
return null;
|
|
107
|
-
const slug = id.slice("skills-sh:".length);
|
|
108
|
-
// Best-effort: the API gives us search-by-name; extract the leaf segment.
|
|
109
|
-
const segments = slug.split("/").filter(Boolean);
|
|
110
|
-
const leaf = segments[segments.length - 1] ?? slug;
|
|
111
|
-
const result = await this.search({ query: leaf, limit: 50, includeAssets: false });
|
|
112
|
-
const match = result.hits.find((hit) => hit.id === id);
|
|
113
|
-
if (!match)
|
|
114
|
-
return null;
|
|
115
|
-
return { id: match.id, installRef: match.installRef };
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* skills.sh entries are always GitHub repositories. Claim only refs whose
|
|
119
|
-
* parsed source is `github`; defer everything else (npm tarballs, local
|
|
120
|
-
* paths, raw git URLs) to other registries.
|
|
121
|
-
*/
|
|
122
|
-
canHandle(ref) {
|
|
123
|
-
return ref.source === "github";
|
|
124
|
-
}
|
|
125
35
|
async fetchSkills(query, limit) {
|
|
126
36
|
// Build a stable DB cache key for this query
|
|
127
37
|
const dbCacheKey = this.queryDbCacheKey(query, limit);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
// Never mask the bun-test isolation guard as "DB unavailable" — see
|
|
138
|
-
// rethrowIfTestIsolationError in src/core/errors.ts. Without this,
|
|
139
|
-
// a leaky test silently gets a cold cache + fresh fetch instead of
|
|
140
|
-
// the loud TEST_ISOLATION_MISSING failure the guard intends.
|
|
141
|
-
rethrowIfTestIsolationError(err);
|
|
142
|
-
// index.db not available yet (pre-migration install or test env) — fall through
|
|
143
|
-
}
|
|
144
|
-
if (dbCacheResult) {
|
|
38
|
+
const baseUrl = this.config.url.replace(/\/+$/, "");
|
|
39
|
+
const url = `${baseUrl}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
|
|
40
|
+
return fetchCachedJson({
|
|
41
|
+
cacheKey: dbCacheKey,
|
|
42
|
+
ttlMs: QUERY_CACHE_TTL_MS,
|
|
43
|
+
// A fresh hit returns even an empty array; a stale fallback only when
|
|
44
|
+
// non-empty. Corrupt cache JSON is swallowed and treated as a miss.
|
|
45
|
+
parseCache: (json, { stale }) => {
|
|
145
46
|
try {
|
|
146
|
-
const parsed = JSON.parse(
|
|
147
|
-
if (Array.isArray(parsed))
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
47
|
+
const parsed = JSON.parse(json);
|
|
48
|
+
if (!Array.isArray(parsed))
|
|
49
|
+
return undefined;
|
|
50
|
+
const entries = parsed.filter(isValidSkillsEntry);
|
|
51
|
+
if (stale && entries.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
return entries;
|
|
151
54
|
}
|
|
152
55
|
catch {
|
|
153
|
-
|
|
56
|
+
return undefined;
|
|
154
57
|
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const baseUrl = this.config.url.replace(/\/+$/, "");
|
|
158
|
-
const url = `${baseUrl}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
|
|
159
|
-
try {
|
|
58
|
+
},
|
|
59
|
+
fetchFresh: async () => {
|
|
160
60
|
const response = await fetchWithRetry(url, undefined, { timeout: 10_000, retries: 1 });
|
|
161
61
|
if (!response.ok) {
|
|
162
62
|
throw new Error(`HTTP ${response.status}`);
|
|
163
63
|
}
|
|
164
64
|
const data = (await response.json());
|
|
165
65
|
const entries = parseSkillsResponse(data);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
upsertRegistryIndexCache(db, dbCacheKey, JSON.stringify(entries));
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
/* best-effort */
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return entries;
|
|
176
|
-
}
|
|
177
|
-
catch (err) {
|
|
178
|
-
// Fetch failed — use stale DB cache if available
|
|
179
|
-
if (dbCacheResult) {
|
|
180
|
-
try {
|
|
181
|
-
const parsed = JSON.parse(dbCacheResult.indexJson);
|
|
182
|
-
if (Array.isArray(parsed)) {
|
|
183
|
-
const entries = parsed.filter(isValidSkillsEntry);
|
|
184
|
-
if (entries.length > 0)
|
|
185
|
-
return entries;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
/* ignore */
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
throw err;
|
|
193
|
-
}
|
|
66
|
+
return { value: entries, cacheJson: JSON.stringify(entries) };
|
|
67
|
+
},
|
|
194
68
|
});
|
|
195
69
|
}
|
|
196
70
|
mapToHits(entries) {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { fetchWithRetry, jsonWithByteCap, toErrorMessage } from "../../core/common.js";
|
|
5
|
-
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
6
|
-
import { closeDatabase, getRegistryIndexCache, openDatabase, upsertRegistryIndexCache } from "../../indexer/db/db.js";
|
|
7
5
|
import { asString } from "../../integrations/github.js";
|
|
6
|
+
import { fetchCachedJson } from "../../storage/repositories/registry-cache.js";
|
|
8
7
|
import { registerProvider } from "../factory.js";
|
|
8
|
+
import { buildInstallRef } from "../resolve.js";
|
|
9
9
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
10
10
|
/** Cache TTL in milliseconds (1 hour). */
|
|
11
11
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
@@ -30,51 +30,6 @@ class StaticIndexProvider {
|
|
|
30
30
|
}
|
|
31
31
|
return { hits, assetHits, warnings: warnings.length > 0 ? warnings : undefined };
|
|
32
32
|
}
|
|
33
|
-
// ── v1-spec §3.1 surface ────────────────────────────────────────────────
|
|
34
|
-
async searchKits(q) {
|
|
35
|
-
const result = await this.search({
|
|
36
|
-
query: q.text,
|
|
37
|
-
limit: q.limit ?? 20,
|
|
38
|
-
includeAssets: false,
|
|
39
|
-
});
|
|
40
|
-
return result.hits.map(hitToKitResult);
|
|
41
|
-
}
|
|
42
|
-
async searchAssets(q) {
|
|
43
|
-
const result = await this.search({
|
|
44
|
-
query: q.text,
|
|
45
|
-
limit: q.limit ?? 20,
|
|
46
|
-
includeAssets: true,
|
|
47
|
-
});
|
|
48
|
-
return (result.assetHits ?? []).map(assetHitToPreview);
|
|
49
|
-
}
|
|
50
|
-
async getKit(id) {
|
|
51
|
-
const allKits = await this.loadAllKits([]);
|
|
52
|
-
const found = allKits.find(({ stash }) => stash.id === id);
|
|
53
|
-
if (!found)
|
|
54
|
-
return null;
|
|
55
|
-
const installRef = buildInstallRef(found.stash.source, found.stash.ref);
|
|
56
|
-
return {
|
|
57
|
-
id: found.stash.id,
|
|
58
|
-
installRef,
|
|
59
|
-
assets: found.stash.assets?.map((asset) => ({
|
|
60
|
-
kitId: found.stash.id,
|
|
61
|
-
type: asset.type,
|
|
62
|
-
name: asset.name,
|
|
63
|
-
summary: asset.description,
|
|
64
|
-
cloneRef: installRef,
|
|
65
|
-
})),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Static-index doesn't own a URL prefix — any `ParsedRegistryRef` could
|
|
70
|
-
* theoretically be backed by an entry in some static-index registry. We
|
|
71
|
-
* therefore claim every ref. The orchestrator picks the first matching
|
|
72
|
-
* provider, and `static-index` is registered first by `index.ts`, so this
|
|
73
|
-
* is effectively the default catch-all.
|
|
74
|
-
*/
|
|
75
|
-
canHandle(_ref) {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
33
|
// ── Internals ───────────────────────────────────────────────────────────
|
|
79
34
|
async loadAllKits(warnings) {
|
|
80
35
|
const allKits = [];
|
|
@@ -94,84 +49,17 @@ class StaticIndexProvider {
|
|
|
94
49
|
return allKits;
|
|
95
50
|
}
|
|
96
51
|
}
|
|
97
|
-
function hitToKitResult(hit) {
|
|
98
|
-
return {
|
|
99
|
-
id: hit.id,
|
|
100
|
-
title: hit.title,
|
|
101
|
-
summary: hit.description,
|
|
102
|
-
installRef: hit.installRef,
|
|
103
|
-
score: hit.score,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function assetHitToPreview(hit) {
|
|
107
|
-
return {
|
|
108
|
-
kitId: hit.stash.id,
|
|
109
|
-
type: hit.assetType,
|
|
110
|
-
name: hit.assetName,
|
|
111
|
-
summary: hit.description,
|
|
112
|
-
cloneRef: hit.action.replace(/^akm add\s+/, ""),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
52
|
// ── Self-register ───────────────────────────────────────────────────────────
|
|
116
53
|
registerProvider("static-index", (config) => new StaticIndexProvider(config));
|
|
117
54
|
// ── Index loading with cache ────────────────────────────────────────────────
|
|
118
|
-
/**
|
|
119
|
-
* RAII-style lifecycle helper for the registry cache DB. Opens the DB (treating
|
|
120
|
-
* a failed open exactly like the legacy fall-through: the bun-test isolation
|
|
121
|
-
* guard is re-thrown, any other failure yields `db = undefined`), runs `fn`,
|
|
122
|
-
* and guarantees the DB is closed in a `finally` after `fn` has fully settled
|
|
123
|
-
* (the await is required: the callbacks are async, and closing before they
|
|
124
|
-
* settle would tear the DB down mid-write).
|
|
125
|
-
*/
|
|
126
|
-
async function withRegistryCacheDb(fn) {
|
|
127
|
-
let db;
|
|
128
|
-
try {
|
|
129
|
-
db = openDatabase();
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
// Never mask the bun-test isolation guard as "DB unavailable".
|
|
133
|
-
rethrowIfTestIsolationError(err);
|
|
134
|
-
db = undefined;
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
return await fn(db);
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
if (db) {
|
|
141
|
-
try {
|
|
142
|
-
closeDatabase(db);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
/* ignore */
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
55
|
async function loadIndex(entry) {
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
// Never mask the bun-test isolation guard as "DB unavailable" — see
|
|
161
|
-
// rethrowIfTestIsolationError in src/core/errors.ts. Without this, a
|
|
162
|
-
// leaky test silently gets a cold cache instead of the loud
|
|
163
|
-
// TEST_ISOLATION_MISSING failure the guard intends.
|
|
164
|
-
rethrowIfTestIsolationError(err);
|
|
165
|
-
// index.db read failed (pre-migration install or test env) — fall through
|
|
166
|
-
}
|
|
167
|
-
if (dbCacheResult) {
|
|
168
|
-
const index = parseRegistryIndex(JSON.parse(dbCacheResult.indexJson));
|
|
169
|
-
if (index) {
|
|
170
|
-
return index;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// ── Step 2: Fetch fresh index from remote ────────────────────────────────
|
|
174
|
-
try {
|
|
56
|
+
return fetchCachedJson({
|
|
57
|
+
cacheKey: entry.url,
|
|
58
|
+
ttlMs: CACHE_TTL_MS,
|
|
59
|
+
// Both the fresh hit and the stale fallback parse identically; a corrupt
|
|
60
|
+
// cache row lets JSON.parse throw out of the load (legacy behaviour).
|
|
61
|
+
parseCache: (json) => parseRegistryIndex(JSON.parse(json)) ?? undefined,
|
|
62
|
+
fetchFresh: async () => {
|
|
175
63
|
const response = await fetchWithRetry(entry.url, undefined, { timeout: 10_000 });
|
|
176
64
|
if (!response.ok) {
|
|
177
65
|
throw new Error(`HTTP ${response.status}`);
|
|
@@ -180,31 +68,13 @@ async function loadIndex(entry) {
|
|
|
180
68
|
// responses from a compromised server would OOM us.
|
|
181
69
|
const data = await jsonWithByteCap(response, 50 * 1024 * 1024);
|
|
182
70
|
const index = parseRegistryIndex(data);
|
|
183
|
-
if (index) {
|
|
184
|
-
|
|
185
|
-
if (db) {
|
|
186
|
-
try {
|
|
187
|
-
const etag = response.headers.get("etag") ?? undefined;
|
|
188
|
-
const lastModified = response.headers.get("last-modified") ?? undefined;
|
|
189
|
-
upsertRegistryIndexCache(db, entry.url, JSON.stringify(index), { etag, lastModified });
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
/* best-effort */
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return index;
|
|
196
|
-
}
|
|
197
|
-
throw new Error("Invalid registry index format");
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
// Fetch failed — use stale DB cache if available
|
|
201
|
-
if (dbCacheResult) {
|
|
202
|
-
const index = parseRegistryIndex(JSON.parse(dbCacheResult.indexJson));
|
|
203
|
-
if (index)
|
|
204
|
-
return index;
|
|
71
|
+
if (!index) {
|
|
72
|
+
throw new Error("Invalid registry index format");
|
|
205
73
|
}
|
|
206
|
-
|
|
207
|
-
|
|
74
|
+
const etag = response.headers.get("etag") ?? undefined;
|
|
75
|
+
const lastModified = response.headers.get("last-modified") ?? undefined;
|
|
76
|
+
return { value: index, cacheJson: JSON.stringify(index), cacheOpts: { etag, lastModified } };
|
|
77
|
+
},
|
|
208
78
|
});
|
|
209
79
|
}
|
|
210
80
|
export function isCacheExpired(mtimeMs) {
|
|
@@ -431,15 +301,3 @@ function asStringArray(value) {
|
|
|
431
301
|
const filtered = value.filter((v) => typeof v === "string");
|
|
432
302
|
return filtered.length > 0 ? filtered : undefined;
|
|
433
303
|
}
|
|
434
|
-
function buildInstallRef(source, ref) {
|
|
435
|
-
switch (source) {
|
|
436
|
-
case "npm":
|
|
437
|
-
return `npm:${ref}`;
|
|
438
|
-
case "git":
|
|
439
|
-
return `git+${ref}`;
|
|
440
|
-
case "local":
|
|
441
|
-
return `file:${ref}`;
|
|
442
|
-
default:
|
|
443
|
-
return `github:${ref}`;
|
|
444
|
-
}
|
|
445
|
-
}
|
package/dist/registry/resolve.js
CHANGED
|
@@ -70,6 +70,19 @@ export function parseRegistryRef(rawRef) {
|
|
|
70
70
|
}
|
|
71
71
|
return parseGithubShorthand(ref, ref);
|
|
72
72
|
}
|
|
73
|
+
/** Inverse of {@link parseRegistryRef}: build the install ref for a source kind. */
|
|
74
|
+
export function buildInstallRef(source, ref) {
|
|
75
|
+
switch (source) {
|
|
76
|
+
case "npm":
|
|
77
|
+
return `npm:${ref}`;
|
|
78
|
+
case "git":
|
|
79
|
+
return `git+${ref}`;
|
|
80
|
+
case "local":
|
|
81
|
+
return `file:${ref}`;
|
|
82
|
+
case "github":
|
|
83
|
+
return `github:${ref}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
/**
|
|
74
87
|
* Known prefixes that `parseRegistryRef` handles as installable sources.
|
|
75
88
|
* Anything with a colon that doesn't start with one of these is likely a
|
|
@@ -105,16 +118,16 @@ function detectRegistrySearchId(ref) {
|
|
|
105
118
|
return lines.join("\n");
|
|
106
119
|
}
|
|
107
120
|
export async function resolveRegistryArtifact(parsed) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
switch (parsed.source) {
|
|
122
|
+
case "npm":
|
|
123
|
+
return resolveNpmArtifact(parsed);
|
|
124
|
+
case "local":
|
|
125
|
+
return resolveLocalArtifact(parsed);
|
|
126
|
+
case "git":
|
|
127
|
+
return resolveGitArtifact(parsed);
|
|
128
|
+
case "github":
|
|
129
|
+
return resolveGithubArtifact(parsed);
|
|
116
130
|
}
|
|
117
|
-
return resolveGithubArtifact(parsed);
|
|
118
131
|
}
|
|
119
132
|
function parseNpmRef(input, originalRef) {
|
|
120
133
|
const trimmed = input.trim();
|