rudderstash 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.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # rudderstash
2
+
3
+ rudderstash is a management, testing and deployment utility for Rudderstack transformations and libraries.
4
+
5
+ The goal is to provide a robust Rudderstack transformation (aka transformer) and library development workflows that the Rudderstack web UI lacks: TypeScript support, version control and collaboration, unit testing, deployment.
6
+
7
+ ## Getting Started
8
+
9
+ ```bash
10
+ # From NPM
11
+ npm i -g rudderstash
12
+
13
+ # From sources
14
+ # git clone git@github.com:soulseekah/rudderstash.git
15
+ # make install
16
+
17
+ mkdir project && cd project
18
+ ```
19
+
20
+ A read-write Rudderstack API token is required for synchronization. This can either be [Personal Access Token](https://app.rudderstack.com/profile) or a [Service Access Token](https://app.rudderstack.com/organization?tab=service_access_tokens). Set the `RUDDERSTACK_API_TOKEN` in the .env file in the project root or supply it via environment variables. See .env.example, and remember to **never commit your .env file**.
21
+
22
+ The `RUDDERSTACK_API_USER` (matches your account email) and `RUDDERSTACK_API_ENDPOINT` should also be supplied.
23
+
24
+ ```bash
25
+ rudderstash status
26
+ ```
27
+
28
+ Check what's upstream and in the local directory before pulling in.
29
+
30
+ Now pull in your existing transformers:
31
+
32
+ ```bash
33
+ rudderstash pull
34
+ ```
35
+
36
+ Modify.
37
+
38
+ ```bash
39
+ rudderstash push
40
+ ```
41
+
42
+ ## Roadmap
43
+
44
+ - [x] Basic synchronization
45
+ - [ ] Handle renames
46
+ - [ ] TypeScript support
47
+ - [ ] Polish a lot
48
+ - [ ] Testing
49
+ - [ ] Support revisions
50
+ - [ ] Git integration
51
+
52
+ ## Support & License
53
+
54
+ **This project is currently UNLICENSED and under heavy development. Use at your own risk.**
55
+
56
+ The code is provided as-is without warranty of any kind. Data loss or corruption may occur. Always backup your transformations before using this tool.
57
+
58
+ For questions or support, contact me on Discord: [soulseekah](https://discord.com/users/360781302397534208)
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ "use strict";import{writeFile as h,readFile as g}from"fs/promises";import S from"dotenv";import N from"yargs";import{hideBin as _}from"yargs/helpers";import{existsSync as p}from"fs";import{writeFile as w,readdir as j,readFile as l}from"fs/promises";import{createHash as D}from"crypto";var b="0.1.0",A="[eeb82c8@trunk; built: 2025-12-20T12:01:28Z]",$=class{headers={};endpoint;constructor(r){this.endpoint=r.RUDDERSTACK_API_ENDPOINT,this.headers={"User-Agent":`rudderstash/${b}`,"Content-Type":"application/json",Authorization:"Basic "+Buffer.from(`${r.RUDDERSTACK_API_USER}:${r.RUDDERSTACK_API_TOKEN}`).toString("base64")}}async get(r){return(await fetch(this.endpoint+r,{headers:this.headers})).json()}async post(r,e){return(await fetch(this.endpoint+r,{method:"POST",headers:this.headers,body:JSON.stringify(e)})).json()}},v=class O{id;versionId;createdAt;updatedAt;name;description;code="";path;constructor(e={}){this.id=e.id,this.versionId=e.versionId,this.createdAt=e.createdAt,this.updatedAt=new Date(e.updatedAt),this.name=e.name}static async get(e){const i=await j(".");for(const n of i.filter(t=>t.endsWith(".transformation.js"))){const t=await l(n,"utf-8");if(!p(`${n}on`))continue;const a=JSON.parse(await l(`${n}on`,"utf-8"));if(a.id===e){const o=new O(a);return o.code=t,o}}return null}hash(){return D("sha1").update(JSON.stringify({id:this.id,code:this.code})).digest("hex")}},u=class y{transformations=[];api;constructor(e){this.api=new $(e)}static async read(e){const i=new y(e),n=await j(".");for(const t of n.filter(a=>a.endsWith(".transformation.js"))){if(!p(`${t}on`)){const s=new v;s.name=t.split(".transformation.js").at(0),s.code=await l(t,"utf-8"),s.path=t,i.transformations.push(s);continue}const a=JSON.parse(await l(`${t}on`,"utf-8")),o=await v.get(a.id);o&&(o.path=t,i.transformations.push(o))}return await i.fetch(),i}async fetch(){p(".ruddercache")||await w(".ruddercache","{}");const e=JSON.parse(await l(".ruddercache","utf8"));e.transformations=(await this.api.get("/transformations")).transformations,e.transformations_fetched_at=Date.now(),await w(".ruddercache",JSON.stringify(e))}async list(){p(".ruddercache")||await w(".ruddercache","{}");let e=JSON.parse(await l(".ruddercache","utf8"));e.transformations===void 0&&(this.fetch(),e=JSON.parse(await l(".ruddercache","utf8")));const i=e.transformations,n={},t={};for(const a of i){const o=a.id;n[o]=new v(a),n[o].code=a.code}for(const a of this.transformations){const o=a.id??crypto.randomUUID();a.id=o,t[o]||(t[o]=[]),t[o].push(a)}return{transformations_fetched_at:e.transformations_fetched_at,transformations:[n,t]}}},f={};S.config({quiet:!0,processEnv:f,override:!1}),Object.entries(f).forEach(([r,e])=>{r in process.env&&(f[r]=process.env[r])});var I=await N(_(process.argv)).parse(),[J,...F]=I._;switch(J){case"version":{console.info(`rudderstash ${b} ${A}`);break}case"status":{const e=await(await u.read(f)).list(),[i,n]=e.transformations;if(console.info(`Last fetched ${new Date(e.transformations_fetched_at)}
3
+ `),console.info("Upstream:"),!Object.keys(i).length)console.info(` (none)
4
+ `);else for(const t of Object.values(i)){let a=" ";const o=[];if(n[t.id]===void 0)a="+";else{const s=(n[t.id]??[]).reduce((d,m)=>d||m.hash()!==t.hash(),!1),c=(n[t.id]??[]).reduce((d,m)=>d||m.name!==t.name||m.description!==t.description,!1);s&&(a="~"),c&&(a="~",o.push("metadata changed")),!s&&!c&&o.push("no changes")}console.info(` ${a} ${t.hash().substring(0,7)} ${t.name}`+(o.length?` (${o.join(", ")})`:""))}if(console.info(`
5
+ Local:`),!Object.keys(n).length)console.info(` (none)
6
+ `);else for(const t of Object.values(n))for(const a of t){let o=" ",s=[];t.filter(d=>d.id===a.id).length>1&&(o="!",s.push("duplicate")),i[a.id]===void 0&&(o="+"),console.info(` ${o} ${a.hash().substring(0,7)} ${a.name} (${a.path})`+(s.length?` (${s.join(", ")})`:""))}break}case"pull":{const e=await(await u.read(f)).list(),[i,n]=e.transformations;if(Object.keys(i).length<1){console.info("No transformations upstream, nothing to pull.");break}for(const t of Object.values(i))if(n[t.id])for(const a of Object.values(n[t.id])){const o=a.path;console.info(`< Pulling new transformation ${t.name} to ${o}...`),await h(o,t.code),console.info(`< Saving metadata to ${o}on.`),delete t.code,await h(o+"on",JSON.stringify(t,null,2))}else{const a=t.name.split(" ").map(o=>o[0].toUpperCase()+o.substring(1)).join("")+".transformation.js";console.info(`< Pulling new transformation ${t.name} to ${a}...`),await h(a,t.code),console.info(`< Saving metadata to ${a}on.`),delete t.code,await h(a+"on",JSON.stringify(t,null,2))}break}case"push":{const e=await(await u.read(f)).list(),[i,n]=e.transformations;if(Object.values(n).length<1){console.info("No local transformations, nothing to push.");break}const t=new $(f);for(const a of Object.values(n)){if(Object.values(a).length>1){console.warn("Conflict! The following local transformations have the same ID: "+Object.values(a).map(d=>d.path).join(`
7
+ `)+". Resolve by leaving only one.");continue}const o=await g(a[0].path,"utf-8");let s={};a[0].versionId===void 0?(s.id="",s.name=a[0].path.split(".transformation.js").at(0),s.description=a[0].path):s=JSON.parse(await g(`${a[0].path}on`,"utf-8")),console.info(`> Pushing ${o.length} bytes of code to ${s.name} transformation...`);const c=await t.post(`/transformations/${s.id}?publish=true`,{code:o,name:s.name,language:"javascript",description:s.description});if(c.error!==void 0){console.error(c.error);continue}delete c.code,await h(`${a[0].path}on`,JSON.stringify({...s,...c},null,2)),console.info(`> Published with version ${c.versionId}.`),console.info("< Metadata updated.")}break}case"diff":{const e=await(await u.read(f)).list()}case"revert":break;default:console.error("Invalid command")}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "rudderstash",
3
+ "version": "0.1.0",
4
+ "description": "Rudderstack transformation version control, deployment and testing.",
5
+ "keywords": [
6
+ "rudderstack"
7
+ ],
8
+ "homepage": "https://github.com/soulseekah/rudderstash#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/soulseekah/rudderstash/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/soulseekah/rudderstash.git"
15
+ },
16
+ "license": "UNLICENSED",
17
+ "author": "Gennady Kovshenin",
18
+ "type": "module",
19
+ "bin": {
20
+ "rudderstash": "./dist/rudderstash.js"
21
+ },
22
+ "scripts": {
23
+ "build": "esbuild rudderstash.ts --bundle --platform=node --format=esm --outfile=dist/rudderstash.js --banner:js='#!/usr/bin/env node' --packages=external",
24
+ "build:minify": "esbuild rudderstash.ts --bundle --platform=node --format=esm --outfile=dist/rudderstash.js --banner:js='#!/usr/bin/env node' --packages=external --minify",
25
+ "dev": "tsx rudderstash.ts",
26
+ "debug": "tsx --inspect-brk rudderstash.ts",
27
+ "test": "echo \"Error: no test specified\" && exit 1"
28
+ },
29
+ "files": [
30
+ "dist/rudderstash.js"
31
+ ],
32
+ "devDependencies": {
33
+ "@types/node": "^25.0.3",
34
+ "@types/yargs": "^17.0.35",
35
+ "esbuild": "^0.27.2",
36
+ "tsx": "^4.21.0",
37
+ "typescript": "^5.9.3"
38
+ },
39
+ "dependencies": {
40
+ "diff": "^8.0.2",
41
+ "dotenv": "^17.2.3",
42
+ "yargs": "^18.0.0"
43
+ }
44
+ }