@ant.sh/colony 0.1.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/dist/cjs/cli.js +281 -0
  4. package/dist/cjs/cli.js.map +7 -0
  5. package/dist/cjs/index.js +383 -0
  6. package/dist/cjs/index.js.map +7 -0
  7. package/dist/cjs/package.json +3 -0
  8. package/dist/cjs/parser.js +319 -0
  9. package/dist/cjs/parser.js.map +7 -0
  10. package/dist/cjs/providers/aws.js +115 -0
  11. package/dist/cjs/providers/aws.js.map +7 -0
  12. package/dist/cjs/providers/openbao.js +49 -0
  13. package/dist/cjs/providers/openbao.js.map +7 -0
  14. package/dist/cjs/providers/vault-base.js +98 -0
  15. package/dist/cjs/providers/vault-base.js.map +7 -0
  16. package/dist/cjs/providers/vault.js +49 -0
  17. package/dist/cjs/providers/vault.js.map +7 -0
  18. package/dist/cjs/resolver.js +247 -0
  19. package/dist/cjs/resolver.js.map +7 -0
  20. package/dist/cjs/secrets.js +238 -0
  21. package/dist/cjs/secrets.js.map +7 -0
  22. package/dist/cjs/strings.js +99 -0
  23. package/dist/cjs/strings.js.map +7 -0
  24. package/dist/cjs/util.js +74 -0
  25. package/dist/cjs/util.js.map +7 -0
  26. package/dist/esm/cli.js +281 -0
  27. package/dist/esm/cli.js.map +7 -0
  28. package/dist/esm/index.d.ts +342 -0
  29. package/dist/esm/index.js +347 -0
  30. package/dist/esm/index.js.map +7 -0
  31. package/dist/esm/package.json +3 -0
  32. package/dist/esm/parser.js +286 -0
  33. package/dist/esm/parser.js.map +7 -0
  34. package/dist/esm/providers/aws.js +82 -0
  35. package/dist/esm/providers/aws.js.map +7 -0
  36. package/dist/esm/providers/openbao.js +26 -0
  37. package/dist/esm/providers/openbao.js.map +7 -0
  38. package/dist/esm/providers/vault-base.js +75 -0
  39. package/dist/esm/providers/vault-base.js.map +7 -0
  40. package/dist/esm/providers/vault.js +26 -0
  41. package/dist/esm/providers/vault.js.map +7 -0
  42. package/dist/esm/resolver.js +224 -0
  43. package/dist/esm/resolver.js.map +7 -0
  44. package/dist/esm/secrets.js +209 -0
  45. package/dist/esm/secrets.js.map +7 -0
  46. package/dist/esm/strings.js +75 -0
  47. package/dist/esm/strings.js.map +7 -0
  48. package/dist/esm/util.js +47 -0
  49. package/dist/esm/util.js.map +7 -0
  50. package/package.json +66 -0
  51. package/src/cli.js +353 -0
  52. package/src/index.d.ts +342 -0
  53. package/src/index.js +473 -0
  54. package/src/parser.js +381 -0
  55. package/src/providers/aws.js +112 -0
  56. package/src/providers/openbao.js +32 -0
  57. package/src/providers/vault-base.js +92 -0
  58. package/src/providers/vault.js +31 -0
  59. package/src/resolver.js +286 -0
  60. package/src/secrets.js +313 -0
  61. package/src/strings.js +84 -0
  62. package/src/util.js +49 -0
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@ant.sh/colony",
3
+ "version": "0.1.0",
4
+ "description": "Scope-based config loader for Node.js with rules, interpolation, and multi-environment support",
5
+ "type": "module",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/esm/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "colony": "./src/cli.js"
18
+ },
19
+ "files": [
20
+ "dist/",
21
+ "src/*.js",
22
+ "src/*.d.ts",
23
+ "src/providers/",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "node build.js",
29
+ "build:playground": "node scripts/build-playground.js",
30
+ "test": "node --test test/*.test.js",
31
+ "test:watch": "node --test --watch test/*.test.js",
32
+ "lint": "node ./src/cli.js lint --entry ./examples/config/app.colony",
33
+ "prepublishOnly": "npm run build && npm test"
34
+ },
35
+ "keywords": [
36
+ "config",
37
+ "configuration",
38
+ "settings",
39
+ "environment",
40
+ "scope-based",
41
+ "rules",
42
+ "interpolation",
43
+ "multi-environment",
44
+ "cli"
45
+ ],
46
+ "author": "Zaxuhe",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/Zaxuhe/colony.git"
51
+ },
52
+ "homepage": "https://colony.ant.sh",
53
+ "bugs": {
54
+ "url": "https://github.com/Zaxuhe/colony/issues"
55
+ },
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ },
59
+ "dependencies": {
60
+ "fast-glob": "^3.3.2",
61
+ "json5": "^2.2.3"
62
+ },
63
+ "devDependencies": {
64
+ "esbuild": "^0.27.2"
65
+ }
66
+ }
package/src/cli.js ADDED
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+ import { loadColony, validateColony, dryRunIncludes, diffColony, lintColony } from "./index.js";
3
+ import { getByPath } from "./util.js";
4
+
5
+ function parseArgs(argv) {
6
+ const args = { _: [] };
7
+ for (let i = 2; i < argv.length; i++) {
8
+ const a = argv[i];
9
+ if (a.startsWith("--")) {
10
+ const k = a.slice(2);
11
+ const v = (i + 1 < argv.length && !argv[i + 1].startsWith("--")) ? argv[++i] : true;
12
+ args[k] = v;
13
+ } else {
14
+ args._.push(a);
15
+ }
16
+ }
17
+ return args;
18
+ }
19
+
20
+ function parseCtx(s) {
21
+ const out = {};
22
+ if (!s || s === true) return out;
23
+ const parts = String(s).split(/\s+/).filter(Boolean);
24
+ for (const p of parts) {
25
+ const idx = p.indexOf("=");
26
+ if (idx === -1) continue;
27
+ out[p.slice(0, idx)] = p.slice(idx + 1);
28
+ }
29
+ return out;
30
+ }
31
+
32
+ function printUsage() {
33
+ console.error(`Usage:
34
+ colony print --entry ./config/app.colony [options]
35
+ colony validate --entry ./config/app.colony
36
+ colony dry-run --entry ./config/app.colony
37
+ colony diff --entry ./config/app.colony --ctx1 "env=dev" --ctx2 "env=prod"
38
+ colony keys --entry ./config/app.colony [--ctx "..."]
39
+ colony env --entry ./config/app.colony [--ctx "..."]
40
+ colony lint --entry ./config/app.colony
41
+
42
+ Commands:
43
+ print Resolve and print the configuration
44
+ validate Check syntax of all colony files without resolving
45
+ dry-run List all files that would be included
46
+ diff Compare configs between two contexts
47
+ keys List all config keys in dot notation
48
+ env Output config as KEY=value for shell sourcing
49
+ lint Check for potential issues (unused rules, shadows, etc.)
50
+
51
+ Options for 'print':
52
+ --entry <file> Entry colony file (required)
53
+ --dims <d1,d2,...> Dimension names (comma-separated)
54
+ --ctx "k1=v1 k2=v2" Context values (space-separated key=value pairs)
55
+ --format <json|env> Output format (default: json)
56
+ --query <key.path> Extract specific value (like jq)
57
+ --explain <key.path> Show which rule set a specific key
58
+ --base-path <dir> Restrict includes to this directory (security)
59
+ --allowed-env <v1,v2> Whitelist of allowed env vars (security)
60
+ --allowed-vars <v1,v2> Whitelist of allowed custom vars (security)
61
+ --max-file-size <bytes> Maximum file size for includes (security)
62
+ --warn-skipped Warn when skipping already-visited includes
63
+ --show-warnings Show all warnings after output
64
+ --strict Exit with error if there are any warnings
65
+
66
+ Options for 'diff':
67
+ --entry <file> Entry colony file (required)
68
+ --dims <d1,d2,...> Dimension names (comma-separated)
69
+ --ctx1 "k1=v1 ..." First context
70
+ --ctx2 "k1=v1 ..." Second context
71
+ --format <json|text> Output format (default: text)
72
+ `);
73
+ }
74
+
75
+ function formatDiff(diff, format = "text") {
76
+ if (format === "json") {
77
+ return JSON.stringify(diff, null, 2);
78
+ }
79
+
80
+ const lines = [];
81
+
82
+ if (diff.added.length > 0) {
83
+ lines.push("Added:");
84
+ for (const key of diff.added) {
85
+ lines.push(` + ${key}`);
86
+ }
87
+ }
88
+
89
+ if (diff.removed.length > 0) {
90
+ if (lines.length > 0) lines.push("");
91
+ lines.push("Removed:");
92
+ for (const key of diff.removed) {
93
+ lines.push(` - ${key}`);
94
+ }
95
+ }
96
+
97
+ if (diff.changed.length > 0) {
98
+ if (lines.length > 0) lines.push("");
99
+ lines.push("Changed:");
100
+ for (const { key, from, to } of diff.changed) {
101
+ lines.push(` ~ ${key}`);
102
+ lines.push(` from: ${JSON.stringify(from)}`);
103
+ lines.push(` to: ${JSON.stringify(to)}`);
104
+ }
105
+ }
106
+
107
+ if (lines.length === 0) {
108
+ return "No differences found.";
109
+ }
110
+
111
+ return lines.join("\n");
112
+ }
113
+
114
+ /**
115
+ * Format config as KEY=value for shell sourcing
116
+ */
117
+ function formatAsEnv(cfg, prefix = "") {
118
+ const lines = [];
119
+
120
+ function flatten(obj, currentPrefix) {
121
+ for (const [key, value] of Object.entries(obj)) {
122
+ const envKey = currentPrefix
123
+ ? `${currentPrefix}_${key}`.toUpperCase().replace(/[^A-Z0-9_]/g, "_")
124
+ : key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
125
+
126
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
127
+ flatten(value, envKey);
128
+ } else {
129
+ const envValue = typeof value === "string"
130
+ ? value
131
+ : JSON.stringify(value);
132
+ // Escape single quotes for shell
133
+ const escaped = envValue.replace(/'/g, "'\\''");
134
+ lines.push(`${envKey}='${escaped}'`);
135
+ }
136
+ }
137
+ }
138
+
139
+ flatten(cfg, prefix);
140
+ return lines.sort().join("\n");
141
+ }
142
+
143
+ (async () => {
144
+ try {
145
+ const args = parseArgs(process.argv);
146
+ const cmd = args._[0];
147
+
148
+ if (!cmd || cmd === "help" || args.help) {
149
+ printUsage();
150
+ process.exit(cmd === "help" || args.help ? 0 : 1);
151
+ }
152
+
153
+ const entry = args.entry;
154
+ if (!entry) {
155
+ console.error("Error: Missing --entry\n");
156
+ printUsage();
157
+ process.exit(1);
158
+ }
159
+
160
+ // Handle validate command
161
+ if (cmd === "validate") {
162
+ const result = await validateColony(entry);
163
+ if (result.valid) {
164
+ console.log(`✓ All ${result.files.length} file(s) valid:`);
165
+ for (const f of result.files) {
166
+ console.log(` ${f}`);
167
+ }
168
+ process.exit(0);
169
+ } else {
170
+ console.error(`✗ Validation failed with ${result.errors.length} error(s):`);
171
+ for (const e of result.errors) {
172
+ console.error(`\n ${e.file}:`);
173
+ console.error(` ${e.error}`);
174
+ }
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ // Handle dry-run command
180
+ if (cmd === "dry-run") {
181
+ const files = await dryRunIncludes(entry);
182
+ console.log(`Files that would be included (${files.length}):`);
183
+ for (const f of files) {
184
+ console.log(` ${f}`);
185
+ }
186
+ process.exit(0);
187
+ }
188
+
189
+ // Handle diff command
190
+ if (cmd === "diff") {
191
+ if (!args.ctx1 || !args.ctx2) {
192
+ console.error("Error: diff command requires both --ctx1 and --ctx2\n");
193
+ printUsage();
194
+ process.exit(1);
195
+ }
196
+
197
+ const dims = typeof args.dims === "string"
198
+ ? args.dims.split(",").map((s) => s.trim()).filter(Boolean)
199
+ : undefined;
200
+
201
+ const ctx1 = parseCtx(args.ctx1);
202
+ const ctx2 = parseCtx(args.ctx2);
203
+
204
+ const result = await diffColony({ entry, dims, ctx1, ctx2 });
205
+
206
+ const format = args.format || "text";
207
+ console.log(formatDiff(result.diff, format));
208
+
209
+ // Exit with code 1 if there are differences (useful for CI)
210
+ const hasDiffs = result.diff.added.length > 0 ||
211
+ result.diff.removed.length > 0 ||
212
+ result.diff.changed.length > 0;
213
+ process.exit(hasDiffs ? 1 : 0);
214
+ }
215
+
216
+ // Handle lint command
217
+ if (cmd === "lint") {
218
+ const dims = typeof args.dims === "string"
219
+ ? args.dims.split(",").map((s) => s.trim()).filter(Boolean)
220
+ : undefined;
221
+
222
+ const result = await lintColony({ entry, dims });
223
+
224
+ if (result.issues.length === 0) {
225
+ console.log("✓ No issues found");
226
+ process.exit(0);
227
+ }
228
+
229
+ console.error(`Found ${result.issues.length} issue(s):\n`);
230
+ for (const issue of result.issues) {
231
+ const icon = issue.severity === "error" ? "✗" : "⚠";
232
+ console.error(`${icon} [${issue.type}] ${issue.message}`);
233
+ if (issue.file) {
234
+ console.error(` at ${issue.file}:${issue.line || 0}`);
235
+ }
236
+ }
237
+ process.exit(result.issues.some((i) => i.severity === "error") ? 1 : 0);
238
+ }
239
+
240
+ // Handle keys command
241
+ if (cmd === "keys") {
242
+ const dims = typeof args.dims === "string"
243
+ ? args.dims.split(",").map((s) => s.trim()).filter(Boolean)
244
+ : undefined;
245
+
246
+ const ctx = parseCtx(args.ctx);
247
+ const cfg = await loadColony({ entry, dims, ctx });
248
+
249
+ const keys = cfg.keys();
250
+ for (const key of keys) {
251
+ console.log(key);
252
+ }
253
+ process.exit(0);
254
+ }
255
+
256
+ // Handle env command
257
+ if (cmd === "env") {
258
+ const dims = typeof args.dims === "string"
259
+ ? args.dims.split(",").map((s) => s.trim()).filter(Boolean)
260
+ : undefined;
261
+
262
+ const ctx = parseCtx(args.ctx);
263
+ const cfg = await loadColony({ entry, dims, ctx });
264
+
265
+ console.log(formatAsEnv(cfg.toJSON(), args.prefix || ""));
266
+ process.exit(0);
267
+ }
268
+
269
+ // Handle print command
270
+ if (cmd !== "print") {
271
+ console.error(`Error: Unknown command "${cmd}"\n`);
272
+ printUsage();
273
+ process.exit(1);
274
+ }
275
+
276
+ const dims = typeof args.dims === "string"
277
+ ? args.dims.split(",").map((s) => s.trim()).filter(Boolean)
278
+ : undefined;
279
+
280
+ const ctx = parseCtx(args.ctx);
281
+
282
+ // Build sandbox options
283
+ const sandbox = {};
284
+ if (args["base-path"]) {
285
+ sandbox.basePath = args["base-path"];
286
+ }
287
+ if (args["allowed-env"]) {
288
+ sandbox.allowedEnvVars = args["allowed-env"].split(",").map((s) => s.trim()).filter(Boolean);
289
+ }
290
+ if (args["allowed-vars"]) {
291
+ sandbox.allowedVars = args["allowed-vars"].split(",").map((s) => s.trim()).filter(Boolean);
292
+ }
293
+ if (args["max-file-size"]) {
294
+ sandbox.maxFileSize = parseInt(args["max-file-size"], 10);
295
+ }
296
+
297
+ const warnOnSkippedIncludes = !!args["warn-skipped"];
298
+
299
+ const cfg = await loadColony({ entry, dims, ctx, sandbox, warnOnSkippedIncludes });
300
+
301
+ // Handle --query option
302
+ if (args.query) {
303
+ const value = getByPath(cfg, args.query);
304
+ if (value === undefined) {
305
+ console.error(`Key not found: ${args.query}`);
306
+ process.exit(1);
307
+ }
308
+ if (typeof value === "object") {
309
+ console.log(JSON.stringify(value, null, 2));
310
+ } else {
311
+ console.log(value);
312
+ }
313
+ } else {
314
+ // Format output
315
+ const format = args.format || "json";
316
+ if (format === "env") {
317
+ console.log(formatAsEnv(cfg.toJSON(), args.prefix || ""));
318
+ } else {
319
+ console.log(JSON.stringify(cfg, null, 2));
320
+ }
321
+ }
322
+
323
+ // Handle --strict flag
324
+ if (args.strict && cfg._warnings?.length > 0) {
325
+ console.error(`\n✗ Strict mode: ${cfg._warnings.length} warning(s) found:`);
326
+ for (const w of cfg._warnings) {
327
+ console.error(` [${w.type}] ${w.message}`);
328
+ }
329
+ process.exit(1);
330
+ }
331
+
332
+ // Show warnings if requested
333
+ if (args["show-warnings"] && cfg._warnings?.length > 0) {
334
+ console.error(`\nWarnings (${cfg._warnings.length}):`);
335
+ for (const w of cfg._warnings) {
336
+ console.error(` [${w.type}] ${w.message}`);
337
+ }
338
+ }
339
+
340
+ if (typeof args.explain === "string") {
341
+ const info = cfg.explain(args.explain);
342
+ console.error(`\nExplain ${args.explain}:`);
343
+ console.error(info ? JSON.stringify(info, null, 2) : "(no matching rule / unset)");
344
+ }
345
+
346
+ } catch (err) {
347
+ console.error(`Error: ${err.message}`);
348
+ if (process.env.DEBUG) {
349
+ console.error(err.stack);
350
+ }
351
+ process.exit(1);
352
+ }
353
+ })();