akm-cli 0.6.0-rc1 → 0.6.0-rc2
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 +21 -0
- package/README.md +9 -9
- package/dist/cli.js +181 -111
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/{curate.js → commands/curate.js} +8 -3
- package/dist/{info.js → commands/info.js} +15 -9
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +4 -7
- package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
- package/dist/{migration-help.js → commands/migration-help.js} +2 -2
- package/dist/{registry-search.js → commands/registry-search.js} +8 -6
- package/dist/{remember.js → commands/remember.js} +55 -49
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +3 -3
- package/dist/{stash-show.js → commands/show.js} +103 -78
- package/dist/{stash-add.js → commands/source-add.js} +42 -32
- package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +1 -1
- package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
- package/dist/{config.js → core/config.js} +79 -31
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
- package/dist/core/write-source.js +280 -0
- package/dist/{db-search.js → indexer/db-search.js} +25 -19
- package/dist/{db.js → indexer/db.js} +70 -47
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +123 -31
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +3 -6
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +52 -41
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +1 -1
- package/dist/{llm-client.js → llm/client.js} +1 -1
- package/dist/{embedders → llm/embedders}/local.js +2 -2
- package/dist/{embedders → llm/embedders}/remote.js +1 -1
- package/dist/{embedders → llm/embedders}/types.js +1 -1
- package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
- package/dist/{cli-hints.js → output/cli-hints.js} +1 -0
- package/dist/{output-context.js → output/context.js} +21 -3
- package/dist/{renderers.js → output/renderers.js} +9 -65
- package/dist/{output-shapes.js → output/shapes.js} +18 -4
- package/dist/{output-text.js → output/text.js} +1 -1
- package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/{providers → registry/providers}/index.js +1 -1
- package/dist/{providers → registry/providers}/skills-sh.js +59 -3
- package/dist/{providers → registry/providers}/static-index.js +80 -12
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +16 -56
- package/dist/{stash-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +53 -64
- package/dist/{stash-providers → sources/providers}/index.js +3 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/{stash-providers → sources/providers}/npm.js +42 -41
- package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
- package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
- package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
- package/dist/{stash-providers → sources/providers}/website.js +29 -65
- package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
- package/dist/{wiki.js → wiki/wiki.js} +12 -11
- package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +72 -28
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/migration/release-notes/0.6.0.md +69 -23
- package/package.json +1 -1
- package/dist/errors.js +0 -45
- package/dist/llm.js +0 -16
- package/dist/registry-factory.js +0 -19
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -3
- package/dist/stash-providers/filesystem.js +0 -71
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -260
- /package/dist/{common.js → core/common.js} +0 -0
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{paths.js → core/paths.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{embedder.js → llm/embedder.js} +0 -0
- /package/dist/{embedders → llm/embedders}/cache.js +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{setup-steps.js → setup/steps.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { filterNonEmptyStrings } from "./common";
|
|
5
|
+
import { ConfigError } from "./errors";
|
|
5
6
|
import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "./paths";
|
|
7
|
+
import { warn } from "./warn";
|
|
6
8
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
7
9
|
export const DEFAULT_CONFIG = {
|
|
8
10
|
semanticSearchMode: "auto",
|
|
@@ -186,16 +188,16 @@ function pickKnownKeys(raw) {
|
|
|
186
188
|
const legacySemanticSearch = raw.semanticSearch;
|
|
187
189
|
config.semanticSearchMode = legacySemanticSearch ? "auto" : "off";
|
|
188
190
|
}
|
|
189
|
-
// Migrate legacy searchPaths into
|
|
191
|
+
// Migrate legacy searchPaths into sources
|
|
190
192
|
if (Array.isArray(raw.searchPaths)) {
|
|
191
193
|
const legacyPaths = raw.searchPaths.filter((d) => typeof d === "string");
|
|
192
194
|
if (legacyPaths.length > 0) {
|
|
193
|
-
const existing = config.
|
|
195
|
+
const existing = config.sources ?? [];
|
|
194
196
|
const migrated = legacyPaths
|
|
195
197
|
.filter((p) => !existing.some((s) => s.type === "filesystem" && s.path === p))
|
|
196
198
|
.map((p) => ({ type: "filesystem", path: p }));
|
|
197
199
|
if (migrated.length > 0) {
|
|
198
|
-
config.
|
|
200
|
+
config.sources = [...existing, ...migrated];
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
203
|
}
|
|
@@ -220,9 +222,23 @@ function pickKnownKeys(raw) {
|
|
|
220
222
|
config.stashInheritance = raw.disableGlobalStashes ? "replace" : "merge";
|
|
221
223
|
config.disableGlobalStashes = raw.disableGlobalStashes;
|
|
222
224
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
// Load `sources` (new key) first, then fall back to legacy `stashes` key.
|
|
226
|
+
const sources = parseStashesConfig(raw.sources);
|
|
227
|
+
if (sources) {
|
|
228
|
+
config.sources = sources;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const legacyStashes = parseStashesConfig(raw.stashes);
|
|
232
|
+
if (legacyStashes) {
|
|
233
|
+
// Backwards-compat migration: `stashes[]` → `sources[]` in-memory.
|
|
234
|
+
// Emit a one-time deprecation warning and carry the value forward as
|
|
235
|
+
// `sources`. The renamed key is persisted on the next `akm config` write.
|
|
236
|
+
warn('Config key "stashes" is deprecated; rename it to "sources" in your config file ' +
|
|
237
|
+
`(edit it directly at ${_getConfigPath()}). ` +
|
|
238
|
+
"Your configuration has been loaded successfully — no manual action is required right now.");
|
|
239
|
+
config.sources = legacyStashes;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
226
242
|
const security = parseSecurityConfig(raw.security);
|
|
227
243
|
if (security)
|
|
228
244
|
config.security = security;
|
|
@@ -232,6 +248,18 @@ function pickKnownKeys(raw) {
|
|
|
232
248
|
if (typeof raw.writable === "boolean") {
|
|
233
249
|
config.writable = raw.writable;
|
|
234
250
|
}
|
|
251
|
+
if (typeof raw.defaultWriteTarget === "string" && raw.defaultWriteTarget.trim()) {
|
|
252
|
+
config.defaultWriteTarget = raw.defaultWriteTarget.trim();
|
|
253
|
+
}
|
|
254
|
+
if (typeof raw.search === "object" && raw.search !== null && !Array.isArray(raw.search)) {
|
|
255
|
+
const searchRaw = raw.search;
|
|
256
|
+
const searchConfig = {};
|
|
257
|
+
if (typeof searchRaw.minScore === "number" && Number.isFinite(searchRaw.minScore) && searchRaw.minScore >= 0) {
|
|
258
|
+
searchConfig.minScore = searchRaw.minScore;
|
|
259
|
+
}
|
|
260
|
+
if (Object.keys(searchConfig).length > 0)
|
|
261
|
+
config.search = searchConfig;
|
|
262
|
+
}
|
|
235
263
|
return config;
|
|
236
264
|
}
|
|
237
265
|
function readNormalizedConfig(configPath) {
|
|
@@ -435,11 +463,10 @@ function parseLlmConfig(value) {
|
|
|
435
463
|
console.warn(`[akm] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
436
464
|
return undefined;
|
|
437
465
|
}
|
|
438
|
-
|
|
439
|
-
return undefined;
|
|
466
|
+
const model = typeof obj.model === "string" ? obj.model : "";
|
|
440
467
|
const result = {
|
|
441
468
|
endpoint: obj.endpoint,
|
|
442
|
-
model
|
|
469
|
+
model,
|
|
443
470
|
};
|
|
444
471
|
if (typeof obj.provider === "string" && obj.provider) {
|
|
445
472
|
result.provider = obj.provider;
|
|
@@ -531,7 +558,7 @@ function asNonEmptyString(value) {
|
|
|
531
558
|
* Restricted to the four kinds that the install pipeline produces
|
|
532
559
|
* (`"npm" | "github" | "git" | "local"`). The full {@link KitSource} union is
|
|
533
560
|
* wider, but persisted `installed[]` entries should never carry the runtime
|
|
534
|
-
* provider kinds (`"filesystem" | "website"
|
|
561
|
+
* provider kinds (`"filesystem" | "website"`).
|
|
535
562
|
*/
|
|
536
563
|
function asKitSource(value) {
|
|
537
564
|
if (value === "npm" || value === "github" || value === "git" || value === "local")
|
|
@@ -552,7 +579,7 @@ function parseStashesConfig(value) {
|
|
|
552
579
|
if (!Array.isArray(value))
|
|
553
580
|
return undefined;
|
|
554
581
|
const entries = value
|
|
555
|
-
.map((entry) =>
|
|
582
|
+
.map((entry) => parseSourceConfigEntry(entry))
|
|
556
583
|
.filter((entry) => entry !== undefined);
|
|
557
584
|
return entries;
|
|
558
585
|
}
|
|
@@ -623,13 +650,17 @@ const STASH_TYPE_ALIASES = {
|
|
|
623
650
|
"context-hub": "git",
|
|
624
651
|
github: "git",
|
|
625
652
|
};
|
|
626
|
-
function
|
|
653
|
+
function parseSourceConfigEntry(value) {
|
|
627
654
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
628
655
|
return undefined;
|
|
629
656
|
const obj = value;
|
|
630
657
|
const rawType = asNonEmptyString(obj.type);
|
|
631
658
|
if (!rawType)
|
|
632
659
|
return undefined;
|
|
660
|
+
if (rawType === "openviking") {
|
|
661
|
+
const name = asNonEmptyString(obj.name) ?? "unnamed";
|
|
662
|
+
throw new ConfigError(`openviking is not supported in akm v1. API-backed sources will return as a\nseparate QuerySource tier post-v1. Remove the source named "${name}" from your config file\nor downgrade to 0.6.x. See docs/migration/v1.md.`, "INVALID_CONFIG_FILE", `Run \`akm remove ${name}\` then re-run, or edit your config file directly at ${_getConfigPath()} to remove the openviking entry.`);
|
|
663
|
+
}
|
|
633
664
|
const type = STASH_TYPE_ALIASES[rawType] ?? rawType;
|
|
634
665
|
const entry = { type };
|
|
635
666
|
const entryPath = asNonEmptyString(obj.path);
|
|
@@ -647,6 +678,14 @@ function parseStashConfigEntry(value) {
|
|
|
647
678
|
entry.writable = obj.writable;
|
|
648
679
|
if (typeof obj.primary === "boolean")
|
|
649
680
|
entry.primary = obj.primary;
|
|
681
|
+
// Locked decision 4 (§6 v1 implementation plan): reject writable: true on
|
|
682
|
+
// website / npm sources at config load. The next sync() would clobber
|
|
683
|
+
// writes — allowing this is a footgun, not a feature. Throw early so the
|
|
684
|
+
// user sees the problem at `akm` startup, not when they try to write.
|
|
685
|
+
if (entry.writable === true && (type === "website" || type === "npm")) {
|
|
686
|
+
const label = entry.name ? ` "${entry.name}"` : "";
|
|
687
|
+
throw new ConfigError(`writable: true is only supported on filesystem and git sources (got "${type}" on source${label}).`, "INVALID_CONFIG_FILE", "To author into a checked-out package, add the same path as a separate filesystem source.");
|
|
688
|
+
}
|
|
650
689
|
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
651
690
|
entry.options = obj.options;
|
|
652
691
|
}
|
|
@@ -655,9 +694,9 @@ function parseStashConfigEntry(value) {
|
|
|
655
694
|
entry.wikiName = wikiName;
|
|
656
695
|
return entry;
|
|
657
696
|
}
|
|
658
|
-
// ──
|
|
697
|
+
// ── ConfiguredSource runtime construction ─────────────────────────────────────────
|
|
659
698
|
/**
|
|
660
|
-
* Synthesize a stable identifier when a {@link
|
|
699
|
+
* Synthesize a stable identifier when a {@link SourceConfigEntry} omits its
|
|
661
700
|
* `name`. Uses a short hash of the discriminating fields so two equivalent
|
|
662
701
|
* entries collapse to the same generated name.
|
|
663
702
|
*/
|
|
@@ -673,8 +712,8 @@ function deriveStashEntryName(entry) {
|
|
|
673
712
|
return `${entry.type}-${hash}`;
|
|
674
713
|
}
|
|
675
714
|
/**
|
|
676
|
-
* Convert a persisted {@link
|
|
677
|
-
* {@link
|
|
715
|
+
* Convert a persisted {@link SourceConfigEntry} into the runtime
|
|
716
|
+
* {@link SourceSpec} discriminated union. Returns `undefined` when the
|
|
678
717
|
* entry is missing the fields its provider type requires (e.g. a
|
|
679
718
|
* `filesystem` entry with no `path`); callers should drop or warn for those.
|
|
680
719
|
*
|
|
@@ -682,7 +721,7 @@ function deriveStashEntryName(entry) {
|
|
|
682
721
|
* legacy aliases (`"context-hub"`, `"github"` for git) still produce a usable
|
|
683
722
|
* runtime value when a path/url is supplied.
|
|
684
723
|
*/
|
|
685
|
-
export function
|
|
724
|
+
export function parseSourceSpec(entry) {
|
|
686
725
|
switch (entry.type) {
|
|
687
726
|
case "filesystem":
|
|
688
727
|
return entry.path ? { type: "filesystem", path: entry.path } : undefined;
|
|
@@ -700,8 +739,6 @@ export function parseStashEntrySource(entry) {
|
|
|
700
739
|
...(typeof entry.options?.maxPages === "number" ? { maxPages: entry.options.maxPages } : {}),
|
|
701
740
|
}
|
|
702
741
|
: undefined;
|
|
703
|
-
case "openviking":
|
|
704
|
-
return entry.url ? { type: "openviking", url: entry.url } : undefined;
|
|
705
742
|
case "npm":
|
|
706
743
|
// Persisted `npm` stash entries are unusual but supported for symmetry.
|
|
707
744
|
return entry.path ? { type: "npm", package: entry.path } : undefined;
|
|
@@ -711,29 +748,31 @@ export function parseStashEntrySource(entry) {
|
|
|
711
748
|
}
|
|
712
749
|
}
|
|
713
750
|
/**
|
|
714
|
-
* Build the full ordered list of runtime {@link
|
|
751
|
+
* Build the full ordered list of runtime {@link ConfiguredSource} values from a
|
|
715
752
|
* loaded {@link AkmConfig}. Order is the canonical iteration order:
|
|
716
753
|
*
|
|
717
754
|
* 1. The entry marked `primary: true` (or, as a backwards-compat shim,
|
|
718
755
|
* a synthetic filesystem entry built from the top-level `stashDir`).
|
|
719
|
-
* 2. Remaining `
|
|
756
|
+
* 2. Remaining `sources[]` entries in declared order.
|
|
720
757
|
* 3. Legacy `installed[]` entries, mapped into runtime entries.
|
|
721
758
|
*
|
|
722
759
|
* Entries with `enabled: false` are still emitted — callers decide whether
|
|
723
760
|
* to honour the flag (mirrors how `installed[]` entries have always been
|
|
724
|
-
* unconditional). Entries that fail {@link
|
|
761
|
+
* unconditional). Entries that fail {@link parseSourceSpec} are
|
|
725
762
|
* dropped silently.
|
|
726
763
|
*/
|
|
727
|
-
export function
|
|
764
|
+
export function resolveConfiguredSources(config) {
|
|
728
765
|
const entries = [];
|
|
729
|
-
|
|
766
|
+
// `sources` is the canonical key. `stashes` is the legacy key that the loader
|
|
767
|
+
// migrates in-memory; only one of the two should be set at runtime.
|
|
768
|
+
const stashes = config.sources ?? config.stashes ?? [];
|
|
730
769
|
// (1) Primary entry: explicit `primary: true` wins; fall back to top-level stashDir.
|
|
731
770
|
let primary = stashes.find((entry) => entry.primary === true);
|
|
732
771
|
if (!primary && config.stashDir) {
|
|
733
772
|
primary = { type: "filesystem", path: config.stashDir, primary: true };
|
|
734
773
|
}
|
|
735
774
|
if (primary) {
|
|
736
|
-
const runtime =
|
|
775
|
+
const runtime = toConfiguredSource(primary, true);
|
|
737
776
|
if (runtime)
|
|
738
777
|
entries.push(runtime);
|
|
739
778
|
}
|
|
@@ -741,7 +780,7 @@ export function resolveStashEntries(config) {
|
|
|
741
780
|
for (const entry of stashes) {
|
|
742
781
|
if (entry === primary)
|
|
743
782
|
continue;
|
|
744
|
-
const runtime =
|
|
783
|
+
const runtime = toConfiguredSource(entry, false);
|
|
745
784
|
if (runtime)
|
|
746
785
|
entries.push(runtime);
|
|
747
786
|
}
|
|
@@ -758,8 +797,8 @@ export function resolveStashEntries(config) {
|
|
|
758
797
|
}
|
|
759
798
|
return entries;
|
|
760
799
|
}
|
|
761
|
-
function
|
|
762
|
-
const source =
|
|
800
|
+
function toConfiguredSource(persisted, isPrimary) {
|
|
801
|
+
const source = parseSourceSpec(persisted);
|
|
763
802
|
if (!source)
|
|
764
803
|
return undefined;
|
|
765
804
|
return {
|
|
@@ -841,12 +880,21 @@ function mergeLoadedConfig(base, override) {
|
|
|
841
880
|
// `disableGlobalStashes` boolean so old config files behave identically.
|
|
842
881
|
const replaceStashes = override.stashInheritance === "replace" ||
|
|
843
882
|
(override.stashInheritance === undefined && override.disableGlobalStashes === true);
|
|
883
|
+
// Merge `sources` (canonical key). Legacy `stashes` key is handled via the
|
|
884
|
+
// pickKnownKeys migration which promotes it to `sources` at load time.
|
|
885
|
+
const overrideSources = override.sources ?? override.stashes ?? [];
|
|
886
|
+
const baseSources = base.sources ?? base.stashes ?? [];
|
|
844
887
|
if (replaceStashes) {
|
|
845
|
-
merged.
|
|
888
|
+
merged.sources = [...overrideSources];
|
|
889
|
+
}
|
|
890
|
+
else if (overrideSources.length > 0) {
|
|
891
|
+
merged.sources = [...baseSources, ...overrideSources];
|
|
846
892
|
}
|
|
847
|
-
else if (
|
|
848
|
-
merged.
|
|
893
|
+
else if (baseSources.length > 0) {
|
|
894
|
+
merged.sources = [...baseSources];
|
|
849
895
|
}
|
|
896
|
+
// Clear deprecated stashes field on the merged result — sources is canonical.
|
|
897
|
+
delete merged.stashes;
|
|
850
898
|
return merged;
|
|
851
899
|
}
|
|
852
900
|
function applyRuntimeEnvApiKeys(config) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error classes for structured exit code classification.
|
|
3
|
+
*
|
|
4
|
+
* - ConfigError -> exit 78 (configuration / environment problems)
|
|
5
|
+
* - UsageError -> exit 2 (bad CLI arguments or invalid input)
|
|
6
|
+
* - NotFoundError -> exit 1 (requested resource missing)
|
|
7
|
+
*
|
|
8
|
+
* Each error carries a machine-readable `code` field. Codes are stable
|
|
9
|
+
* identifiers safe to consume from scripts and JSON output. Existing throw
|
|
10
|
+
* sites without an explicit code receive a default code per error class so
|
|
11
|
+
* older call sites continue to compile and behave unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Each error also exposes a `hint()` method returning an actionable hint
|
|
14
|
+
* string (or `undefined`). Hints can be supplied at construction time or
|
|
15
|
+
* derived from the error `code` via the per-class default mapping below.
|
|
16
|
+
* The CLI surfaces this via `error.hint()` rather than message-regex parsing.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Default hint for each ConfigError code. Keep these short, actionable, and
|
|
20
|
+
* imperative. Returning undefined means "no canned hint".
|
|
21
|
+
*/
|
|
22
|
+
const CONFIG_HINTS = {
|
|
23
|
+
STASH_DIR_NOT_FOUND: "Run `akm init` to create the default stash, or set stashDir in your config.",
|
|
24
|
+
STASH_DIR_NOT_A_DIRECTORY: "The configured stashDir exists but isn't a directory. Update stashDir to point at a folder.",
|
|
25
|
+
STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update stashDir.",
|
|
26
|
+
EMBEDDING_NOT_CONFIGURED: 'Run `akm config set embedding \'{"endpoint":"...","model":"..."}\'` to enable embeddings.',
|
|
27
|
+
LLM_NOT_CONFIGURED: 'Run `akm config set llm \'{"endpoint":"...","model":"..."}\'` to configure the LLM.',
|
|
28
|
+
};
|
|
29
|
+
/** Default hint for each UsageError code. */
|
|
30
|
+
const USAGE_HINTS = {
|
|
31
|
+
INVALID_SOURCE_VALUE: "Pick one of: stash, registry, both.",
|
|
32
|
+
INVALID_FORMAT_VALUE: "Pick one of: json, jsonl, text, yaml.",
|
|
33
|
+
INVALID_DETAIL_VALUE: "Pick one of: brief, normal, full, summary, agent.",
|
|
34
|
+
INVALID_JSON_CONFIG_VALUE: 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.',
|
|
35
|
+
MISSING_OR_AMBIGUOUS_TARGET: "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg` (not both).",
|
|
36
|
+
TARGET_NOT_UPDATABLE: "Run `akm list` to view your sources, then retry with one of those values.",
|
|
37
|
+
MISSING_REQUIRED_ARGUMENT: "Refs use the form type:name, e.g. `akm show skill:deploy` or `akm show knowledge:guide.md`.",
|
|
38
|
+
};
|
|
39
|
+
/** Default hint for each NotFoundError code. */
|
|
40
|
+
const NOT_FOUND_HINTS = {
|
|
41
|
+
SOURCE_NOT_FOUND: "Run `akm list` to view your sources, then retry with one of those values.",
|
|
42
|
+
};
|
|
43
|
+
/** Raised when configuration or environment is invalid or missing. */
|
|
44
|
+
export class ConfigError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
_hint;
|
|
47
|
+
constructor(msg, code = "INVALID_CONFIG_FILE", hint) {
|
|
48
|
+
super(msg);
|
|
49
|
+
this.name = "ConfigError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
this._hint = hint;
|
|
52
|
+
// Fixes `instanceof` checks under ES5 transpilation targets.
|
|
53
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
54
|
+
}
|
|
55
|
+
hint() {
|
|
56
|
+
return this._hint ?? CONFIG_HINTS[this.code];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Raised when the user supplies invalid arguments or input. */
|
|
60
|
+
export class UsageError extends Error {
|
|
61
|
+
code;
|
|
62
|
+
_hint;
|
|
63
|
+
constructor(msg, code = "INVALID_FLAG_VALUE", hint) {
|
|
64
|
+
super(msg);
|
|
65
|
+
this.name = "UsageError";
|
|
66
|
+
this.code = code;
|
|
67
|
+
this._hint = hint;
|
|
68
|
+
// Fixes `instanceof` checks under ES5 transpilation targets.
|
|
69
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
70
|
+
}
|
|
71
|
+
hint() {
|
|
72
|
+
return this._hint ?? USAGE_HINTS[this.code];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Raised when a requested resource (asset, entry, file) is not found. */
|
|
76
|
+
export class NotFoundError extends Error {
|
|
77
|
+
code;
|
|
78
|
+
_hint;
|
|
79
|
+
constructor(msg, code = "ASSET_NOT_FOUND", hint) {
|
|
80
|
+
super(msg);
|
|
81
|
+
this.name = "NotFoundError";
|
|
82
|
+
this.code = code;
|
|
83
|
+
this._hint = hint;
|
|
84
|
+
// Fixes `instanceof` checks under ES5 transpilation targets.
|
|
85
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
86
|
+
}
|
|
87
|
+
hint() {
|
|
88
|
+
return this._hint ?? NOT_FOUND_HINTS[this.code];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* **Limitations**: This is a hand-rolled YAML-subset parser with intentional
|
|
13
13
|
* constraints for simplicity and safety:
|
|
14
|
-
* - **
|
|
15
|
-
*
|
|
14
|
+
* - **Top-level values**: string, boolean, and number scalars are supported,
|
|
15
|
+
* as well as top-level list-valued keys using YAML block sequences
|
|
16
|
+
* (`- item`) or flow arrays (`[a, b, c]`).
|
|
17
|
+
* - **List item types**: list items must be scalar values and may be strings,
|
|
18
|
+
* booleans, or numbers.
|
|
16
19
|
* - **No nested objects beyond one level**: Only a single level of indented
|
|
17
20
|
* key-value pairs is supported.
|
|
18
|
-
* - **Scalar values only**: string, boolean, and number scalars are supported.
|
|
19
21
|
*/
|
|
20
22
|
export function parseFrontmatter(raw) {
|
|
21
23
|
const parsedBlock = parseFrontmatterBlock(raw);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-source — the only place in the codebase that branches on `source.kind`.
|
|
3
|
+
*
|
|
4
|
+
* v1 architecture spec §2.6 / §2.7 / §10 step 5: writing to a source is *not*
|
|
5
|
+
* a SourceProvider interface concern. It's a small command-layer helper that
|
|
6
|
+
* does a plain filesystem write, plus a git-specific commit (and optional
|
|
7
|
+
* push) when the source is backed by a git working tree.
|
|
8
|
+
*
|
|
9
|
+
* If a third kind ever needs special write handling, it gets added here. For
|
|
10
|
+
* v1 there are exactly two cases. Adding more parallel scoring systems for
|
|
11
|
+
* different provider kinds is explicitly disallowed by CLAUDE.md.
|
|
12
|
+
*
|
|
13
|
+
* This module is the **single dispatch point** for `kind`-branching write
|
|
14
|
+
* logic. Callers (remember, import, source-add, etc.) MUST go through
|
|
15
|
+
* `writeAssetToSource` / `deleteAssetFromSource` rather than re-inlining the
|
|
16
|
+
* filesystem-write + git-commit dance.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { getCachePaths, parseGitRepoUrl } from "../sources/providers/git";
|
|
22
|
+
import { makeAssetRef } from "./asset-ref";
|
|
23
|
+
import { resolveAssetPathFromName, TYPE_DIRS } from "./asset-spec";
|
|
24
|
+
import { isWithin, resolveStashDir } from "./common";
|
|
25
|
+
import { resolveConfiguredSources } from "./config";
|
|
26
|
+
import { ConfigError, UsageError } from "./errors";
|
|
27
|
+
/**
|
|
28
|
+
* Source kinds that the loader is allowed to mark `writable: true`. Anything
|
|
29
|
+
* else is rejected at config load (per locked decision 4) — see
|
|
30
|
+
* {@link assertWritableAllowedForKind}.
|
|
31
|
+
*/
|
|
32
|
+
const REJECTED_WRITABLE_KINDS = new Set(["website", "npm"]);
|
|
33
|
+
// ── Public helpers ──────────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the effective `writable` flag for a source config entry, applying
|
|
36
|
+
* the v1 default policy from spec §5.4:
|
|
37
|
+
*
|
|
38
|
+
* - `filesystem` → `true` by default
|
|
39
|
+
* - everything else → `false` by default
|
|
40
|
+
*
|
|
41
|
+
* Users can opt out for `filesystem` via `writable: false`. They cannot opt
|
|
42
|
+
* **in** for `website` / `npm` — that combination is rejected at config load
|
|
43
|
+
* (see {@link assertWritableAllowedForKind}).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveWritable(entry) {
|
|
46
|
+
if (typeof entry.writable === "boolean")
|
|
47
|
+
return entry.writable;
|
|
48
|
+
return entry.type === "filesystem";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Reject `writable: true` on `website` / `npm` sources at config-load time.
|
|
52
|
+
* Per locked decision 4 (§6 of the v1 implementation plan): `sync()` would
|
|
53
|
+
* clobber writes on the next refresh, so allowing writes here is a footgun.
|
|
54
|
+
*
|
|
55
|
+
* Throws {@link ConfigError} when the combination is rejected.
|
|
56
|
+
*/
|
|
57
|
+
export function assertWritableAllowedForKind(entry) {
|
|
58
|
+
if (entry.writable !== true)
|
|
59
|
+
return;
|
|
60
|
+
if (REJECTED_WRITABLE_KINDS.has(entry.type)) {
|
|
61
|
+
const label = entry.name ? ` "${entry.name}"` : "";
|
|
62
|
+
throw new ConfigError(`writable: true is only supported on filesystem and git sources (got "${entry.type}" on source${label}).`, "INVALID_CONFIG_FILE", "To author into a checked-out package, add the same path as a separate filesystem source.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write a textual asset (`content`) into `source` at the path implied by
|
|
67
|
+
* `ref`. Always:
|
|
68
|
+
*
|
|
69
|
+
* 1. Refuses if `config.writable` is not truthy (per §5.4).
|
|
70
|
+
* 2. Performs a plain filesystem write to `path.join(source.path, …)`.
|
|
71
|
+
*
|
|
72
|
+
* For sources of `kind === "git"`, additionally:
|
|
73
|
+
*
|
|
74
|
+
* 3. `git -C <path> add <file>`
|
|
75
|
+
* 4. `git -C <path> commit -m "Update <ref>"`
|
|
76
|
+
* 5. `git -C <path> push` when `config.options.pushOnCommit` is truthy.
|
|
77
|
+
*
|
|
78
|
+
* Any other `kind` reaching this helper is a configuration bug — the loader
|
|
79
|
+
* rejects unsupported writable kinds — so we throw {@link ConfigError}.
|
|
80
|
+
*/
|
|
81
|
+
export async function writeAssetToSource(source, config, ref, content) {
|
|
82
|
+
ensureWritable(source, config);
|
|
83
|
+
const filePath = resolveAssetFilePath(source, ref);
|
|
84
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
85
|
+
const normalized = content.endsWith("\n") ? content : `${content}\n`;
|
|
86
|
+
fs.writeFileSync(filePath, normalized, "utf8");
|
|
87
|
+
await runKindSpecificCommit(source, config, filePath, `Update ${formatRefForMessage(ref)}`);
|
|
88
|
+
return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Delete the asset at `ref` from `source`. Symmetric to
|
|
92
|
+
* {@link writeAssetToSource}: same writable check, same git-commit-and-push
|
|
93
|
+
* convenience for `kind === "git"`.
|
|
94
|
+
*/
|
|
95
|
+
export async function deleteAssetFromSource(source, config, ref) {
|
|
96
|
+
ensureWritable(source, config);
|
|
97
|
+
const filePath = resolveAssetFilePath(source, ref);
|
|
98
|
+
if (!fs.existsSync(filePath)) {
|
|
99
|
+
throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
|
|
100
|
+
}
|
|
101
|
+
fs.unlinkSync(filePath);
|
|
102
|
+
await runKindSpecificCommit(source, config, filePath, `Remove ${formatRefForMessage(ref)}`);
|
|
103
|
+
return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the destination for a write per locked decision 3:
|
|
107
|
+
*
|
|
108
|
+
* 1. Explicit `--target <name>` (when supplied)
|
|
109
|
+
* 2. `config.defaultWriteTarget`
|
|
110
|
+
* 3. `config.stashDir` (the working stash created by `akm init`)
|
|
111
|
+
* 4. `ConfigError("no writable source configured; run `akm init`")`
|
|
112
|
+
*
|
|
113
|
+
* The legacy `first-writable-in-source-array-order` fallback is *not* used —
|
|
114
|
+
* see plan §6 decision 3 for the rationale.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveWriteTarget(akmConfig, explicitTarget) {
|
|
117
|
+
const configuredSources = resolveConfiguredSources(akmConfig);
|
|
118
|
+
// 1. Explicit --target wins.
|
|
119
|
+
if (explicitTarget) {
|
|
120
|
+
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
121
|
+
if (!match) {
|
|
122
|
+
throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
123
|
+
}
|
|
124
|
+
// Up-front writable check so an explicit --target fails fast with a
|
|
125
|
+
// ConfigError (rather than the generic UsageError ensureWritable would
|
|
126
|
+
// raise after we've already started building paths). Resolve the
|
|
127
|
+
// effective writable flag (filesystem defaults to true; everything else
|
|
128
|
+
// defaults to false) so unset values are interpreted correctly.
|
|
129
|
+
const effectiveWritable = resolveWritable({ type: match.type, writable: match.writable });
|
|
130
|
+
if (!effectiveWritable) {
|
|
131
|
+
throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass --target to a different source.`);
|
|
132
|
+
}
|
|
133
|
+
return adaptConfiguredSource(match);
|
|
134
|
+
}
|
|
135
|
+
// 2. config.defaultWriteTarget.
|
|
136
|
+
if (akmConfig.defaultWriteTarget) {
|
|
137
|
+
const match = configuredSources.find((s) => s.name === akmConfig.defaultWriteTarget);
|
|
138
|
+
if (match)
|
|
139
|
+
return adaptConfiguredSource(match);
|
|
140
|
+
// Fall through if the named target no longer exists — surface a clear error.
|
|
141
|
+
throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" does not match any configured source.`, "INVALID_CONFIG_FILE", "Update `defaultWriteTarget` in your config (run `akm config get defaultWriteTarget`) or run `akm list` to see configured sources.");
|
|
142
|
+
}
|
|
143
|
+
// 3. Working stash (config.stashDir / resolveStashDir()).
|
|
144
|
+
try {
|
|
145
|
+
const stashDir = resolveStashDir({ readOnly: true });
|
|
146
|
+
return {
|
|
147
|
+
source: { kind: "filesystem", name: "stash", path: stashDir },
|
|
148
|
+
config: { type: "filesystem", path: stashDir, name: "stash", writable: true },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Fall through to the final ConfigError below.
|
|
153
|
+
}
|
|
154
|
+
// 4. Nothing usable.
|
|
155
|
+
throw new ConfigError("no writable source configured; run `akm init`", "STASH_DIR_NOT_FOUND", "Run `akm init` to create a working stash, or set `defaultWriteTarget` in your config.");
|
|
156
|
+
}
|
|
157
|
+
// ── Internals ───────────────────────────────────────────────────────────────
|
|
158
|
+
function ensureWritable(source, config) {
|
|
159
|
+
// Apply the same default-resolution rule as resolveWritable so callers can
|
|
160
|
+
// pass through a SourceConfigEntry with an absent `writable` field.
|
|
161
|
+
const writable = resolveWritable(config);
|
|
162
|
+
if (!writable) {
|
|
163
|
+
throw new UsageError(`Source "${source.name}" is not writable. Set \`writable: true\` on the source config entry to enable writes.`, "INVALID_FLAG_VALUE");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function resolveAssetFilePath(source, ref) {
|
|
167
|
+
const typeDir = TYPE_DIRS[ref.type];
|
|
168
|
+
if (!typeDir) {
|
|
169
|
+
throw new UsageError(`Unknown asset type "${ref.type}". Cannot resolve a write path.`, "INVALID_FLAG_VALUE");
|
|
170
|
+
}
|
|
171
|
+
const typeRoot = path.join(source.path, typeDir);
|
|
172
|
+
const assetPath = resolveAssetPathFromName(ref.type, typeRoot, ref.name);
|
|
173
|
+
if (!isWithin(assetPath, typeRoot)) {
|
|
174
|
+
throw new UsageError(`Resolved asset path escapes its source: "${ref.name}" in source "${source.name}".`, "PATH_ESCAPE_VIOLATION");
|
|
175
|
+
}
|
|
176
|
+
return assetPath;
|
|
177
|
+
}
|
|
178
|
+
async function runKindSpecificCommit(source, config, filePath, message) {
|
|
179
|
+
if (source.kind === "filesystem") {
|
|
180
|
+
return; // No commit step.
|
|
181
|
+
}
|
|
182
|
+
if (source.kind === "git") {
|
|
183
|
+
runGitCommit(source.path, filePath, message);
|
|
184
|
+
if (config.options?.pushOnCommit) {
|
|
185
|
+
runGitPush(source.path);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Reject any other kind reaching the helper. The config loader is the
|
|
190
|
+
// first line of defence (assertWritableAllowedForKind), but we throw here
|
|
191
|
+
// so external callers that bypass the loader still get a clear error.
|
|
192
|
+
throw new ConfigError(`write-source: unsupported kind "${source.kind}" for source "${source.name}". ` +
|
|
193
|
+
"Writes are only defined for `filesystem` and `git` sources.", "INVALID_CONFIG_FILE", 'Set `kind: "filesystem"` (or `kind: "git"`) on the source, or add a parallel filesystem entry.');
|
|
194
|
+
}
|
|
195
|
+
function runGitCommit(repoDir, filePath, message) {
|
|
196
|
+
// Stage the specific file rather than `add -A` so unrelated working-tree
|
|
197
|
+
// changes don't get folded into the asset commit.
|
|
198
|
+
const relPath = path.relative(repoDir, filePath) || filePath;
|
|
199
|
+
const addResult = spawnSync("git", ["-C", repoDir, "add", "--", relPath], { encoding: "utf8" });
|
|
200
|
+
if (addResult.status !== 0) {
|
|
201
|
+
throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
|
|
202
|
+
}
|
|
203
|
+
// Provide a fallback identity so fresh CI/test environments without
|
|
204
|
+
// user.name/user.email configured can always commit.
|
|
205
|
+
const commitResult = spawnSync("git", ["-C", repoDir, "-c", "user.name=akm", "-c", "user.email=akm@local", "commit", "-m", message], { encoding: "utf8" });
|
|
206
|
+
if (commitResult.status !== 0) {
|
|
207
|
+
// `nothing to commit` is a no-op success — the file may have matched the
|
|
208
|
+
// existing tree exactly. Surface other errors verbatim.
|
|
209
|
+
const stderr = commitResult.stderr ?? "";
|
|
210
|
+
if (/nothing to commit|no changes added/i.test(stderr) ||
|
|
211
|
+
/nothing to commit|no changes added/i.test(commitResult.stdout ?? "")) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`git commit failed: ${stderr.trim() || "unknown error"}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function runGitPush(repoDir) {
|
|
218
|
+
const pushResult = spawnSync("git", ["-C", repoDir, "push"], { encoding: "utf8", timeout: 120_000 });
|
|
219
|
+
if (pushResult.status !== 0) {
|
|
220
|
+
throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function formatRefForMessage(ref) {
|
|
224
|
+
return ref.origin ? `${ref.origin}//${ref.type}:${ref.name}` : `${ref.type}:${ref.name}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Derive a {@link WriteTargetSource} + persisted {@link SourceConfigEntry}
|
|
228
|
+
* from the runtime {@link ConfiguredSource} representation used elsewhere in
|
|
229
|
+
* the codebase. The mapping is:
|
|
230
|
+
*
|
|
231
|
+
* ConfiguredSource.type → WriteTargetSource.kind
|
|
232
|
+
* ConfiguredSource.name → WriteTargetSource.name
|
|
233
|
+
* ConfiguredSource.source.* → WriteTargetSource.path (via parseSourceSpec)
|
|
234
|
+
*
|
|
235
|
+
* Legacy aliases (`context-hub`, `github`) have already been normalised to
|
|
236
|
+
* `git` by the config loader, so this mapping is straightforward.
|
|
237
|
+
*/
|
|
238
|
+
function adaptConfiguredSource(runtime) {
|
|
239
|
+
const writePath = pathFromConfiguredSource(runtime);
|
|
240
|
+
if (!writePath) {
|
|
241
|
+
throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
|
|
242
|
+
}
|
|
243
|
+
// Map the runtime kind to the write helper's `kind` discriminator. Only
|
|
244
|
+
// filesystem and git produce writable sources at v1.
|
|
245
|
+
const kind = runtime.type === "filesystem" || runtime.type === "git" ? runtime.type : runtime.type;
|
|
246
|
+
const config = {
|
|
247
|
+
type: runtime.type,
|
|
248
|
+
name: runtime.name,
|
|
249
|
+
...(writePath !== undefined ? { path: writePath } : {}),
|
|
250
|
+
...(runtime.writable !== undefined ? { writable: runtime.writable } : {}),
|
|
251
|
+
...(runtime.options ? { options: runtime.options } : {}),
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
source: { kind, name: runtime.name, path: writePath },
|
|
255
|
+
config,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function pathFromConfiguredSource(runtime) {
|
|
259
|
+
// ConfiguredSource.source is the parsed SourceSpec (filesystem|git|website|npm).
|
|
260
|
+
// For writable kinds we only ever care about a local on-disk path: filesystem
|
|
261
|
+
// sources expose it directly; git sources resolve through the cache mirror
|
|
262
|
+
// (handled by the existing source provider). For v1 the helper trusts
|
|
263
|
+
// callers to materialise the cache path beforehand and does not re-clone.
|
|
264
|
+
const spec = runtime.source;
|
|
265
|
+
if (spec.type === "filesystem")
|
|
266
|
+
return spec.path;
|
|
267
|
+
// For git sources we fall back to the cached repo directory the provider
|
|
268
|
+
// already materialised. The lookup is intentionally lazy — we only import
|
|
269
|
+
// it when needed to keep the helper's import graph small.
|
|
270
|
+
if (spec.type === "git") {
|
|
271
|
+
try {
|
|
272
|
+
const repo = parseGitRepoUrl(spec.url);
|
|
273
|
+
return getCachePaths(repo.canonicalUrl).repoDir;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|