@adriangalilea/utils 0.1.0 → 0.3.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/README.md CHANGED
@@ -162,8 +162,43 @@ expect(() => assert(false, 'boom')).toThrow(Panic)
162
162
  - **File Operations**: Read, write with automatic path resolution
163
163
  - **Directory Operations**: Create, list, walk directories
164
164
  - **KEV**: Redis-style environment variable management with monorepo support
165
+ - **XDG**: XDG Base Directory paths — reads env vars set by [xdg-dirs](https://github.com/adriangalilea/xdg-dirs), falls back to spec defaults
166
+ - **Unseen**: Persistent dedup filter — "what's new since last time?" for cron/monitoring workflows
165
167
  - **Project Discovery**: Find project/monorepo roots, detect JS/TS projects
166
168
 
169
+ ### XDG Base Directories
170
+
171
+ XDG paths that respect env vars from [xdg-dirs](https://github.com/adriangalilea/xdg-dirs) with spec-compliant fallbacks:
172
+
173
+ ```typescript
174
+ import { xdg, dir } from '@adriangalilea/utils'
175
+
176
+ xdg.state('notify') // ~/.local/state/notify
177
+ xdg.state('notify', 'watchers.json') // ~/.local/state/notify/watchers.json
178
+ xdg.config('myapp') // ~/.config/myapp
179
+ xdg.data('myapp') // ~/.local/share/myapp
180
+ xdg.cache('myapp') // ~/.cache/myapp
181
+ xdg.runtime('myapp') // $XDG_RUNTIME_DIR/myapp
182
+
183
+ // Ensure the directory exists before writing
184
+ dir.create(xdg.state('notify'))
185
+ ```
186
+
187
+ ### Unseen
188
+
189
+ Persistent dedup filter — makes any script idempotent. Run it once or a thousand times, you only process each item once. Any scheduling works: manual, cron, a loop, whatever.
190
+
191
+ ```typescript
192
+ import { unseen } from '@adriangalilea/utils'
193
+
194
+ // only notifies on NEW orders — safe to run on any schedule
195
+ const fresh = await unseen('orders', allOrders, o => o.id)
196
+ for (const o of fresh) await notify(o.summary)
197
+ // 1st run: 5 orders → notifies 5. 2nd run: same 5 → notifies 0. 3rd run: 7 → notifies 2.
198
+ ```
199
+
200
+ State persists at `~/.local/state/unseen/{namespace}.json`.
201
+
167
202
  ## Release
168
203
 
169
204
  Bump version in `package.json` (and `jsr.json`), push to `main`. CI handles everything:
package/dist/index.d.ts CHANGED
@@ -13,4 +13,6 @@ export * from './platform/dir.js';
13
13
  export * from './platform/path.js';
14
14
  export * from './platform/project.js';
15
15
  export * from './platform/kev.js';
16
+ export * from './platform/xdg.js';
17
+ export * from './platform/unseen.js';
16
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGvD,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAG7C,cAAc,gBAAgB,CAAA;AAI9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGvD,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAG7C,cAAc,gBAAgB,CAAA;AAI9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -17,4 +17,6 @@ export * from './platform/dir.js';
17
17
  export * from './platform/path.js';
18
18
  export * from './platform/project.js';
19
19
  export * from './platform/kev.js';
20
+ export * from './platform/xdg.js';
21
+ export * from './platform/unseen.js';
20
22
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8CAA8C;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,wCAAwC;AACxC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAE7C,yDAAyD;AACzD,cAAc,gBAAgB,CAAA;AAE9B,8BAA8B;AAC9B,mEAAmE;AACnE,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8CAA8C;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,wCAAwC;AACxC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAE7C,yDAAyD;AACzD,cAAc,gBAAgB,CAAA;AAE9B,8BAA8B;AAC9B,mEAAmE;AACnE,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Persistent dedup filter — "what's new since last time?"
3
+ *
4
+ * Makes any script idempotent. Run it once, run it a thousand times —
5
+ * you only process each item once. This means any scheduling works:
6
+ * manual `tsx check.ts`, a fish loop, a cron job, whatever.
7
+ * Can't double-notify, can't miss items, can't corrupt state.
8
+ *
9
+ * 1st run: 5 orders exist → returns 5
10
+ * 2nd run: same 5 orders → returns 0
11
+ * 3rd run: 7 orders exist → returns 2
12
+ *
13
+ * ```ts
14
+ * const fresh = await unseen('orders', allOrders, o => o.id)
15
+ * for (const o of fresh) await notify(o.summary)
16
+ * ```
17
+ *
18
+ * State persists at `~/.local/state/unseen/{namespace}.json`
19
+ *
20
+ * @param namespace - Seen-set name (e.g. 'messages', 'github-issues', 'orders')
21
+ * @param items - Items to filter
22
+ * @param key - Extract a unique string key from each item
23
+ * @returns Only items whose key hasn't been seen before
24
+ */
25
+ export declare function unseen<T>(namespace: string, items: T[], key: (item: T) => string): Promise<T[]>;
26
+ //# sourceMappingURL=unseen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unseen.d.ts","sourceRoot":"","sources":["../../src/platform/unseen.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAmBrG"}
@@ -0,0 +1,45 @@
1
+ import { xdg } from './xdg.js';
2
+ import { dir } from './dir.js';
3
+ import { file } from './file.js';
4
+ import { join } from 'node:path';
5
+ const STORE_DIR = xdg.state('unseen');
6
+ /**
7
+ * Persistent dedup filter — "what's new since last time?"
8
+ *
9
+ * Makes any script idempotent. Run it once, run it a thousand times —
10
+ * you only process each item once. This means any scheduling works:
11
+ * manual `tsx check.ts`, a fish loop, a cron job, whatever.
12
+ * Can't double-notify, can't miss items, can't corrupt state.
13
+ *
14
+ * 1st run: 5 orders exist → returns 5
15
+ * 2nd run: same 5 orders → returns 0
16
+ * 3rd run: 7 orders exist → returns 2
17
+ *
18
+ * ```ts
19
+ * const fresh = await unseen('orders', allOrders, o => o.id)
20
+ * for (const o of fresh) await notify(o.summary)
21
+ * ```
22
+ *
23
+ * State persists at `~/.local/state/unseen/{namespace}.json`
24
+ *
25
+ * @param namespace - Seen-set name (e.g. 'messages', 'github-issues', 'orders')
26
+ * @param items - Items to filter
27
+ * @param key - Extract a unique string key from each item
28
+ * @returns Only items whose key hasn't been seen before
29
+ */
30
+ export async function unseen(namespace, items, key) {
31
+ dir.create(STORE_DIR);
32
+ const storePath = join(STORE_DIR, `${namespace}.json`);
33
+ const seen = new Set(file.exists(storePath) ? JSON.parse(file.readText(storePath)) : []);
34
+ const result = [];
35
+ for (const item of items) {
36
+ const k = key(item);
37
+ if (!seen.has(k)) {
38
+ seen.add(k);
39
+ result.push(item);
40
+ }
41
+ }
42
+ file.write(storePath, JSON.stringify([...seen]));
43
+ return result;
44
+ }
45
+ //# sourceMappingURL=unseen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unseen.js","sourceRoot":"","sources":["../../src/platform/unseen.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAC9B,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;AAErC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAI,SAAiB,EAAE,KAAU,EAAE,GAAwB;IACrF,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,SAAS,OAAO,CAAC,CAAA;IAEtD,MAAM,IAAI,GAAgB,IAAI,GAAG,CAC/B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAa,CAAC,CAAC,CAAC,EAAE,CAC/E,CAAA;IAED,MAAM,MAAM,GAAQ,EAAE,CAAA;IACtB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;QACnB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACX,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IAChD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,8 @@
1
+ export declare const xdg: {
2
+ config: (...segments: string[]) => string;
3
+ data: (...segments: string[]) => string;
4
+ state: (...segments: string[]) => string;
5
+ cache: (...segments: string[]) => string;
6
+ runtime: (...segments: string[]) => string;
7
+ };
8
+ //# sourceMappingURL=xdg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xdg.d.ts","sourceRoot":"","sources":["../../src/platform/xdg.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,GAAG;0BACS,MAAM,EAAE;wBACR,MAAM,EAAE;yBACR,MAAM,EAAE;yBACR,MAAM,EAAE;2BACR,MAAM,EAAE;CAChC,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ const home = homedir();
4
+ const env = (key, fallback) => process.env[key] || fallback;
5
+ export const xdg = {
6
+ config: (...segments) => join(env('XDG_CONFIG_HOME', join(home, '.config')), ...segments),
7
+ data: (...segments) => join(env('XDG_DATA_HOME', join(home, '.local', 'share')), ...segments),
8
+ state: (...segments) => join(env('XDG_STATE_HOME', join(home, '.local', 'state')), ...segments),
9
+ cache: (...segments) => join(env('XDG_CACHE_HOME', join(home, '.cache')), ...segments),
10
+ runtime: (...segments) => join(env('XDG_RUNTIME_DIR', join(home, '.local', 'run')), ...segments),
11
+ };
12
+ //# sourceMappingURL=xdg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xdg.js","sourceRoot":"","sources":["../../src/platform/xdg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;AACtB,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAA;AAE3E,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,MAAM,EAAG,CAAC,GAAG,QAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC;IACpG,IAAI,EAAK,CAAC,GAAG,QAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC;IAC1G,KAAK,EAAI,CAAC,GAAG,QAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC;IAC3G,KAAK,EAAI,CAAC,GAAG,QAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC;IAClG,OAAO,EAAE,CAAC,GAAG,QAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC;CAC3G,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adriangalilea/utils",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript utilities - logger, currency, formatter, and more",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",