@cullit/config 1.0.0 → 1.5.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/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ interface SourceConfig {
17
17
  type: string;
18
18
  owner?: string;
19
19
  repo?: string;
20
+ repoPath?: string;
20
21
  enrichment?: EnrichmentType[];
21
22
  }
22
23
  interface PublishTarget {
@@ -54,10 +55,18 @@ interface ConfluenceConfig {
54
55
  interface NotionConfig {
55
56
  databaseId: string;
56
57
  }
58
+ interface RepoSource {
59
+ url?: string;
60
+ path?: string;
61
+ name?: string;
62
+ from?: string;
63
+ to?: string;
64
+ }
57
65
  interface CullConfig {
58
66
  ai: AIConfig;
59
67
  source: SourceConfig;
60
68
  publish: PublishTarget[];
69
+ repos?: RepoSource[];
61
70
  jira?: JiraConfig;
62
71
  linear?: LinearConfig;
63
72
  openclaw?: OpenClawConfig;
@@ -74,4 +83,4 @@ interface CullConfig {
74
83
  */
75
84
  declare function loadConfig(cwdOrPath?: string): CullConfig;
76
85
 
77
- export { type AIConfig, type AIProvider, type Audience, type BitbucketConfig, type ConfluenceConfig, type CullConfig, type EnrichmentType, type GitLabConfig, type JiraConfig, type LinearConfig, type NotionConfig, type OpenClawConfig, type OutputFormat, type PublishTarget, type PublisherType, type SourceConfig, type Tone, loadConfig };
86
+ export { type AIConfig, type AIProvider, type Audience, type BitbucketConfig, type ConfluenceConfig, type CullConfig, type EnrichmentType, type GitLabConfig, type JiraConfig, type LinearConfig, type NotionConfig, type OpenClawConfig, type OutputFormat, type PublishTarget, type PublisherType, type RepoSource, type SourceConfig, type Tone, loadConfig };
package/dist/index.js CHANGED
@@ -25,6 +25,14 @@ function loadConfig(cwdOrPath = process.cwd()) {
25
25
  return mergeWithDefaults(resolved);
26
26
  }
27
27
  function parseSimpleYaml(raw) {
28
+ const RESERVED_KEYS = ["__proto__", "constructor", "prototype"];
29
+ const safeKey = (k) => {
30
+ const trimmed = k.trim();
31
+ if (RESERVED_KEYS.includes(trimmed)) {
32
+ throw new Error(`Config error: reserved key "${trimmed}" is not allowed`);
33
+ }
34
+ return trimmed;
35
+ };
28
36
  const result = {};
29
37
  let currentSection = "";
30
38
  let currentArray = null;
@@ -37,10 +45,10 @@ function parseSimpleYaml(raw) {
37
45
  const [key, ...valParts] = trimmed.split(":");
38
46
  const val = valParts.join(":").trim();
39
47
  if (val) {
40
- result[key.trim()] = parseValue(val);
48
+ result[safeKey(key)] = parseValue(val);
41
49
  } else {
42
- result[key.trim()] = {};
43
- currentSection = key.trim();
50
+ result[safeKey(key)] = {};
51
+ currentSection = safeKey(key);
44
52
  }
45
53
  currentArray = null;
46
54
  continue;
@@ -58,7 +66,7 @@ function parseSimpleYaml(raw) {
58
66
  }
59
67
  const obj = {};
60
68
  const [k, ...vParts] = content.split(":");
61
- obj[k.trim()] = parseValue(vParts.join(":").trim());
69
+ obj[safeKey(k)] = parseValue(vParts.join(":").trim());
62
70
  currentArray.push(obj);
63
71
  } else {
64
72
  if (currentSection && currentArrayKey) {
@@ -82,15 +90,15 @@ function parseSimpleYaml(raw) {
82
90
  if (currentArray && currentArray.length > 0) {
83
91
  const lastObj = currentArray[currentArray.length - 1];
84
92
  if (typeof lastObj === "object") {
85
- lastObj[key.trim()] = parseValue(val);
93
+ lastObj[safeKey(key)] = parseValue(val);
86
94
  continue;
87
95
  }
88
96
  }
89
97
  if (currentSection) {
90
- result[currentSection][key.trim()] = parseValue(val);
98
+ result[currentSection][safeKey(key)] = parseValue(val);
91
99
  }
92
100
  } else {
93
- currentArrayKey = key.trim();
101
+ currentArrayKey = safeKey(key);
94
102
  currentArray = null;
95
103
  if (currentSection) {
96
104
  result[currentSection][currentArrayKey] = {};
@@ -118,9 +126,11 @@ function parseValue(val) {
118
126
  return val;
119
127
  }
120
128
  function resolveEnvVars(obj) {
121
- if (typeof obj === "string" && obj.startsWith("$")) {
122
- const envKey = obj.substring(1);
123
- return process.env[envKey] || obj;
129
+ if (typeof obj === "string") {
130
+ if (obj.startsWith("$")) {
131
+ const envKey = obj.startsWith("${") && obj.endsWith("}") ? obj.slice(2, -1) : obj.substring(1);
132
+ return process.env[envKey] || obj;
133
+ }
124
134
  }
125
135
  if (Array.isArray(obj)) return obj.map(resolveEnvVars);
126
136
  if (obj && typeof obj === "object") {
@@ -145,15 +155,53 @@ function mergeWithDefaults(parsed) {
145
155
  publish: normalizePublishTargets(parsed.publish || DEFAULT_CONFIG.publish),
146
156
  jira: parsed.jira,
147
157
  linear: parsed.linear,
148
- openclaw: parsed.openclaw
158
+ openclaw: parsed.openclaw,
159
+ gitlab: parsed.gitlab,
160
+ bitbucket: parsed.bitbucket,
161
+ confluence: parsed.confluence,
162
+ notion: parsed.notion,
163
+ ...parsed.repos ? { repos: validateRepos(parsed.repos) } : {}
149
164
  };
150
165
  }
166
+ function validateRepos(repos) {
167
+ if (!Array.isArray(repos)) {
168
+ throw new Error('Config error: "repos" must be an array');
169
+ }
170
+ return repos.map((repo, i) => {
171
+ if (!repo || typeof repo !== "object") {
172
+ throw new Error(`Config error: repos[${i}] must be an object`);
173
+ }
174
+ if (!repo.url && !repo.path) {
175
+ throw new Error(`Config error: repos[${i}] must have either "url" or "path"`);
176
+ }
177
+ if (repo.url && typeof repo.url !== "string") {
178
+ throw new Error(`Config error: repos[${i}].url must be a string`);
179
+ }
180
+ if (repo.path && typeof repo.path !== "string") {
181
+ throw new Error(`Config error: repos[${i}].path must be a string`);
182
+ }
183
+ return repo;
184
+ });
185
+ }
151
186
  function normalizePublishTargets(targets) {
152
187
  return targets.map((t) => {
153
- const normalized = { type: t.type };
154
- if (t.webhookUrl || t.webhook_url) normalized.webhookUrl = t.webhookUrl || t.webhook_url;
155
- if (t.channel) normalized.channel = t.channel;
156
- if (t.path) normalized.path = t.path;
188
+ const normalized = { ...t };
189
+ if (t.webhook_url && !t.webhookUrl) {
190
+ normalized.webhookUrl = t.webhook_url;
191
+ delete normalized["webhook_url"];
192
+ }
193
+ if (t.parent_page_id && !t.parentPageId) {
194
+ normalized.parentPageId = t.parent_page_id;
195
+ delete normalized["parent_page_id"];
196
+ }
197
+ if (t.space_key && !t.spaceKey) {
198
+ normalized.spaceKey = t.space_key;
199
+ delete normalized["space_key"];
200
+ }
201
+ if (t.database_id && !t.databaseId) {
202
+ normalized.databaseId = t.database_id;
203
+ delete normalized["database_id"];
204
+ }
157
205
  return normalized;
158
206
  });
159
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullit/config",
3
- "version": "1.0.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Config loader for Cullit — YAML config parsing with env var resolution.",
6
6
  "license": "MIT",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "scripts": {
28
28
  "build": "tsup src/index.ts --format esm --dts --clean",
29
- "dev": "tsup src/index.ts --format esm --watch"
29
+ "dev": "tsup src/index.ts --format esm --watch",
30
+ "test": "vitest run"
30
31
  }
31
32
  }