@elmundi/ship-cli 0.8.1 → 0.11.2

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.
Files changed (76) hide show
  1. package/README.md +415 -22
  2. package/bin/shipctl.mjs +165 -0
  3. package/lib/adapters/_fs.mjs +165 -0
  4. package/lib/adapters/agents/index.mjs +26 -0
  5. package/lib/adapters/ci/azure-pipelines.mjs +23 -0
  6. package/lib/adapters/ci/buildkite.mjs +24 -0
  7. package/lib/adapters/ci/circleci.mjs +23 -0
  8. package/lib/adapters/ci/gh-actions.mjs +29 -0
  9. package/lib/adapters/ci/gitlab-ci.mjs +23 -0
  10. package/lib/adapters/ci/jenkins.mjs +23 -0
  11. package/lib/adapters/ci/manual.mjs +18 -0
  12. package/lib/adapters/index.mjs +122 -0
  13. package/lib/adapters/language/dart.mjs +23 -0
  14. package/lib/adapters/language/go.mjs +23 -0
  15. package/lib/adapters/language/java.mjs +27 -0
  16. package/lib/adapters/language/js.mjs +32 -0
  17. package/lib/adapters/language/kotlin.mjs +48 -0
  18. package/lib/adapters/language/py.mjs +34 -0
  19. package/lib/adapters/language/rust.mjs +23 -0
  20. package/lib/adapters/language/swift.mjs +37 -0
  21. package/lib/adapters/language/ts.mjs +35 -0
  22. package/lib/adapters/trackers/azure-boards.mjs +49 -0
  23. package/lib/adapters/trackers/clickup.mjs +43 -0
  24. package/lib/adapters/trackers/github-issues.mjs +52 -0
  25. package/lib/adapters/trackers/jira.mjs +72 -0
  26. package/lib/adapters/trackers/linear.mjs +62 -0
  27. package/lib/adapters/trackers/none.mjs +18 -0
  28. package/lib/adapters/trackers/spreadsheet.mjs +28 -0
  29. package/lib/artifacts/fs-index.mjs +230 -0
  30. package/lib/bootstrap/render.mjs +373 -0
  31. package/lib/cache/store.mjs +422 -0
  32. package/lib/commands/bootstrap.mjs +4 -0
  33. package/lib/commands/callback.mjs +302 -0
  34. package/lib/commands/config.mjs +257 -0
  35. package/lib/commands/docs.mjs +1 -1
  36. package/lib/commands/doctor.mjs +583 -0
  37. package/lib/commands/feedback.mjs +355 -0
  38. package/lib/commands/help.mjs +96 -21
  39. package/lib/commands/init.mjs +830 -158
  40. package/lib/commands/kickoff.mjs +192 -0
  41. package/lib/commands/knowledge.mjs +368 -0
  42. package/lib/commands/lanes.mjs +502 -0
  43. package/lib/commands/manifest-catalog.mjs +102 -38
  44. package/lib/commands/migrate.mjs +204 -0
  45. package/lib/commands/new.mjs +452 -0
  46. package/lib/commands/patterns.mjs +9 -43
  47. package/lib/commands/run.mjs +617 -0
  48. package/lib/commands/sync.mjs +749 -0
  49. package/lib/commands/telemetry.mjs +390 -0
  50. package/lib/commands/verify.mjs +187 -0
  51. package/lib/config/io.mjs +232 -0
  52. package/lib/config/migrate.mjs +215 -0
  53. package/lib/config/schema.mjs +650 -0
  54. package/lib/detect.mjs +162 -19
  55. package/lib/feedback/drafts.mjs +129 -0
  56. package/lib/find-ship-root.mjs +16 -10
  57. package/lib/http.mjs +237 -11
  58. package/lib/state/idempotency.mjs +183 -0
  59. package/lib/state/lockfile.mjs +180 -0
  60. package/lib/telemetry/outbox.mjs +224 -0
  61. package/lib/templates.mjs +53 -65
  62. package/lib/verify/checks/agents-on-disk.mjs +58 -0
  63. package/lib/verify/checks/api-reachable.mjs +39 -0
  64. package/lib/verify/checks/artifacts-up-to-date.mjs +78 -0
  65. package/lib/verify/checks/bootstrap-files.mjs +67 -0
  66. package/lib/verify/checks/cache-integrity.mjs +51 -0
  67. package/lib/verify/checks/ci-secrets.mjs +86 -0
  68. package/lib/verify/checks/config-present.mjs +39 -0
  69. package/lib/verify/checks/gitignore-cache.mjs +51 -0
  70. package/lib/verify/checks/rules-markers.mjs +135 -0
  71. package/lib/verify/checks/stack-enums.mjs +33 -0
  72. package/lib/verify/checks/tracker-labels.mjs +91 -0
  73. package/lib/verify/registry.mjs +120 -0
  74. package/lib/version.mjs +34 -0
  75. package/package.json +10 -3
  76. package/bin/ship.mjs +0 -68
@@ -0,0 +1,23 @@
1
+ import { isFile } from "../_fs.mjs";
2
+
3
+ export const id = "rust";
4
+ export const kind = "language";
5
+
6
+ export async function detect(cwd) {
7
+ if (isFile(cwd, "Cargo.toml")) {
8
+ return {
9
+ present: true,
10
+ confidence: 1,
11
+ evidence: [{ type: "file", where: "Cargo.toml", match: "present" }],
12
+ };
13
+ }
14
+ return { present: false, confidence: 0, evidence: [] };
15
+ }
16
+
17
+ export async function bootstrap() {
18
+ return { todo: true };
19
+ }
20
+
21
+ export async function verify() {
22
+ return { todo: true };
23
+ }
@@ -0,0 +1,37 @@
1
+ import { isFile, listDir, readText } from "../_fs.mjs";
2
+
3
+ export const id = "swift";
4
+ export const kind = "language";
5
+
6
+ export async function detect(cwd) {
7
+ const evidence = [];
8
+
9
+ if (isFile(cwd, "Package.swift")) {
10
+ evidence.push({ type: "file", where: "Package.swift", match: "present" });
11
+ }
12
+
13
+ for (const name of listDir(cwd, ".")) {
14
+ if (name.endsWith(".xcodeproj")) {
15
+ evidence.push({ type: "dir", where: name, match: "xcodeproj" });
16
+ break;
17
+ }
18
+ }
19
+
20
+ if (isFile(cwd, "Podfile")) {
21
+ const body = readText(cwd, "Podfile") || "";
22
+ if (/\bswift\b/i.test(body) || /:swift/i.test(body)) {
23
+ evidence.push({ type: "file", where: "Podfile", match: "swift pods" });
24
+ }
25
+ }
26
+
27
+ if (!evidence.length) return { present: false, confidence: 0, evidence };
28
+ return { present: true, confidence: 1, evidence };
29
+ }
30
+
31
+ export async function bootstrap() {
32
+ return { todo: true };
33
+ }
34
+
35
+ export async function verify() {
36
+ return { todo: true };
37
+ }
@@ -0,0 +1,35 @@
1
+ import { isFile, pkgDeps, readJson } from "../_fs.mjs";
2
+
3
+ export const id = "ts";
4
+ export const kind = "language";
5
+
6
+ export async function detect(cwd) {
7
+ const evidence = [];
8
+ let hit = false;
9
+
10
+ if (isFile(cwd, "tsconfig.json")) {
11
+ hit = true;
12
+ evidence.push({ type: "file", where: "tsconfig.json", match: "present" });
13
+ }
14
+
15
+ const pkg = readJson(cwd, "package.json");
16
+ const deps = pkgDeps(pkg);
17
+ if (deps.typescript) {
18
+ hit = true;
19
+ evidence.push({
20
+ type: "package",
21
+ where: "package.json",
22
+ match: `typescript@${deps.typescript}`,
23
+ });
24
+ }
25
+
26
+ return { present: hit, confidence: hit ? 1 : 0, evidence };
27
+ }
28
+
29
+ export async function bootstrap() {
30
+ return { todo: true };
31
+ }
32
+
33
+ export async function verify() {
34
+ return { todo: true };
35
+ }
@@ -0,0 +1,49 @@
1
+ import { exists, readEnvFiles, readGithubWorkflows } from "../_fs.mjs";
2
+
3
+ export const id = "azure-boards";
4
+ export const kind = "tracker";
5
+
6
+ const ENV_KEYS = /\b(AZURE_DEVOPS_PAT|AZURE_DEVOPS_ORG|AZURE_DEVOPS_EXT_PAT)\b/;
7
+
8
+ export async function detect(cwd) {
9
+ const evidence = [];
10
+ let envHit = false;
11
+ let ciHit = false;
12
+
13
+ for (const { file, content } of readEnvFiles(cwd)) {
14
+ const m = content.match(ENV_KEYS);
15
+ if (m) {
16
+ envHit = true;
17
+ evidence.push({ type: "env", where: file, match: m[1] });
18
+ }
19
+ }
20
+
21
+ if (exists(cwd, ".vsts-ci.yml")) {
22
+ ciHit = true;
23
+ evidence.push({ type: "file", where: ".vsts-ci.yml", match: "present" });
24
+ }
25
+
26
+ for (const { file, content } of readGithubWorkflows(cwd)) {
27
+ if (/azure[-_ ]?boards/i.test(content) || /dev\.azure\.com/i.test(content)) {
28
+ ciHit = true;
29
+ evidence.push({ type: "workflow", where: file, match: "azure-boards reference" });
30
+ }
31
+ }
32
+
33
+ const present = envHit || ciHit;
34
+ let confidence = 0;
35
+ if (present) {
36
+ confidence = 0.7;
37
+ if (envHit && ciHit) confidence = 0.95;
38
+ }
39
+
40
+ return { present, confidence, evidence };
41
+ }
42
+
43
+ export async function bootstrap() {
44
+ return { todo: true };
45
+ }
46
+
47
+ export async function verify() {
48
+ return { todo: true };
49
+ }
@@ -0,0 +1,43 @@
1
+ import { pkgDeps, readEnvFiles, readJson } from "../_fs.mjs";
2
+
3
+ export const id = "clickup";
4
+ export const kind = "tracker";
5
+
6
+ export async function detect(cwd) {
7
+ const evidence = [];
8
+ let envHit = false;
9
+ let packageHit = false;
10
+
11
+ for (const { file, content } of readEnvFiles(cwd)) {
12
+ if (/\bCLICKUP_API_TOKEN\b/.test(content)) {
13
+ envHit = true;
14
+ evidence.push({ type: "env", where: file, match: "CLICKUP_API_TOKEN" });
15
+ }
16
+ }
17
+
18
+ const pkg = readJson(cwd, "package.json");
19
+ const deps = pkgDeps(pkg);
20
+ for (const name of Object.keys(deps)) {
21
+ if (name.startsWith("@clickup/")) {
22
+ packageHit = true;
23
+ evidence.push({ type: "package", where: "package.json", match: name });
24
+ }
25
+ }
26
+
27
+ const present = envHit || packageHit;
28
+ let confidence = 0;
29
+ if (present) {
30
+ confidence = 0.7;
31
+ if (envHit && packageHit) confidence = 0.95;
32
+ }
33
+
34
+ return { present, confidence, evidence };
35
+ }
36
+
37
+ export async function bootstrap() {
38
+ return { todo: true };
39
+ }
40
+
41
+ export async function verify() {
42
+ return { todo: true };
43
+ }
@@ -0,0 +1,52 @@
1
+ import { isDir, listDir, readEnvFiles, readGithubWorkflows } from "../_fs.mjs";
2
+
3
+ export const id = "github-issues";
4
+ export const kind = "tracker";
5
+
6
+ export async function detect(cwd) {
7
+ const evidence = [];
8
+ let templateHit = false;
9
+ let tokenHit = false;
10
+
11
+ if (isDir(cwd, ".github", "ISSUE_TEMPLATE")) {
12
+ const entries = listDir(cwd, ".github/ISSUE_TEMPLATE").filter((n) => !n.startsWith("."));
13
+ templateHit = true;
14
+ evidence.push({
15
+ type: "dir",
16
+ where: ".github/ISSUE_TEMPLATE/",
17
+ match: `${entries.length} template(s)`,
18
+ });
19
+ }
20
+
21
+ for (const { file, content } of readEnvFiles(cwd)) {
22
+ if (/\bGITHUB_TOKEN\b/.test(content)) {
23
+ tokenHit = true;
24
+ evidence.push({ type: "env", where: file, match: "GITHUB_TOKEN" });
25
+ break;
26
+ }
27
+ }
28
+ if (!tokenHit) {
29
+ for (const { file, content } of readGithubWorkflows(cwd)) {
30
+ if (/\bGITHUB_TOKEN\b/.test(content)) {
31
+ tokenHit = true;
32
+ evidence.push({ type: "workflow", where: file, match: "GITHUB_TOKEN" });
33
+ break;
34
+ }
35
+ }
36
+ }
37
+
38
+ const present = templateHit || tokenHit;
39
+ let confidence = 0;
40
+ if (templateHit) confidence = 0.8;
41
+ else if (tokenHit) confidence = 0.3;
42
+
43
+ return { present, confidence, evidence };
44
+ }
45
+
46
+ export async function bootstrap() {
47
+ return { todo: true };
48
+ }
49
+
50
+ export async function verify() {
51
+ return { todo: true };
52
+ }
@@ -0,0 +1,72 @@
1
+ import {
2
+ exists,
3
+ readEnvFiles,
4
+ readGithubWorkflows,
5
+ readJson,
6
+ } from "../_fs.mjs";
7
+
8
+ export const id = "jira";
9
+ export const kind = "tracker";
10
+
11
+ const ENV_KEYS = /\b(JIRA_URL|JIRA_API_TOKEN|ATLASSIAN_TOKEN|ATLASSIAN_API_TOKEN)\b/;
12
+
13
+ export async function detect(cwd) {
14
+ const evidence = [];
15
+ let envHit = false;
16
+ let workflowHit = false;
17
+ let fileHit = false;
18
+
19
+ for (const { file, content } of readEnvFiles(cwd)) {
20
+ const m = content.match(ENV_KEYS);
21
+ if (m) {
22
+ envHit = true;
23
+ evidence.push({ type: "env", where: file, match: m[1] });
24
+ }
25
+ }
26
+
27
+ for (const { file, content } of readGithubWorkflows(cwd)) {
28
+ if (/\bjira\b/i.test(content) || /atlassian/i.test(content)) {
29
+ workflowHit = true;
30
+ evidence.push({ type: "workflow", where: file, match: "jira/atlassian reference" });
31
+ }
32
+ }
33
+
34
+ if (exists(cwd, ".jira")) {
35
+ fileHit = true;
36
+ evidence.push({ type: "dir", where: ".jira/", match: "present" });
37
+ }
38
+ if (exists(cwd, "atlassian-connect.json")) {
39
+ fileHit = true;
40
+ evidence.push({ type: "file", where: "atlassian-connect.json", match: "present" });
41
+ }
42
+
43
+ const pkg = readJson(cwd, "package.json");
44
+ const scripts = pkg && typeof pkg === "object" ? pkg.scripts : null;
45
+ if (scripts && typeof scripts === "object") {
46
+ for (const [k, v] of Object.entries(scripts)) {
47
+ if (typeof v === "string" && /\bjira\b/i.test(v)) {
48
+ workflowHit = true;
49
+ evidence.push({ type: "script", where: `package.json:scripts.${k}`, match: "jira reference" });
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ const present = envHit || workflowHit || fileHit;
56
+ let confidence = 0;
57
+ if (present) {
58
+ confidence = 0.7;
59
+ if (envHit && (workflowHit || fileHit)) confidence = 0.95;
60
+ if (fileHit && workflowHit) confidence = Math.max(confidence, 0.85);
61
+ }
62
+
63
+ return { present, confidence, evidence };
64
+ }
65
+
66
+ export async function bootstrap() {
67
+ return { todo: true };
68
+ }
69
+
70
+ export async function verify() {
71
+ return { todo: true };
72
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ exists,
3
+ pkgDeps,
4
+ readEnvFiles,
5
+ readGithubWorkflows,
6
+ readJson,
7
+ } from "../_fs.mjs";
8
+
9
+ export const id = "linear";
10
+ export const kind = "tracker";
11
+
12
+ export async function detect(cwd) {
13
+ const evidence = [];
14
+ let envHit = false;
15
+ let workflowHit = false;
16
+ let packageHit = false;
17
+
18
+ for (const { file, content } of readEnvFiles(cwd)) {
19
+ if (/\bLINEAR_API_KEY\b/.test(content)) {
20
+ envHit = true;
21
+ evidence.push({ type: "env", where: file, match: "LINEAR_API_KEY" });
22
+ }
23
+ }
24
+
25
+ const pkg = readJson(cwd, "package.json");
26
+ const deps = pkgDeps(pkg);
27
+ for (const name of Object.keys(deps)) {
28
+ if (name.startsWith("@linear/")) {
29
+ packageHit = true;
30
+ evidence.push({ type: "package", where: "package.json", match: name });
31
+ }
32
+ }
33
+
34
+ for (const { file, content } of readGithubWorkflows(cwd)) {
35
+ if (/\blinear\b/i.test(content)) {
36
+ workflowHit = true;
37
+ evidence.push({ type: "workflow", where: file, match: "linear reference" });
38
+ }
39
+ }
40
+
41
+ if (exists(cwd, ".linear")) {
42
+ packageHit = true;
43
+ evidence.push({ type: "dir", where: ".linear/", match: "present" });
44
+ }
45
+
46
+ const present = envHit || workflowHit || packageHit;
47
+ let confidence = 0;
48
+ if (present) {
49
+ confidence = 0.7;
50
+ if (envHit && (workflowHit || packageHit)) confidence = 0.95;
51
+ }
52
+
53
+ return { present, confidence, evidence };
54
+ }
55
+
56
+ export async function bootstrap() {
57
+ return { todo: true };
58
+ }
59
+
60
+ export async function verify() {
61
+ return { todo: true };
62
+ }
@@ -0,0 +1,18 @@
1
+ export const id = "none";
2
+ export const kind = "tracker";
3
+
4
+ export async function detect() {
5
+ return {
6
+ present: true,
7
+ confidence: 0.05,
8
+ evidence: [{ type: "fallback", where: "-", match: "no tracker detected" }],
9
+ };
10
+ }
11
+
12
+ export async function bootstrap() {
13
+ return { todo: true };
14
+ }
15
+
16
+ export async function verify() {
17
+ return { todo: true };
18
+ }
@@ -0,0 +1,28 @@
1
+ import { exists } from "../_fs.mjs";
2
+
3
+ export const id = "spreadsheet";
4
+ export const kind = "tracker";
5
+
6
+ export async function detect(cwd) {
7
+ const evidence = [];
8
+ let hit = false;
9
+
10
+ if (exists(cwd, ".ship", "tracker-sheet.csv")) {
11
+ hit = true;
12
+ evidence.push({ type: "file", where: ".ship/tracker-sheet.csv", match: "present" });
13
+ }
14
+ if (exists(cwd, "tracker.xlsx")) {
15
+ hit = true;
16
+ evidence.push({ type: "file", where: "tracker.xlsx", match: "present" });
17
+ }
18
+
19
+ return { present: hit, confidence: hit ? 0.9 : 0, evidence };
20
+ }
21
+
22
+ export async function bootstrap() {
23
+ return { todo: true };
24
+ }
25
+
26
+ export async function verify() {
27
+ return { todo: true };
28
+ }
@@ -0,0 +1,230 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Shared filesystem index for v2 artifact trees: walks
6
+ * `<repoRoot>/artifacts/<plural>/<id>/ARTIFACT.md` and parses just enough YAML
7
+ * front-matter to reconstruct the same entry shape we used to read out of the
8
+ * legacy `<plural>/manifest.json` files.
9
+ *
10
+ * Zero dependencies — only node builtins. Anything we cannot parse falls
11
+ * through (the entry still gets emitted with whatever fields we recovered).
12
+ */
13
+
14
+ const KIND_TO_PLURAL = {
15
+ pattern: "patterns",
16
+ tool: "tools",
17
+ collection: "collections",
18
+ };
19
+
20
+ /**
21
+ * @param {"pattern"|"tool"|"collection"} kind
22
+ */
23
+ export function pluralFor(kind) {
24
+ return KIND_TO_PLURAL[kind] || `${kind}s`;
25
+ }
26
+
27
+ /**
28
+ * Walk `artifacts/<plural>/*` and return the parsed entries (same shape as the
29
+ * legacy manifest).
30
+ *
31
+ * @param {string} repoRoot
32
+ * @param {"pattern"|"tool"|"collection"} kind
33
+ * @returns {Array<Record<string, any>>}
34
+ */
35
+ export function scanArtifacts(repoRoot, kind) {
36
+ const plural = pluralFor(kind);
37
+ const dir = path.join(repoRoot, "artifacts", plural);
38
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
39
+
40
+ const ids = fs.readdirSync(dir, { withFileTypes: true })
41
+ .filter((e) => e.isDirectory())
42
+ .map((e) => e.name)
43
+ .sort();
44
+
45
+ /** @type {Array<Record<string, any>>} */
46
+ const out = [];
47
+ for (const id of ids) {
48
+ const file = path.join(dir, id, "ARTIFACT.md");
49
+ if (!fs.existsSync(file)) continue;
50
+ let raw;
51
+ try {
52
+ raw = fs.readFileSync(file, "utf8");
53
+ } catch {
54
+ continue;
55
+ }
56
+ const { fm } = parseFrontMatter(raw);
57
+ const entry = entryFromFrontmatter(fm, kind, id);
58
+ out.push(entry);
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /**
64
+ * Read the full ARTIFACT.md (frontmatter + body) for a specific id. Returns
65
+ * null when the file is absent so callers can emit the same "Unknown id"
66
+ * messages they did before.
67
+ *
68
+ * @param {string} repoRoot
69
+ * @param {"pattern"|"tool"|"collection"} kind
70
+ * @param {string} id
71
+ */
72
+ export function readArtifactFile(repoRoot, kind, id) {
73
+ const plural = pluralFor(kind);
74
+ const file = path.join(repoRoot, "artifacts", plural, id, "ARTIFACT.md");
75
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return null;
76
+ return { absPath: file, content: fs.readFileSync(file, "utf8") };
77
+ }
78
+
79
+ function entryFromFrontmatter(fm, kind, id) {
80
+ const plural = pluralFor(kind);
81
+ const description = typeof fm.description === "string" ? fm.description : "";
82
+ const summary = description ? firstSentence(description) : "";
83
+ return {
84
+ id: typeof fm.id === "string" && fm.id ? fm.id : id,
85
+ title: typeof fm.name === "string" ? fm.name : id,
86
+ summary,
87
+ path: `artifacts/${plural}/${id}/ARTIFACT.md`,
88
+ tags: Array.isArray(fm.tags) ? fm.tags : [],
89
+ group: typeof fm.group === "string" ? fm.group : null,
90
+ version: typeof fm.version === "string" ? fm.version : null,
91
+ content_sha256: typeof fm.content_sha256 === "string" ? fm.content_sha256 : null,
92
+ updated_at: typeof fm.updated_at === "string" ? fm.updated_at : null,
93
+ channel: typeof fm.channel === "string" ? fm.channel : null,
94
+ min_shipctl: typeof fm.min_shipctl === "string" ? fm.min_shipctl : null,
95
+ deprecated: fm.deprecated === true || fm.deprecated === "true",
96
+ replaced_by: fm.replaced_by ?? null,
97
+ yanked: fm.yanked === true || fm.yanked === "true",
98
+ };
99
+ }
100
+
101
+ function firstSentence(text) {
102
+ const trimmed = text.trim();
103
+ if (!trimmed) return "";
104
+ const m = /[.!?](\s|$)/.exec(trimmed);
105
+ if (!m) return trimmed;
106
+ return trimmed.slice(0, m.index + 1).trim();
107
+ }
108
+
109
+ /**
110
+ * Tiny YAML front-matter parser tailored for v2 ARTIFACT.md files.
111
+ *
112
+ * Supports:
113
+ * - simple `key: value`
114
+ * - inline lists `key: [a, b]`
115
+ * - folded scalars `key: >` / `key: >-` with indented continuation lines
116
+ * - quoted strings (single or double)
117
+ * - one level of nested mapping (used by `spec:`)
118
+ * - comments (`# …`)
119
+ *
120
+ * Anything else is best-effort: the value is captured as the trimmed string.
121
+ *
122
+ * @param {string} source
123
+ * @returns {{fm: Record<string, any>, body: string}}
124
+ */
125
+ export function parseFrontMatter(source) {
126
+ if (typeof source !== "string") return { fm: {}, body: "" };
127
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
128
+ if (!match) return { fm: {}, body: source };
129
+ const block = match[1];
130
+ const body = source.slice(match[0].length);
131
+ /** @type {Record<string, any>} */
132
+ const fm = {};
133
+ const lines = block.split(/\r?\n/);
134
+ let i = 0;
135
+ while (i < lines.length) {
136
+ const rawLine = lines[i];
137
+ const line = rawLine.replace(/\s+$/, "");
138
+ if (!line || /^\s*#/.test(line)) {
139
+ i += 1;
140
+ continue;
141
+ }
142
+ const top = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/.exec(line);
143
+ if (!top) {
144
+ i += 1;
145
+ continue;
146
+ }
147
+ const key = top[1];
148
+ const value = top[2];
149
+
150
+ if (value === ">" || value === ">-") {
151
+ const folded = [];
152
+ i += 1;
153
+ while (i < lines.length) {
154
+ const cont = lines[i];
155
+ if (cont === "" || cont === "\r") {
156
+ // Preserve paragraph breaks as a single space in folded scalars.
157
+ folded.push("");
158
+ i += 1;
159
+ continue;
160
+ }
161
+ const m = /^(\s+)(.*)$/.exec(cont);
162
+ if (!m) break;
163
+ folded.push(m[2]);
164
+ i += 1;
165
+ }
166
+ let joined = folded.join(" ").replace(/\s+/g, " ").trim();
167
+ if (value === ">-") joined = joined.replace(/\s+$/, "");
168
+ fm[key] = joined;
169
+ continue;
170
+ }
171
+
172
+ if (/^\[.*\]$/.test(value.trim())) {
173
+ const inner = value.trim().slice(1, -1).trim();
174
+ fm[key] = inner.length ? inner.split(/\s*,\s*/).map(unquote) : [];
175
+ i += 1;
176
+ continue;
177
+ }
178
+
179
+ if (value === "") {
180
+ // Possible nested mapping or empty scalar. Peek ahead.
181
+ const child = {};
182
+ let saw = false;
183
+ let j = i + 1;
184
+ while (j < lines.length) {
185
+ const cont = lines[j];
186
+ if (!cont.trim()) { j += 1; continue; }
187
+ const indented = /^(\s{2,})([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/.exec(cont);
188
+ if (!indented) break;
189
+ const [, , subKey, subVal] = indented;
190
+ if (/^\[.*\]$/.test(subVal.trim())) {
191
+ const inner = subVal.trim().slice(1, -1).trim();
192
+ child[subKey] = inner.length ? inner.split(/\s*,\s*/).map(unquote) : [];
193
+ } else {
194
+ child[subKey] = coerceScalar(subVal);
195
+ }
196
+ saw = true;
197
+ j += 1;
198
+ }
199
+ if (saw) {
200
+ fm[key] = child;
201
+ i = j;
202
+ continue;
203
+ }
204
+ fm[key] = "";
205
+ i += 1;
206
+ continue;
207
+ }
208
+
209
+ fm[key] = coerceScalar(value);
210
+ i += 1;
211
+ }
212
+ return { fm, body };
213
+ }
214
+
215
+ function unquote(value) {
216
+ if (typeof value !== "string") return value;
217
+ const v = value.trim();
218
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
219
+ return v.slice(1, -1);
220
+ }
221
+ return v;
222
+ }
223
+
224
+ function coerceScalar(rawValue) {
225
+ const v = unquote(String(rawValue).trim());
226
+ if (v === "true") return true;
227
+ if (v === "false") return false;
228
+ if (v === "null" || v === "~") return null;
229
+ return v;
230
+ }