akm-cli 0.6.0-rc1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +9 -9
- package/dist/cli.js +199 -114
- 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} +69 -3
- package/dist/{stash-show.js → commands/show.js} +104 -84
- 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} +133 -56
- 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} +79 -47
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +132 -33
- 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} +3 -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} +2 -2
- 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} +34 -18
- 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 +91 -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
|
@@ -30,7 +30,7 @@ export function makeAssetRef(type, name, origin) {
|
|
|
30
30
|
export function parseAssetRef(ref) {
|
|
31
31
|
const trimmed = ref.trim();
|
|
32
32
|
if (!trimmed)
|
|
33
|
-
throw new UsageError("Empty ref.");
|
|
33
|
+
throw new UsageError("Empty ref.", "MISSING_REQUIRED_ARGUMENT");
|
|
34
34
|
let origin;
|
|
35
35
|
let body = trimmed;
|
|
36
36
|
const boundary = trimmed.indexOf("//");
|
|
@@ -38,16 +38,16 @@ export function parseAssetRef(ref) {
|
|
|
38
38
|
origin = trimmed.slice(0, boundary);
|
|
39
39
|
body = trimmed.slice(boundary + 2);
|
|
40
40
|
if (!origin)
|
|
41
|
-
throw new UsageError("Empty origin in ref.");
|
|
41
|
+
throw new UsageError("Empty origin in ref.", "MISSING_REQUIRED_ARGUMENT");
|
|
42
42
|
}
|
|
43
43
|
const colon = body.indexOf(":");
|
|
44
44
|
if (colon <= 0) {
|
|
45
|
-
throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name
|
|
45
|
+
throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name, e.g. skill:deploy or knowledge:guide.md`, "MISSING_REQUIRED_ARGUMENT");
|
|
46
46
|
}
|
|
47
47
|
const rawType = body.slice(0, colon);
|
|
48
48
|
const rawName = body.slice(colon + 1);
|
|
49
49
|
if (!isAssetType(rawType)) {
|
|
50
|
-
throw new UsageError(`Invalid asset type: "${rawType}"
|
|
50
|
+
throw new UsageError(`Invalid asset type: "${rawType}".`, "MISSING_REQUIRED_ARGUMENT");
|
|
51
51
|
}
|
|
52
52
|
validateName(rawName);
|
|
53
53
|
const name = normalizeName(rawName);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* `db-search.ts` import from, eliminating the import-order dependency
|
|
11
11
|
* entirely.
|
|
12
12
|
*/
|
|
13
|
-
import { buildWorkflowAction } from "
|
|
13
|
+
import { buildWorkflowAction } from "../output/renderers";
|
|
14
14
|
/** Map asset types to their primary renderer names. */
|
|
15
15
|
export const TYPE_TO_RENDERER = {
|
|
16
16
|
script: "script-source",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { buildWorkflowAction } from "../output/renderers";
|
|
2
3
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
|
|
3
4
|
import { toPosix } from "./common";
|
|
4
|
-
import { buildWorkflowAction } from "./renderers";
|
|
5
5
|
const markdownSpec = {
|
|
6
6
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
7
7
|
toCanonicalName: (typeRoot, filePath) => {
|
|
@@ -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",
|
|
@@ -106,20 +108,7 @@ export function saveConfig(config) {
|
|
|
106
108
|
const dir = path.dirname(configPath);
|
|
107
109
|
fs.mkdirSync(dir, { recursive: true });
|
|
108
110
|
const sanitized = sanitizeConfigForWrite(config);
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
fs.writeFileSync(tmpPath, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
|
|
112
|
-
fs.renameSync(tmpPath, configPath);
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
try {
|
|
116
|
-
fs.unlinkSync(tmpPath);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
/* ignore cleanup failure */
|
|
120
|
-
}
|
|
121
|
-
throw err;
|
|
122
|
-
}
|
|
111
|
+
writeConfigObject(configPath, sanitized);
|
|
123
112
|
}
|
|
124
113
|
/**
|
|
125
114
|
* Strip apiKey fields before writing config to disk.
|
|
@@ -186,16 +175,16 @@ function pickKnownKeys(raw) {
|
|
|
186
175
|
const legacySemanticSearch = raw.semanticSearch;
|
|
187
176
|
config.semanticSearchMode = legacySemanticSearch ? "auto" : "off";
|
|
188
177
|
}
|
|
189
|
-
// Migrate legacy searchPaths into
|
|
178
|
+
// Migrate legacy searchPaths into sources
|
|
190
179
|
if (Array.isArray(raw.searchPaths)) {
|
|
191
180
|
const legacyPaths = raw.searchPaths.filter((d) => typeof d === "string");
|
|
192
181
|
if (legacyPaths.length > 0) {
|
|
193
|
-
const existing = config.
|
|
182
|
+
const existing = config.sources ?? [];
|
|
194
183
|
const migrated = legacyPaths
|
|
195
184
|
.filter((p) => !existing.some((s) => s.type === "filesystem" && s.path === p))
|
|
196
185
|
.map((p) => ({ type: "filesystem", path: p }));
|
|
197
186
|
if (migrated.length > 0) {
|
|
198
|
-
config.
|
|
187
|
+
config.sources = [...existing, ...migrated];
|
|
199
188
|
}
|
|
200
189
|
}
|
|
201
190
|
}
|
|
@@ -220,9 +209,20 @@ function pickKnownKeys(raw) {
|
|
|
220
209
|
config.stashInheritance = raw.disableGlobalStashes ? "replace" : "merge";
|
|
221
210
|
config.disableGlobalStashes = raw.disableGlobalStashes;
|
|
222
211
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
212
|
+
// Load `sources` (new key) first, then fall back to legacy `stashes` key.
|
|
213
|
+
const sources = parseStashesConfig(raw.sources);
|
|
214
|
+
if (sources) {
|
|
215
|
+
config.sources = sources;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const legacyStashes = parseStashesConfig(raw.stashes);
|
|
219
|
+
if (legacyStashes) {
|
|
220
|
+
// Backwards-compat fallback: configs that still carry `stashes[]` are
|
|
221
|
+
// normalized to `sources[]` after the raw file loader has had a chance to
|
|
222
|
+
// auto-migrate the on-disk key.
|
|
223
|
+
config.sources = legacyStashes;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
226
|
const security = parseSecurityConfig(raw.security);
|
|
227
227
|
if (security)
|
|
228
228
|
config.security = security;
|
|
@@ -232,24 +232,32 @@ function pickKnownKeys(raw) {
|
|
|
232
232
|
if (typeof raw.writable === "boolean") {
|
|
233
233
|
config.writable = raw.writable;
|
|
234
234
|
}
|
|
235
|
+
if (typeof raw.defaultWriteTarget === "string" && raw.defaultWriteTarget.trim()) {
|
|
236
|
+
config.defaultWriteTarget = raw.defaultWriteTarget.trim();
|
|
237
|
+
}
|
|
238
|
+
if (typeof raw.search === "object" && raw.search !== null && !Array.isArray(raw.search)) {
|
|
239
|
+
const searchRaw = raw.search;
|
|
240
|
+
const searchConfig = {};
|
|
241
|
+
if (typeof searchRaw.minScore === "number" && Number.isFinite(searchRaw.minScore) && searchRaw.minScore >= 0) {
|
|
242
|
+
searchConfig.minScore = searchRaw.minScore;
|
|
243
|
+
}
|
|
244
|
+
if (Object.keys(searchConfig).length > 0)
|
|
245
|
+
config.search = searchConfig;
|
|
246
|
+
}
|
|
235
247
|
return config;
|
|
236
248
|
}
|
|
237
249
|
function readNormalizedConfig(configPath) {
|
|
238
250
|
const raw = readConfigObject(configPath);
|
|
239
|
-
const
|
|
251
|
+
const migrated = raw ? maybeAutoMigrateLegacyStashes(configPath, raw) : undefined;
|
|
252
|
+
const expanded = migrated ? expandEnvVars(migrated) : undefined;
|
|
240
253
|
return expanded ? pickKnownKeys(expanded) : undefined;
|
|
241
254
|
}
|
|
242
|
-
function readNormalizedConfigFromText(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
raw = JSON.parse(stripJsonComments(text));
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
255
|
+
function readNormalizedConfigFromText(configPath, text) {
|
|
256
|
+
const raw = parseConfigObjectFromText(text);
|
|
257
|
+
if (!raw)
|
|
251
258
|
return undefined;
|
|
252
|
-
const
|
|
259
|
+
const migrated = maybeAutoMigrateLegacyStashes(configPath, raw);
|
|
260
|
+
const expanded = expandEnvVars(migrated);
|
|
253
261
|
return pickKnownKeys(expanded);
|
|
254
262
|
}
|
|
255
263
|
function parseOutputConfig(value) {
|
|
@@ -312,6 +320,14 @@ function expandEnvVars(value, fieldName) {
|
|
|
312
320
|
function readConfigObject(configPath) {
|
|
313
321
|
try {
|
|
314
322
|
const text = fs.readFileSync(configPath, "utf8");
|
|
323
|
+
return parseConfigObjectFromText(text);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function parseConfigObjectFromText(text) {
|
|
330
|
+
try {
|
|
315
331
|
const raw = JSON.parse(stripJsonComments(text));
|
|
316
332
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
317
333
|
return undefined;
|
|
@@ -321,6 +337,47 @@ function readConfigObject(configPath) {
|
|
|
321
337
|
return undefined;
|
|
322
338
|
}
|
|
323
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Best-effort on-disk config migration for the legacy `stashes` key.
|
|
342
|
+
*
|
|
343
|
+
* When a config file still uses `stashes` and does not already define
|
|
344
|
+
* `sources`, rewrite the file in place with `sources` replacing `stashes`,
|
|
345
|
+
* emit a one-time notice on success, and return the migrated object. If the
|
|
346
|
+
* rewrite fails, emit a warning and return the original object so the loader
|
|
347
|
+
* can still continue with an in-memory fallback.
|
|
348
|
+
*/
|
|
349
|
+
function maybeAutoMigrateLegacyStashes(configPath, raw) {
|
|
350
|
+
if (Object.hasOwn(raw, "sources") || !Object.hasOwn(raw, "stashes")) {
|
|
351
|
+
return raw;
|
|
352
|
+
}
|
|
353
|
+
const migrated = Object.fromEntries(Object.entries(raw).map(([key, value]) => (key === "stashes" ? ["sources", value] : [key, value])));
|
|
354
|
+
try {
|
|
355
|
+
writeConfigObject(configPath, migrated);
|
|
356
|
+
warn('Config migrated: "stashes" → "sources" in config.json');
|
|
357
|
+
return migrated;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
warn('Failed to migrate "stashes" → "sources" in config.json; continuing with the legacy key in memory. ' +
|
|
361
|
+
"Check file permissions or rename the key manually if this persists.");
|
|
362
|
+
return raw;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function writeConfigObject(configPath, config) {
|
|
366
|
+
const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
367
|
+
try {
|
|
368
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
369
|
+
fs.renameSync(tmpPath, configPath);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
try {
|
|
373
|
+
fs.unlinkSync(tmpPath);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
/* ignore cleanup failure */
|
|
377
|
+
}
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
324
381
|
/**
|
|
325
382
|
* Strip JavaScript-style comments from a JSON string (JSONC support).
|
|
326
383
|
* Handles // line comments and /* block comments while preserving
|
|
@@ -435,11 +492,10 @@ function parseLlmConfig(value) {
|
|
|
435
492
|
console.warn(`[akm] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
436
493
|
return undefined;
|
|
437
494
|
}
|
|
438
|
-
|
|
439
|
-
return undefined;
|
|
495
|
+
const model = typeof obj.model === "string" ? obj.model : "";
|
|
440
496
|
const result = {
|
|
441
497
|
endpoint: obj.endpoint,
|
|
442
|
-
model
|
|
498
|
+
model,
|
|
443
499
|
};
|
|
444
500
|
if (typeof obj.provider === "string" && obj.provider) {
|
|
445
501
|
result.provider = obj.provider;
|
|
@@ -531,7 +587,7 @@ function asNonEmptyString(value) {
|
|
|
531
587
|
* Restricted to the four kinds that the install pipeline produces
|
|
532
588
|
* (`"npm" | "github" | "git" | "local"`). The full {@link KitSource} union is
|
|
533
589
|
* wider, but persisted `installed[]` entries should never carry the runtime
|
|
534
|
-
* provider kinds (`"filesystem" | "website"
|
|
590
|
+
* provider kinds (`"filesystem" | "website"`).
|
|
535
591
|
*/
|
|
536
592
|
function asKitSource(value) {
|
|
537
593
|
if (value === "npm" || value === "github" || value === "git" || value === "local")
|
|
@@ -552,7 +608,7 @@ function parseStashesConfig(value) {
|
|
|
552
608
|
if (!Array.isArray(value))
|
|
553
609
|
return undefined;
|
|
554
610
|
const entries = value
|
|
555
|
-
.map((entry) =>
|
|
611
|
+
.map((entry) => parseSourceConfigEntry(entry))
|
|
556
612
|
.filter((entry) => entry !== undefined);
|
|
557
613
|
return entries;
|
|
558
614
|
}
|
|
@@ -623,13 +679,17 @@ const STASH_TYPE_ALIASES = {
|
|
|
623
679
|
"context-hub": "git",
|
|
624
680
|
github: "git",
|
|
625
681
|
};
|
|
626
|
-
function
|
|
682
|
+
function parseSourceConfigEntry(value) {
|
|
627
683
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
628
684
|
return undefined;
|
|
629
685
|
const obj = value;
|
|
630
686
|
const rawType = asNonEmptyString(obj.type);
|
|
631
687
|
if (!rawType)
|
|
632
688
|
return undefined;
|
|
689
|
+
if (rawType === "openviking") {
|
|
690
|
+
const name = asNonEmptyString(obj.name) ?? "unnamed";
|
|
691
|
+
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.`);
|
|
692
|
+
}
|
|
633
693
|
const type = STASH_TYPE_ALIASES[rawType] ?? rawType;
|
|
634
694
|
const entry = { type };
|
|
635
695
|
const entryPath = asNonEmptyString(obj.path);
|
|
@@ -647,6 +707,14 @@ function parseStashConfigEntry(value) {
|
|
|
647
707
|
entry.writable = obj.writable;
|
|
648
708
|
if (typeof obj.primary === "boolean")
|
|
649
709
|
entry.primary = obj.primary;
|
|
710
|
+
// Locked decision 4 (§6 v1 implementation plan): reject writable: true on
|
|
711
|
+
// website / npm sources at config load. The next sync() would clobber
|
|
712
|
+
// writes — allowing this is a footgun, not a feature. Throw early so the
|
|
713
|
+
// user sees the problem at `akm` startup, not when they try to write.
|
|
714
|
+
if (entry.writable === true && (type === "website" || type === "npm")) {
|
|
715
|
+
const label = entry.name ? ` "${entry.name}"` : "";
|
|
716
|
+
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.");
|
|
717
|
+
}
|
|
650
718
|
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
651
719
|
entry.options = obj.options;
|
|
652
720
|
}
|
|
@@ -655,9 +723,9 @@ function parseStashConfigEntry(value) {
|
|
|
655
723
|
entry.wikiName = wikiName;
|
|
656
724
|
return entry;
|
|
657
725
|
}
|
|
658
|
-
// ──
|
|
726
|
+
// ── ConfiguredSource runtime construction ─────────────────────────────────────────
|
|
659
727
|
/**
|
|
660
|
-
* Synthesize a stable identifier when a {@link
|
|
728
|
+
* Synthesize a stable identifier when a {@link SourceConfigEntry} omits its
|
|
661
729
|
* `name`. Uses a short hash of the discriminating fields so two equivalent
|
|
662
730
|
* entries collapse to the same generated name.
|
|
663
731
|
*/
|
|
@@ -673,8 +741,8 @@ function deriveStashEntryName(entry) {
|
|
|
673
741
|
return `${entry.type}-${hash}`;
|
|
674
742
|
}
|
|
675
743
|
/**
|
|
676
|
-
* Convert a persisted {@link
|
|
677
|
-
* {@link
|
|
744
|
+
* Convert a persisted {@link SourceConfigEntry} into the runtime
|
|
745
|
+
* {@link SourceSpec} discriminated union. Returns `undefined` when the
|
|
678
746
|
* entry is missing the fields its provider type requires (e.g. a
|
|
679
747
|
* `filesystem` entry with no `path`); callers should drop or warn for those.
|
|
680
748
|
*
|
|
@@ -682,7 +750,7 @@ function deriveStashEntryName(entry) {
|
|
|
682
750
|
* legacy aliases (`"context-hub"`, `"github"` for git) still produce a usable
|
|
683
751
|
* runtime value when a path/url is supplied.
|
|
684
752
|
*/
|
|
685
|
-
export function
|
|
753
|
+
export function parseSourceSpec(entry) {
|
|
686
754
|
switch (entry.type) {
|
|
687
755
|
case "filesystem":
|
|
688
756
|
return entry.path ? { type: "filesystem", path: entry.path } : undefined;
|
|
@@ -700,8 +768,6 @@ export function parseStashEntrySource(entry) {
|
|
|
700
768
|
...(typeof entry.options?.maxPages === "number" ? { maxPages: entry.options.maxPages } : {}),
|
|
701
769
|
}
|
|
702
770
|
: undefined;
|
|
703
|
-
case "openviking":
|
|
704
|
-
return entry.url ? { type: "openviking", url: entry.url } : undefined;
|
|
705
771
|
case "npm":
|
|
706
772
|
// Persisted `npm` stash entries are unusual but supported for symmetry.
|
|
707
773
|
return entry.path ? { type: "npm", package: entry.path } : undefined;
|
|
@@ -711,29 +777,31 @@ export function parseStashEntrySource(entry) {
|
|
|
711
777
|
}
|
|
712
778
|
}
|
|
713
779
|
/**
|
|
714
|
-
* Build the full ordered list of runtime {@link
|
|
780
|
+
* Build the full ordered list of runtime {@link ConfiguredSource} values from a
|
|
715
781
|
* loaded {@link AkmConfig}. Order is the canonical iteration order:
|
|
716
782
|
*
|
|
717
783
|
* 1. The entry marked `primary: true` (or, as a backwards-compat shim,
|
|
718
784
|
* a synthetic filesystem entry built from the top-level `stashDir`).
|
|
719
|
-
* 2. Remaining `
|
|
785
|
+
* 2. Remaining `sources[]` entries in declared order.
|
|
720
786
|
* 3. Legacy `installed[]` entries, mapped into runtime entries.
|
|
721
787
|
*
|
|
722
788
|
* Entries with `enabled: false` are still emitted — callers decide whether
|
|
723
789
|
* to honour the flag (mirrors how `installed[]` entries have always been
|
|
724
|
-
* unconditional). Entries that fail {@link
|
|
790
|
+
* unconditional). Entries that fail {@link parseSourceSpec} are
|
|
725
791
|
* dropped silently.
|
|
726
792
|
*/
|
|
727
|
-
export function
|
|
793
|
+
export function resolveConfiguredSources(config) {
|
|
728
794
|
const entries = [];
|
|
729
|
-
|
|
795
|
+
// `sources` is the canonical key. `stashes` is the legacy key that the loader
|
|
796
|
+
// migrates in-memory; only one of the two should be set at runtime.
|
|
797
|
+
const stashes = config.sources ?? config.stashes ?? [];
|
|
730
798
|
// (1) Primary entry: explicit `primary: true` wins; fall back to top-level stashDir.
|
|
731
799
|
let primary = stashes.find((entry) => entry.primary === true);
|
|
732
800
|
if (!primary && config.stashDir) {
|
|
733
801
|
primary = { type: "filesystem", path: config.stashDir, primary: true };
|
|
734
802
|
}
|
|
735
803
|
if (primary) {
|
|
736
|
-
const runtime =
|
|
804
|
+
const runtime = toConfiguredSource(primary, true);
|
|
737
805
|
if (runtime)
|
|
738
806
|
entries.push(runtime);
|
|
739
807
|
}
|
|
@@ -741,7 +809,7 @@ export function resolveStashEntries(config) {
|
|
|
741
809
|
for (const entry of stashes) {
|
|
742
810
|
if (entry === primary)
|
|
743
811
|
continue;
|
|
744
|
-
const runtime =
|
|
812
|
+
const runtime = toConfiguredSource(entry, false);
|
|
745
813
|
if (runtime)
|
|
746
814
|
entries.push(runtime);
|
|
747
815
|
}
|
|
@@ -758,8 +826,8 @@ export function resolveStashEntries(config) {
|
|
|
758
826
|
}
|
|
759
827
|
return entries;
|
|
760
828
|
}
|
|
761
|
-
function
|
|
762
|
-
const source =
|
|
829
|
+
function toConfiguredSource(persisted, isPrimary) {
|
|
830
|
+
const source = parseSourceSpec(persisted);
|
|
763
831
|
if (!source)
|
|
764
832
|
return undefined;
|
|
765
833
|
return {
|
|
@@ -841,12 +909,21 @@ function mergeLoadedConfig(base, override) {
|
|
|
841
909
|
// `disableGlobalStashes` boolean so old config files behave identically.
|
|
842
910
|
const replaceStashes = override.stashInheritance === "replace" ||
|
|
843
911
|
(override.stashInheritance === undefined && override.disableGlobalStashes === true);
|
|
912
|
+
// Merge `sources` (canonical key). Legacy `stashes` key is handled via the
|
|
913
|
+
// pickKnownKeys migration which promotes it to `sources` at load time.
|
|
914
|
+
const overrideSources = override.sources ?? override.stashes ?? [];
|
|
915
|
+
const baseSources = base.sources ?? base.stashes ?? [];
|
|
844
916
|
if (replaceStashes) {
|
|
845
|
-
merged.
|
|
917
|
+
merged.sources = [...overrideSources];
|
|
918
|
+
}
|
|
919
|
+
else if (overrideSources.length > 0) {
|
|
920
|
+
merged.sources = [...baseSources, ...overrideSources];
|
|
846
921
|
}
|
|
847
|
-
else if (
|
|
848
|
-
merged.
|
|
922
|
+
else if (baseSources.length > 0) {
|
|
923
|
+
merged.sources = [...baseSources];
|
|
849
924
|
}
|
|
925
|
+
// Clear deprecated stashes field on the merged result — sources is canonical.
|
|
926
|
+
delete merged.stashes;
|
|
850
927
|
return merged;
|
|
851
928
|
}
|
|
852
929
|
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);
|