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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fetchWithRetry, ResponseTooLargeError, readBodyWithByteCap } from "
|
|
5
|
-
import { ConfigError, UsageError } from "
|
|
6
|
-
import { getRegistryIndexCacheDir } from "
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { fetchWithRetry, ResponseTooLargeError, readBodyWithByteCap } from "../../core/common";
|
|
5
|
+
import { ConfigError, UsageError } from "../../core/errors";
|
|
6
|
+
import { getRegistryIndexCacheDir } from "../../core/paths";
|
|
7
|
+
import { warn } from "../../core/warn";
|
|
8
|
+
import { registerSourceProvider } from "../provider-factory";
|
|
9
|
+
import { isExpired, sanitizeString } from "./provider-utils";
|
|
9
10
|
/** Refresh website snapshots every 12 hours to balance freshness with scraping load. */
|
|
10
11
|
const CACHE_REFRESH_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
11
12
|
/** Allow up to 7 days of stale snapshots when refresh fails so search remains available during outages. */
|
|
@@ -28,68 +29,34 @@ const WEBSITE_PAGE_BYTE_CAP = 5 * 1024 * 1024;
|
|
|
28
29
|
*/
|
|
29
30
|
const WEBSITE_CRAWL_WALL_CLOCK_MS = 10 * 60 * 1000;
|
|
30
31
|
/**
|
|
31
|
-
* Website
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Website source provider — scrapes pages into a local mirror so the FTS5
|
|
33
|
+
* indexer can walk them. Implements the v1 {@link SourceProvider} interface
|
|
34
|
+
* (spec §2.1): `{ name, kind, init, path, sync }`.
|
|
35
|
+
*
|
|
36
|
+
* Reading is the indexer's job — this class doesn't implement `search` or
|
|
37
|
+
* `show`.
|
|
34
38
|
*/
|
|
35
|
-
class
|
|
36
|
-
|
|
37
|
-
kind = "syncable";
|
|
39
|
+
class WebsiteSourceProvider {
|
|
40
|
+
kind = "website";
|
|
38
41
|
name;
|
|
39
|
-
config;
|
|
42
|
+
#config;
|
|
43
|
+
#url;
|
|
40
44
|
constructor(config) {
|
|
41
|
-
this
|
|
45
|
+
this.#config = config;
|
|
42
46
|
this.name = config.name ?? "website";
|
|
43
|
-
validateWebsiteUrl(config.url ?? "");
|
|
47
|
+
this.#url = validateWebsiteUrl(config.url ?? "");
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return { hits: [] };
|
|
49
|
+
async init(_ctx) {
|
|
50
|
+
// URL validation already happens in the constructor; nothing else to do.
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw new Error("Website provider content is shown via local index");
|
|
52
|
+
path() {
|
|
53
|
+
return getCachePaths(this.#url).stashDir;
|
|
52
54
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
async sync(config, options) {
|
|
58
|
-
const cachePaths = await ensureWebsiteMirror(config, { requireStashDir: true, force: options?.force });
|
|
59
|
-
const syncedAt = (options?.now ?? new Date()).toISOString();
|
|
60
|
-
const url = config.url ?? "";
|
|
61
|
-
// #123 added "website" to the StashSource union, so we can use it directly.
|
|
62
|
-
return {
|
|
63
|
-
id: url,
|
|
64
|
-
source: "website",
|
|
65
|
-
ref: url,
|
|
66
|
-
artifactUrl: url,
|
|
67
|
-
contentDir: cachePaths.stashDir,
|
|
68
|
-
cacheDir: cachePaths.rootDir,
|
|
69
|
-
extractedDir: cachePaths.stashDir,
|
|
70
|
-
syncedAt,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
getContentDir(config) {
|
|
74
|
-
const url = config.url ?? "";
|
|
75
|
-
return getCachePaths(url).stashDir;
|
|
76
|
-
}
|
|
77
|
-
async remove(config) {
|
|
78
|
-
const url = config.url;
|
|
79
|
-
if (!url)
|
|
80
|
-
return;
|
|
81
|
-
const paths = getCachePaths(url);
|
|
82
|
-
if (isDirectory(paths.rootDir)) {
|
|
83
|
-
try {
|
|
84
|
-
fs.rmSync(paths.rootDir, { recursive: true, force: true });
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
/* best-effort */
|
|
88
|
-
}
|
|
89
|
-
}
|
|
55
|
+
async sync() {
|
|
56
|
+
await ensureWebsiteMirror(this.#config, { requireStashDir: true });
|
|
90
57
|
}
|
|
91
58
|
}
|
|
92
|
-
|
|
59
|
+
registerSourceProvider("website", (config) => new WebsiteSourceProvider(config));
|
|
93
60
|
function getCachePaths(siteUrl) {
|
|
94
61
|
const key = createHash("sha256").update(normalizeSiteUrl(siteUrl)).digest("hex").slice(0, 16);
|
|
95
62
|
const rootDir = path.join(getRegistryIndexCacheDir(), `website-${key}`);
|
|
@@ -182,12 +149,9 @@ async function crawlWebsite(startUrl, options) {
|
|
|
182
149
|
const visited = new Set();
|
|
183
150
|
const pages = [];
|
|
184
151
|
const deadline = Date.now() + WEBSITE_CRAWL_WALL_CLOCK_MS;
|
|
185
|
-
let stoppedAtDeadline = false;
|
|
186
152
|
while (queue.length > 0 && pages.length < options.maxPages) {
|
|
187
|
-
if (Date.now() > deadline)
|
|
188
|
-
stoppedAtDeadline = true;
|
|
153
|
+
if (Date.now() > deadline)
|
|
189
154
|
break;
|
|
190
|
-
}
|
|
191
155
|
const next = queue.shift();
|
|
192
156
|
if (!next)
|
|
193
157
|
break;
|
|
@@ -212,8 +176,8 @@ async function crawlWebsite(startUrl, options) {
|
|
|
212
176
|
queue.push({ url: candidate, depth: next.depth + 1 });
|
|
213
177
|
}
|
|
214
178
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
179
|
+
if (Date.now() > deadline) {
|
|
180
|
+
warn("[akm] website crawl stopped at the %ds wall-clock cap with %d/%d pages collected from %s.", WEBSITE_CRAWL_WALL_CLOCK_MS / 1000, pages.length, options.maxPages, startUrl);
|
|
217
181
|
}
|
|
218
182
|
return pages;
|
|
219
183
|
}
|
|
@@ -516,4 +480,4 @@ function safeCodePointToString(value) {
|
|
|
516
480
|
return undefined;
|
|
517
481
|
}
|
|
518
482
|
}
|
|
519
|
-
export { ensureWebsiteMirror, getCachePaths, validateWebsiteInputUrl, validateWebsiteUrl,
|
|
483
|
+
export { ensureWebsiteMirror, getCachePaths, validateWebsiteInputUrl, validateWebsiteUrl, WebsiteSourceProvider };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, resolveAssetPathFromName, TYPE_DIRS, } from "
|
|
4
|
-
import { hasErrnoCode, isWithin } from "
|
|
5
|
-
import { NotFoundError, UsageError } from "
|
|
6
|
-
import { runMatchers } from "
|
|
7
|
-
import { walkStashFlat } from "
|
|
3
|
+
import { deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, resolveAssetPathFromName, TYPE_DIRS, } from "../core/asset-spec";
|
|
4
|
+
import { hasErrnoCode, isWithin } from "../core/common";
|
|
5
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
6
|
+
import { runMatchers } from "../indexer/file-context";
|
|
7
|
+
import { walkStashFlat } from "../indexer/walker";
|
|
8
8
|
/**
|
|
9
9
|
* Resolve an asset path from a stash directory, type, and name.
|
|
10
10
|
*/
|
|
@@ -50,7 +50,8 @@ function resolveInTypeDir(stashDir, typeDir, type, name) {
|
|
|
50
50
|
function resolveAndValidateTypeRoot(root, type, name) {
|
|
51
51
|
const rootStat = readTypeRootStat(root, type, name);
|
|
52
52
|
if (!rootStat.isDirectory()) {
|
|
53
|
-
throw new NotFoundError(`
|
|
53
|
+
throw new NotFoundError(`Asset directory for ${type} assets is not accessible — got a file where a directory was expected for ref: ${type}:${name}. ` +
|
|
54
|
+
"Run `akm index` to rebuild the index, or check your source configuration.", "ASSET_NOT_FOUND", "Run `akm list` to see your configured sources and verify the source path exists.");
|
|
54
55
|
}
|
|
55
56
|
return fs.realpathSync(root);
|
|
56
57
|
}
|
|
@@ -60,7 +61,7 @@ function readTypeRootStat(root, type, name) {
|
|
|
60
61
|
}
|
|
61
62
|
catch (error) {
|
|
62
63
|
if (hasErrnoCode(error, "ENOENT")) {
|
|
63
|
-
throw new NotFoundError(`
|
|
64
|
+
throw new NotFoundError(`Asset not found for ref: ${type}:${name}. No ${type} assets are present in the configured source.`, "ASSET_NOT_FOUND", "Run `akm list` to see your configured sources, or `akm index` to rebuild the search index.");
|
|
64
65
|
}
|
|
65
66
|
throw error;
|
|
66
67
|
}
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { parse as yamlParse } from "yaml";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { buildIndexMd, buildLogMd, buildSchemaMd } from "
|
|
18
|
+
import { akmSearch } from "../commands/search";
|
|
19
|
+
import { isWithin } from "../core/common";
|
|
20
|
+
import { loadUserConfig, saveConfig } from "../core/config";
|
|
21
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
22
|
+
import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
|
|
23
|
+
import { resolveSourceEntries } from "../indexer/search-source";
|
|
24
|
+
import { buildIndexMd, buildLogMd, buildSchemaMd } from "../templates/wiki-templates";
|
|
25
25
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
26
26
|
export const WIKIS_SUBDIR = "wikis";
|
|
27
27
|
export const SCHEMA_MD = "schema.md";
|
|
@@ -64,7 +64,7 @@ function wikiNotFoundMessage(name) {
|
|
|
64
64
|
return `Wiki not found: ${name}. Run \`akm wiki create ${name}\` to create it or \`akm wiki register ${name} <path-or-repo>\` to register an external wiki.`;
|
|
65
65
|
}
|
|
66
66
|
function registeredWikiSources(stashDir) {
|
|
67
|
-
return
|
|
67
|
+
return resolveSourceEntries(stashDir)
|
|
68
68
|
.filter((source) => typeof source.wikiName === "string")
|
|
69
69
|
.map((source) => ({
|
|
70
70
|
name: source.wikiName,
|
|
@@ -76,7 +76,7 @@ function registeredWikiSources(stashDir) {
|
|
|
76
76
|
export function resolveWikiSource(stashDir, name) {
|
|
77
77
|
validateWikiName(name);
|
|
78
78
|
const wikiDir = resolveWikiDir(stashDir, name);
|
|
79
|
-
if (fs.existsSync(wikiDir)) {
|
|
79
|
+
if (fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir)) {
|
|
80
80
|
return { name, path: wikiDir, mode: "stash" };
|
|
81
81
|
}
|
|
82
82
|
const external = registeredWikiSources(stashDir).find((source) => source.name === name);
|
|
@@ -87,7 +87,7 @@ export function resolveWikiSource(stashDir, name) {
|
|
|
87
87
|
export function ensureWikiNameAvailable(stashDir, name) {
|
|
88
88
|
validateWikiName(name);
|
|
89
89
|
const wikiDir = resolveWikiDir(stashDir, name);
|
|
90
|
-
if (fs.existsSync(wikiDir)) {
|
|
90
|
+
if (fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir)) {
|
|
91
91
|
throw new UsageError(`Wiki already exists: ${name}.`, "RESOURCE_ALREADY_EXISTS");
|
|
92
92
|
}
|
|
93
93
|
const external = registeredWikiSources(stashDir).find((source) => source.name === name);
|
|
@@ -164,6 +164,17 @@ function scanWikiFiles(wikiDir) {
|
|
|
164
164
|
}
|
|
165
165
|
return { pages, raws, lastModifiedMs, pagesLastModifiedMs };
|
|
166
166
|
}
|
|
167
|
+
function hasWikiInfrastructure(wikiDir) {
|
|
168
|
+
for (const file of WIKI_SPECIAL_FILES) {
|
|
169
|
+
if (fs.existsSync(path.join(wikiDir, file)))
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function isRecognizedStashWiki(wikiDir, buckets) {
|
|
175
|
+
const scanned = buckets ?? scanWikiFiles(wikiDir);
|
|
176
|
+
return scanned.pages.length > 0 || hasWikiInfrastructure(wikiDir);
|
|
177
|
+
}
|
|
167
178
|
function readSchemaDescription(wikiDir) {
|
|
168
179
|
const schemaPath = path.join(wikiDir, SCHEMA_MD);
|
|
169
180
|
let raw;
|
|
@@ -207,6 +218,8 @@ export function listWikis(stashDir) {
|
|
|
207
218
|
}
|
|
208
219
|
const summarize = (name, dir) => {
|
|
209
220
|
const buckets = scanWikiFiles(dir);
|
|
221
|
+
if (!isRecognizedStashWiki(dir, buckets))
|
|
222
|
+
return;
|
|
210
223
|
const summary = {
|
|
211
224
|
name,
|
|
212
225
|
path: dir,
|
|
@@ -353,15 +366,18 @@ export function createWiki(stashDir, name) {
|
|
|
353
366
|
* ignore that (e.g. idempotent cleanup) by catching.
|
|
354
367
|
*/
|
|
355
368
|
export function removeWiki(stashDir, name, options = {}) {
|
|
356
|
-
|
|
357
|
-
const wikiDir =
|
|
358
|
-
|
|
369
|
+
validateWikiName(name);
|
|
370
|
+
const wikiDir = resolveWikiDir(stashDir, name);
|
|
371
|
+
const external = registeredWikiSources(stashDir).find((source) => source.name === name);
|
|
372
|
+
const isStashWiki = fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir);
|
|
373
|
+
if (!isStashWiki && external) {
|
|
359
374
|
const config = loadUserConfig();
|
|
360
|
-
const
|
|
375
|
+
const filteredSources = (config.sources ?? config.stashes ?? []).filter((entry) => entry.wikiName !== name);
|
|
361
376
|
const installed = (config.installed ?? []).filter((entry) => entry.wikiName !== name);
|
|
362
377
|
saveConfig({
|
|
363
378
|
...config,
|
|
364
|
-
|
|
379
|
+
sources: filteredSources.length > 0 ? filteredSources : undefined,
|
|
380
|
+
stashes: undefined,
|
|
365
381
|
installed: installed.length > 0 ? installed : undefined,
|
|
366
382
|
});
|
|
367
383
|
return {
|
|
@@ -372,8 +388,8 @@ export function removeWiki(stashDir, name, options = {}) {
|
|
|
372
388
|
unregistered: true,
|
|
373
389
|
};
|
|
374
390
|
}
|
|
375
|
-
if (!fs.existsSync(wikiDir)) {
|
|
376
|
-
throw new NotFoundError(
|
|
391
|
+
if (!fs.existsSync(wikiDir) || (!isStashWiki && !options.withSources)) {
|
|
392
|
+
throw new NotFoundError(wikiNotFoundMessage(name), "STASH_NOT_FOUND");
|
|
377
393
|
}
|
|
378
394
|
const wikisRoot = resolveWikisRoot(stashDir);
|
|
379
395
|
if (!isWithin(wikiDir, wikisRoot)) {
|
|
@@ -527,7 +543,7 @@ export async function searchInWiki(input) {
|
|
|
527
543
|
const rawDir = path.join(wikiDir, RAW_SUBDIR);
|
|
528
544
|
const filtered = [];
|
|
529
545
|
for (const hit of response.hits) {
|
|
530
|
-
// hits can be
|
|
546
|
+
// hits can be SourceSearchHit or RegistrySearchResultHit (union); filter
|
|
531
547
|
// by path inclusion. Registry hits have no path and are dropped.
|
|
532
548
|
if (hit.type === "registry")
|
|
533
549
|
continue;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { resolveAssetPathFromName } from "
|
|
4
|
-
import { isWithin, resolveStashDir } from "
|
|
5
|
-
import { UsageError } from "
|
|
6
|
-
import { warn } from "
|
|
7
|
-
import {
|
|
3
|
+
import { resolveAssetPathFromName } from "../core/asset-spec";
|
|
4
|
+
import { isWithin, resolveStashDir } from "../core/common";
|
|
5
|
+
import { UsageError } from "../core/errors";
|
|
6
|
+
import { warn } from "../core/warn";
|
|
7
|
+
import { parseWorkflow } from "./parser";
|
|
8
8
|
const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate({
|
|
9
9
|
title: "Example Workflow",
|
|
10
10
|
firstStepTitle: "First Step",
|
|
@@ -23,7 +23,10 @@ export function buildWorkflowTemplate(name) {
|
|
|
23
23
|
firstStepTitle: `${title} Setup`,
|
|
24
24
|
firstStepId: `${stepId}-setup`,
|
|
25
25
|
});
|
|
26
|
-
|
|
26
|
+
const result = parseWorkflow(customized, { path: `<template:${name}>` });
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
throw new UsageError(formatWorkflowErrors(`<template:${name}>`, result.errors));
|
|
29
|
+
}
|
|
27
30
|
return customized;
|
|
28
31
|
}
|
|
29
32
|
export function createWorkflowAsset(input) {
|
|
@@ -41,14 +44,10 @@ export function createWorkflowAsset(input) {
|
|
|
41
44
|
const content = input.from
|
|
42
45
|
? readWorkflowSource(input.from, stashDir)
|
|
43
46
|
: (input.content ?? buildWorkflowTemplate(normalizedName));
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (error instanceof WorkflowValidationError) {
|
|
49
|
-
throw new UsageError(error.message);
|
|
50
|
-
}
|
|
51
|
-
throw error;
|
|
47
|
+
const sourcePath = input.from ?? `workflows/${normalizedName}.md`;
|
|
48
|
+
const result = parseWorkflow(content, { path: sourcePath });
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
throw new UsageError(formatWorkflowErrors(sourcePath, result.errors));
|
|
52
51
|
}
|
|
53
52
|
fs.mkdirSync(path.dirname(assetPath), { recursive: true });
|
|
54
53
|
fs.writeFileSync(assetPath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
|
|
@@ -110,6 +109,30 @@ function slugifyWorkflowStepId(name) {
|
|
|
110
109
|
.replace(/[^a-z0-9]+/g, "-")
|
|
111
110
|
.replace(/^-+|-+$/g, "") || "workflow");
|
|
112
111
|
}
|
|
112
|
+
export function formatWorkflowErrors(path, errors) {
|
|
113
|
+
const lines = errors.map((e) => ` ${path}:${e.line} — ${e.message}`);
|
|
114
|
+
const heading = errors.length === 1 ? "Workflow has 1 error:" : `Workflow has ${errors.length} errors:`;
|
|
115
|
+
return [heading, ...lines].join("\n");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validate a workflow by ref (`workflow:<name>`) or filesystem path.
|
|
119
|
+
*
|
|
120
|
+
* Returns the parse result plus the source-relative path used. Throws
|
|
121
|
+
* `UsageError` only when the target cannot be located on disk; parse
|
|
122
|
+
* failures are returned as `{ ok: false, errors }` so callers can
|
|
123
|
+
* format them however they like.
|
|
124
|
+
*/
|
|
125
|
+
export function validateWorkflowSource(target) {
|
|
126
|
+
if (target.startsWith("workflow:")) {
|
|
127
|
+
throw new UsageError(`validateWorkflowSource expects a filesystem path; resolve refs to paths in the caller before invoking.`);
|
|
128
|
+
}
|
|
129
|
+
const resolved = path.resolve(target);
|
|
130
|
+
if (!fs.existsSync(resolved)) {
|
|
131
|
+
throw new UsageError(`Workflow file not found: "${target}".`);
|
|
132
|
+
}
|
|
133
|
+
const content = fs.readFileSync(resolved, "utf8");
|
|
134
|
+
return { path: target, parse: parseWorkflow(content, { path: target }) };
|
|
135
|
+
}
|
|
113
136
|
function renderWorkflowTemplate(input) {
|
|
114
137
|
return `---
|
|
115
138
|
description: Describe what this workflow accomplishes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UsageError } from "
|
|
1
|
+
import { UsageError } from "../core/errors";
|
|
2
2
|
export const WORKFLOW_STEP_STATES = [
|
|
3
3
|
"completed",
|
|
4
4
|
"blocked",
|
|
@@ -14,6 +14,7 @@ export const WORKFLOW_SUBCOMMANDS = new Set([
|
|
|
14
14
|
"create",
|
|
15
15
|
"template",
|
|
16
16
|
"resume",
|
|
17
|
+
"validate",
|
|
17
18
|
]);
|
|
18
19
|
export function parseWorkflowJsonObject(raw, flagName) {
|
|
19
20
|
if (!raw)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { getWorkflowDbPath } from "
|
|
4
|
+
import { getWorkflowDbPath } from "../core/paths";
|
|
5
5
|
export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
6
6
|
const dir = path.dirname(dbPath);
|
|
7
7
|
if (!fs.existsSync(dir)) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-channel cache that lets the workflow renderer hand a validated
|
|
3
|
+
* `WorkflowDocument` to the indexer without persisting it through the
|
|
4
|
+
* `entry_json` column or widening `StashEntry` with a workflow-shaped field.
|
|
5
|
+
*
|
|
6
|
+
* The renderer is called during metadata generation; the indexer writes the
|
|
7
|
+
* document to `workflow_documents` after `upsertEntry` returns the row id.
|
|
8
|
+
* A WeakMap keyed by the entry object preserves the parse work between the
|
|
9
|
+
* two phases without leaking memory if the entry is dropped.
|
|
10
|
+
*/
|
|
11
|
+
const cache = new WeakMap();
|
|
12
|
+
export function cacheWorkflowDocument(entry, doc) {
|
|
13
|
+
cache.set(entry, doc);
|
|
14
|
+
}
|
|
15
|
+
export function takeWorkflowDocument(entry) {
|
|
16
|
+
const doc = cache.get(entry);
|
|
17
|
+
if (doc !== undefined)
|
|
18
|
+
cache.delete(entry);
|
|
19
|
+
return doc;
|
|
20
|
+
}
|