@gershy/lilac 0.0.1

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/cmp/cjs/main.d.ts +75 -0
  2. package/cmp/cjs/main.js +299 -0
  3. package/cmp/cjs/package.json +1 -0
  4. package/cmp/cjs/petal/petal.d.ts +3 -0
  5. package/cmp/cjs/petal/petal.js +6 -0
  6. package/cmp/cjs/petal/terraform/terraform.d.ts +82 -0
  7. package/cmp/cjs/petal/terraform/terraform.js +194 -0
  8. package/cmp/cjs/util/aws.d.ts +5 -0
  9. package/cmp/cjs/util/aws.js +61 -0
  10. package/cmp/cjs/util/awsTerraform.d.ts +3 -0
  11. package/cmp/cjs/util/awsTerraform.js +9 -0
  12. package/cmp/cjs/util/capitalize.d.ts +2 -0
  13. package/cmp/cjs/util/capitalize.js +12 -0
  14. package/cmp/cjs/util/hash.d.ts +2 -0
  15. package/cmp/cjs/util/hash.js +14 -0
  16. package/cmp/cjs/util/logger.d.ts +20 -0
  17. package/cmp/cjs/util/logger.js +150 -0
  18. package/cmp/cjs/util/normalize.d.ts +2 -0
  19. package/cmp/cjs/util/normalize.js +25 -0
  20. package/cmp/cjs/util/procTerraform.d.ts +8 -0
  21. package/cmp/cjs/util/procTerraform.js +31 -0
  22. package/cmp/cjs/util/slashEscape.d.ts +2 -0
  23. package/cmp/cjs/util/slashEscape.js +10 -0
  24. package/cmp/cjs/util/snakeCase.d.ts +2 -0
  25. package/cmp/cjs/util/snakeCase.js +3 -0
  26. package/cmp/cjs/util/terraform.d.ts +2 -0
  27. package/cmp/cjs/util/terraform.js +11 -0
  28. package/cmp/cjs/util/tryWithHealing.d.ts +7 -0
  29. package/cmp/cjs/util/tryWithHealing.js +11 -0
  30. package/cmp/mjs/main.d.ts +75 -0
  31. package/cmp/mjs/main.js +276 -0
  32. package/cmp/mjs/package.json +1 -0
  33. package/cmp/mjs/petal/petal.d.ts +3 -0
  34. package/cmp/mjs/petal/petal.js +3 -0
  35. package/cmp/mjs/petal/terraform/terraform.d.ts +82 -0
  36. package/cmp/mjs/petal/terraform/terraform.js +188 -0
  37. package/cmp/mjs/util/aws.d.ts +5 -0
  38. package/cmp/mjs/util/aws.js +54 -0
  39. package/cmp/mjs/util/awsTerraform.d.ts +3 -0
  40. package/cmp/mjs/util/awsTerraform.js +5 -0
  41. package/cmp/mjs/util/capitalize.d.ts +2 -0
  42. package/cmp/mjs/util/capitalize.js +10 -0
  43. package/cmp/mjs/util/hash.d.ts +2 -0
  44. package/cmp/mjs/util/hash.js +12 -0
  45. package/cmp/mjs/util/logger.d.ts +20 -0
  46. package/cmp/mjs/util/logger.js +147 -0
  47. package/cmp/mjs/util/normalize.d.ts +2 -0
  48. package/cmp/mjs/util/normalize.js +23 -0
  49. package/cmp/mjs/util/procTerraform.d.ts +8 -0
  50. package/cmp/mjs/util/procTerraform.js +26 -0
  51. package/cmp/mjs/util/slashEscape.d.ts +2 -0
  52. package/cmp/mjs/util/slashEscape.js +8 -0
  53. package/cmp/mjs/util/snakeCase.d.ts +2 -0
  54. package/cmp/mjs/util/snakeCase.js +1 -0
  55. package/cmp/mjs/util/terraform.d.ts +2 -0
  56. package/cmp/mjs/util/terraform.js +6 -0
  57. package/cmp/mjs/util/tryWithHealing.d.ts +7 -0
  58. package/cmp/mjs/util/tryWithHealing.js +9 -0
  59. package/cmp/sideEffects.d.ts +2 -0
  60. package/license +15 -0
  61. package/package.json +55 -0
  62. package/readme.md +39 -0
@@ -0,0 +1,20 @@
1
+ type LoggerData = null | boolean | number | string | {
2
+ [limn]: (...args: any) => LoggerData;
3
+ } | LoggerData[] | {
4
+ [k: string]: LoggerData;
5
+ };
6
+ export default class Logger {
7
+ static dummy: Logger;
8
+ private format;
9
+ private domain;
10
+ private ctx;
11
+ private write;
12
+ private opts;
13
+ constructor(domain: string, ctx?: Obj<any>, opts?: typeof this.opts, write?: typeof this.write);
14
+ getDomain(): string;
15
+ getTraceId(term: string): string;
16
+ log(data: LoggerData): void;
17
+ kid(domain: string): Logger;
18
+ scope<Fn extends (logger: Logger) => any>(domain: string, ctx: Obj<any>, fn: Fn): ReturnType<Fn>;
19
+ }
20
+ export {};
@@ -0,0 +1,147 @@
1
+ // TODO: @gershy/logger (or @gershy/watcher?)
2
+ import { getClsName, inCls, isCls } from '@gershy/clearing';
3
+ const scrubbed = (val, opts = {}) => {
4
+ const { isScrub = (k, _v) => k[0] === '!', doScrub = ((v) => `$scrub(${getClsName(v)})`), seen = new Map() } = opts ?? {};
5
+ if (val === null)
6
+ return val;
7
+ if (isCls(val, Boolean))
8
+ return val;
9
+ if (isCls(val, Number))
10
+ return val;
11
+ if (isCls(val, String))
12
+ return val;
13
+ if (seen.has(val))
14
+ return seen.get(val) ?? null;
15
+ if (isCls(val, Array)) {
16
+ const result = [];
17
+ seen.set(val, result);
18
+ for (const item of val)
19
+ result.push(scrubbed(item, { isScrub, doScrub, seen }));
20
+ return result;
21
+ }
22
+ else if (isCls(val, Object)) {
23
+ // Consider using hashes to display scrubbed values? (Makes them correlatable!)
24
+ const result = {};
25
+ seen.set(val, result);
26
+ for (const [k, v] of val)
27
+ result[k] = isScrub(k, v) ? doScrub(v) : scrubbed(v, { isScrub, doScrub, seen });
28
+ return result;
29
+ }
30
+ else if (inCls(val[limn], Function)) {
31
+ return scrubbed(val[limn](), { isScrub, doScrub, seen });
32
+ }
33
+ else {
34
+ // Consider processing for other types (sets, maps?)
35
+ return val;
36
+ }
37
+ };
38
+ export default class Logger {
39
+ static dummy = {
40
+ getTraceId() { return ''; },
41
+ log() { },
42
+ kid() { return Logger.dummy; },
43
+ scope(...args) { return args.at(-1)(Logger.dummy); }
44
+ };
45
+ format(v /* should not have cycles */, seen = new Map()) {
46
+ // Formats any value into json (so that it can be logged)
47
+ if (v == null)
48
+ return null;
49
+ if (isCls(v, Boolean))
50
+ return v;
51
+ if (isCls(v, Number))
52
+ return v;
53
+ if (isCls(v, String))
54
+ return v.length > this.opts.maxStrLen ? v.slice(0, this.opts.maxStrLen - 1) + '\u2026' : v;
55
+ if (seen.has(v))
56
+ return `<cyc> ${getClsName(v)}(...)`;
57
+ if (inCls(v[limn], Function)) {
58
+ const formatted = {};
59
+ seen.set(v, formatted);
60
+ Object.assign(formatted, this.format(v[limn](), seen));
61
+ return formatted;
62
+ }
63
+ if (isCls(v, Array)) {
64
+ const arr = [];
65
+ seen.set(v, arr);
66
+ for (const vv of v)
67
+ arr.push(this.format(vv, seen));
68
+ return arr;
69
+ }
70
+ if (isCls(v, Object)) {
71
+ const obj = {};
72
+ seen.set(v, obj);
73
+ for (const k in v)
74
+ obj[k] = this.format(v[k], seen);
75
+ return obj;
76
+ }
77
+ return `${getClsName(v)}(...)`;
78
+ }
79
+ ;
80
+ domain;
81
+ ctx;
82
+ write;
83
+ opts;
84
+ constructor(domain, ctx = {}, opts, write) {
85
+ this.domain = domain;
86
+ this.ctx = {
87
+ ...ctx,
88
+ $: this.domain,
89
+ [this.domain.split('.').at(-1)]: Math.random().toString(36).slice(2, 12)[padTail](10, '0'),
90
+ };
91
+ this.opts = opts ?? { maxStrLen: 250 };
92
+ // Note this default `this.write` function produces truncated values (sloppy outputting) in the
93
+ // cli, but works perfectly for lambdas with json-style logging configured!
94
+ this.write = write ?? ((val) => console.log(val));
95
+ }
96
+ getDomain() { return this.domain; }
97
+ getTraceId(term) {
98
+ const traceId = this.ctx[term];
99
+ if (!traceId)
100
+ throw Error('trace term invalid')[mod]({ term, ctx: this.ctx });
101
+ return traceId;
102
+ }
103
+ log(data) {
104
+ // Use-cases:
105
+ // 1. logger.log('a basic string')
106
+ // - The logged payload is converted to `{ msg: 'a basic string' }`
107
+ // 2. logger.log({ msg: 'a basic string' })
108
+ // - Logged as-is; equivalent to #1
109
+ // 3. logger.log({ $$: 'note', msg: 'a basic string' })
110
+ // - Same as #1 and #2, but logger domain has "note" appended to it
111
+ // 4. logger.log({ $$: 'noteOnly' })
112
+ // - Logged payload will be `{}`; only info is the tag - useful for infinitesimal moments in
113
+ // the flamegraph!
114
+ // 5. logger.log({ $$: 'context.noteOnly' })
115
+ // - Valid way to extend domain with more than 1 component at once!
116
+ let dmn = null;
117
+ if (data && isCls(data?.$$, String))
118
+ ({ $$: dmn, ...data } = data);
119
+ const domain = dmn ? [this.domain, dmn].join('.') : this.domain;
120
+ if (!isCls(data, Object))
121
+ data = { msg: data };
122
+ this.write({}[merge]({ $: this.ctx })[merge](scrubbed(this.format(data)))[merge]({ $: { $: domain } }));
123
+ }
124
+ kid(domain) {
125
+ const d = [this.domain, domain].filter(Boolean).join('.');
126
+ const logger = new Logger(d, this.ctx, this.opts, this.write);
127
+ return logger;
128
+ }
129
+ scope(domain, ctx, fn) {
130
+ const ms = Date.now();
131
+ const logger = this.kid(domain);
132
+ logger.log({ $$: 'launch', ...ctx });
133
+ const accept = val => { logger.log({ $$: 'accept', ms: Date.now() - ms }); return val; };
134
+ const glitch = err => { logger.log({ $$: err.log?.term ?? 'glitch', ms: Date.now() - ms, err }); throw err; }; // Always throws an error! Note allowing the consumer to override the logged term/domain offers a lot of flexibility!
135
+ let v;
136
+ try {
137
+ v = fn(logger);
138
+ }
139
+ catch (err) {
140
+ glitch(err);
141
+ }
142
+ return isCls(v, Promise)
143
+ ? v.then(accept, glitch)
144
+ : accept(v);
145
+ }
146
+ }
147
+ ;
@@ -0,0 +1,2 @@
1
+ declare const normalize: (val: any, seen?: Set<any>) => any;
2
+ export default normalize;
@@ -0,0 +1,23 @@
1
+ import { isCls, getClsName } from '@gershy/clearing';
2
+ const normalize = (val, seen = new Set()) => {
3
+ // Derives a json-stringifiable value from *any* value
4
+ // E.g. to hash *anything*: hash(JSON.stringify(normalized(anything)));
5
+ // Handles terminations
6
+ if (isCls(val, String))
7
+ return val;
8
+ if (isCls(val, Number))
9
+ return val;
10
+ if (val === null)
11
+ return null;
12
+ if (val === undefined)
13
+ return null;
14
+ if (seen.has(val))
15
+ return '<<!circ!>>';
16
+ seen.add(val);
17
+ if (isCls(val, Array))
18
+ return val[map](v => normalize(v, seen));
19
+ if (isCls(val, Object))
20
+ return normalize(Object.entries(val).sort((e0, e1) => e0[0] < e1[0] ? -1 : 1), seen);
21
+ return normalize({ $form: getClsName(val), ...val }, seen);
22
+ };
23
+ export default normalize;
@@ -0,0 +1,8 @@
1
+ import { rootFact } from '@gershy/disk';
2
+ import { ProcOpts } from '@gershy/util-nodejs-proc';
3
+ type Fact = typeof rootFact;
4
+ declare const _default: (fact: Fact, cmd: string, opts?: ProcOpts) => Promise<{
5
+ logDb: import("@gershy/disk").Fact;
6
+ output: string;
7
+ }>;
8
+ export default _default;
@@ -0,0 +1,26 @@
1
+ import proc from '@gershy/util-nodejs-proc';
2
+ export default (fact, cmd, opts) => {
3
+ const numTailingTfLogLines = 20;
4
+ const writeLog = async (result) => {
5
+ const [yr, mo, dy, hr, mn, sc, ms] = new Date().toISOString().match(/([0-9]{4})[-]([0-9]{2})[-]([0-9]{2})[T]([0-9]{2})[:]([0-9]{2})[:]([0-9]{2})[.]([0-9]+)[Z]/).slice(1);
6
+ const term = `${cmd.split(' ')[1]}-${yr}${mo}${dy}-${hr}${mn}${sc}`;
7
+ const logDb = fact.kid(['.terraform.log', `${term}.txt`]);
8
+ await logDb.setData(result);
9
+ return logDb;
10
+ };
11
+ return proc(cmd, {
12
+ timeoutMs: 0,
13
+ ...opts,
14
+ cwd: fact,
15
+ env: { TF_DATA_DIR: '' }
16
+ }).then(async (result) => {
17
+ const logDb = await writeLog(result.output);
18
+ return { logDb, output: result.output.split('\n').slice(-numTailingTfLogLines).join('\n') };
19
+ }, async (err) => {
20
+ const logDb = await writeLog(err.output ?? err[limn]());
21
+ throw Error(`terraform failed (${err.message})`)[mod]({
22
+ logDb,
23
+ ...(err.output ? { output: err.output.split('\n').slice(-numTailingTfLogLines).join('\n') } : { cause: err })
24
+ });
25
+ });
26
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: (str: string, escapeChars: string) => string;
2
+ export default _default;
@@ -0,0 +1,8 @@
1
+ export default (str, escapeChars) => {
2
+ if (!escapeChars[has]('\\'))
3
+ escapeChars += '\\';
4
+ // The regex to construct must have "\" and "]" escaped:
5
+ const escChars = escapeChars.replace(/([\\\]])/g, '\\$1');
6
+ const reg = new RegExp(`([${escChars}])`, 'g');
7
+ return str.replace(reg, '\\$1');
8
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: (val: string) => Lowercase<string>;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ export default (val) => val.replace(/([A-Z])/g, '_$1')[lower]();
@@ -0,0 +1,2 @@
1
+ export declare const embed: (v: string) => string;
2
+ export declare const json: (v: Json) => string;
@@ -0,0 +1,6 @@
1
+ export const embed = (v) => '${' + v + '}';
2
+ export const json = (v) => [
3
+ '| <<EOF',
4
+ JSON.stringify(v, null, 2)[indent](2),
5
+ 'EOF'
6
+ ].join('\n');
@@ -0,0 +1,7 @@
1
+ export type TryWithHealingArgs<T> = {
2
+ fn: () => Promise<T>;
3
+ canHeal: (err: any) => boolean;
4
+ heal: () => Promise<any>;
5
+ };
6
+ declare const _default: <T>(args: TryWithHealingArgs<T>) => Promise<T>;
7
+ export default _default;
@@ -0,0 +1,9 @@
1
+ export default async (args) => {
2
+ const { fn, heal, canHeal } = args;
3
+ return fn().catch(async (err) => {
4
+ if (!canHeal(err))
5
+ throw err;
6
+ await heal();
7
+ return fn();
8
+ });
9
+ };
@@ -0,0 +1,2 @@
1
+ declare global {}
2
+ export {};
package/license ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Gershom Maes
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@gershy/lilac",
3
+ "version": "0.0.1",
4
+ "description": "Luscious infrastructure Living as Code - an opinionated approach to IAC",
5
+ "keywords": [
6
+ "lilac",
7
+ "luscious",
8
+ "iac",
9
+ "infrastructure"
10
+ ],
11
+ "author": "Gershom Maes",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/gershy/lilac.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/gershy/lilac/issues"
18
+ },
19
+ "homepage": "https://github.com/gershy/lilac#readme",
20
+ "license": "ISC",
21
+ "peerDependencies": {
22
+ "@gershy/clearing": "^0.0.24"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^24.10.1",
26
+ "tsx": "^4.21.0",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "type": "module",
30
+ "files": [
31
+ "cmp"
32
+ ],
33
+ "sideEffects": false,
34
+ "types": "./cmp/mjs/main.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "import": "./cmp/mjs/main.js",
38
+ "require": "./cmp/cjs/main.js"
39
+ }
40
+ },
41
+ "scripts": {
42
+ "test": "npm run ts.check && npx tsx ./src/main.test.ts",
43
+ "ts.check": "npx tsc --noEmit",
44
+ "build.cjs": "tsc -p build/tsconfig.cjs.json",
45
+ "build.mjs": "tsc -p build/tsconfig.mjs.json",
46
+ "build": "node ./build/act.js removeCmp && npm run build.cjs && npm run build.mjs && node ./build/act.js finalizeExportVariants",
47
+ "git.pub": "npm run test && git add --all && git commit -m \"automated\" && git push",
48
+ "npm.login": "npm login",
49
+ "npm.pub": "npm run test && npm run build && npm publish --access public"
50
+ },
51
+ "dependencies": {
52
+ "@gershy/disk": "^0.0.8",
53
+ "@gershy/util-nodejs-proc": "^0.0.1"
54
+ }
55
+ }
package/readme.md ADDED
@@ -0,0 +1,39 @@
1
+ # Lilac
2
+
3
+ Lilac is "Luscious Infrastructure Living As Code". It's an opinionated take on typical infrastructure-as-code.
4
+
5
+ Understanding Lilac involves multiple concepts:
6
+ 1. Flowers (logical services)
7
+ - Petals (individually provisioned infrastructure/microservices)
8
+ - Stems (iac providers, e.g. terraform)
9
+ 2. Pollen (inter-flower interfacing)
10
+ 3. Seeds (registry of all available Flowers)
11
+
12
+ ## 1. Flowers
13
+
14
+ Flowers are logical infrastructural service. Some examples of Flowers are:
15
+ 1. An api gateway
16
+ 2. A compute cluster
17
+ 3. A lambda function
18
+ 4. A document-style database
19
+ 5. A relational-style database
20
+ 5. A blob storage database
21
+ 6. A queue
22
+
23
+ The purpose of an infrastructural service is to provide some sort of systems behaviour. In implementing systems, we often have to think about additional, non-behavioural systems concerns - for example, access controls. A Lilac Resource represents only the distilled systems behaviour - when working with Lilac Resources, non-behavioural concerns are abstracted away.
24
+
25
+ ## 2. Lilac Comms (TODO: Rename "comms" -> "bees" / "pollinators" / "pollen"?)
26
+
27
+ A Lilac Comm represents a dependency between some Lilac Resources. For example, a lambda function may query/insert documents into a document database. A Comm could be used to represent the lambda function's link to the document database.
28
+
29
+ ## 3. IAC Entities
30
+
31
+ Lilac provides an opinion on how to represent systems, but it is unopinionated about how that representation is consumed and physically provisioned in the world. In provisioning a Lilac setup, the setup as a whole is converted into a number of IAC Entities - these IAC Entities are aware of a particular provisioning strategy, e.g. aws cloudformation or terraform. A set of IAC Entities is essentially the output of a Lilac setup; they allow a Lilac representation to become a real, functioning system.
32
+
33
+ ## 4. Lilac Registry
34
+
35
+ The Lilac Registry exhaustively represents the set of Lilac Resource types available. For example, a particular systems primitive (e.g. Temporal) may not have a corresponding Lilac Resource. In such a case, if Temporal is desired, a new Lilac Resource would have to be written for it, and added to the Registry. The Registry also enables development-time testing - a fully mocked Registry can be substituted to the Lilac setup, and the result will be a true-to-production test environment (e.g. local testing).
36
+
37
+ ## 5. The Patio
38
+
39
+ IAC deploys always involve the creation of infrastructure-describing code, which is abstracted away by Lilac and treated as ephemeral. But sometimes, deploys also involve the creation of files which are expected to be checked into version control. The "patio" is a file pointer that ought to be provided by the consumer, and points to some arbitrary directory in their control, which is checked into version control. Lilac will populate this directory, and pull from it appropriately (in order to reproduce deploys where ephemeral code is generated, and version-controlled code is still version-controlled). Note a good example of a version-controlled IAC file is terraform's .terraform.lock.hcl file!