Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/node.tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Node Tests

on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main", "develop" ]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm run build
- run: npm run tests
63 changes: 49 additions & 14 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ tasks:
- src/**/*.ts
generates:
- build/**/*.js
build:clean:
desc: Remove build directory
cmds:
- rm -rf build
- git checkout -- build

dev-install:
desc: Local development setup
Expand All @@ -42,21 +47,37 @@ tasks:
- task build
- sudo npm install -g

publish:
desc: Create a new release, task publish -- [patch|minor|major]
silent: true
is-branch-safe:
desc: Check if the current branch is main
vars:
STEP: '{{default "patch" .CLI_ARGS}}'
BRANCH:
sh: git rev-parse --abbrev-ref HEAD
cmds:
- |
STEP="{{.STEP}}"

# Only allowed on main branch
if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then
echo "Not on main branch"
exit 0
if [ "{{.BRANCH}}" != "main" ] && [ "{{.BRANCH}}" != "develop" ]; then
echo "Not on main or develop branch"
exit 1
fi


release-*:
desc: Create a new release, task release-[patch|minor|major]
silent: true
preconditions:
- sh: task is-branch-safe
msg: "Not on main or develop branch"
vars:
STEP: '{{index .MATCH 0}}'
BRANCH:
sh: git rev-parse --abbrev-ref HEAD
PRERELEASE:
sh: |
if [ "{{.BRANCH}}" = "develop" ]; then
echo "--prerelease"
fi
cmds:
- |
task build:clean
task tests

VERSION=$(gh release list --json tagName | jq -r '.[] | .tagName' | sort -V | tail -n 1)
Expand All @@ -65,14 +86,14 @@ tasks:
PATCH=$(echo $VERSION | cut -d. -f3)

echo "Current version: $VERSION"
if [ "$STEP" = "major" ]; then
if [ "{{.STEP}}" = "major" ]; then
MAJOR=$((MAJOR+1))
MINOR=0
PATCH=0
elif [ "$STEP" = "minor" ]; then
elif [ "{{.STEP}}" = "minor" ]; then
MINOR=$((MINOR+1))
PATCH=0
elif [ "$STEP" = "patch" ]; then
elif [ "{{.STEP}}" = "patch" ]; then
PATCH=$((PATCH+1))
else
echo "Invalid step: $STEP"
Expand All @@ -83,5 +104,19 @@ tasks:

npm version $VERSION
git push
gh release create "$VERSION" --generate-notes --target main
gh release create "$VERSION" --generate-notes --target {{.BRANCH}} {{.PRERELEASE}}
npm publish --access public
publish:
desc: publish a release to npm
preconditions:
- task is-branch-safe
cmds:
- exit 0
- npm publish --access public
release-*-pub:
desc: Create a new release and publish it to npm, release-[patch|minor|major]-pub
vars:
STEP: '{{index .MATCH 0}}'
cmds:
- task: release-{{.STEP}}
- task: publish
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mikegarde/dotenv-cli",
"version": "0.2.0",
"version": "0.3.0",
"description": "Read and update dotenv files from the cli",
"main": "build/app.js",
"types": "build/app.d.ts",
Expand Down
110 changes: 19 additions & 91 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#!/usr/bin/env node

import {program} from 'commander';
import formatValue from './formatValue.js';
import fs from 'node:fs';
import path from 'node:path';
import parseEnvFile from './envParser.js';
import RuleViolation from './ruleViolationError.js';
import * as url from 'node:url';
import {program} from 'commander';
import fs from 'node:fs';
import path from 'node:path';
import * as url from 'node:url';
import parseEnvFile from './envParser.js';
import handlers from "./services/handlers.js";

import log, {setLogDebug} from './log.js';
import escapeAndQuote from "./escapeAndQuote.js";
import readPipe from "./readPipe.js";
import RuleViolationError from './errors/RuleViolationError.js';

async function app() {
const installDir: string = path.dirname(url.fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -38,7 +37,7 @@ async function app() {
setLogDebug(options.debug);

const stdin: string | void = await readPipe().catch((err) => {
throw new RuleViolation(`Error reading from stdin: ${err}`);
throw new RuleViolationError(`Error reading from stdin: ${err}`);
});

const envFilePath: string = options.file || '.env';
Expand All @@ -56,7 +55,7 @@ async function app() {
let setValue: string = '';
if (stdin && set) {
// - cannot have both --set [value] and stdin
throw new RuleViolation('Cannot use --set and stdin together');
throw new RuleViolationError('Cannot use --set and stdin together');
} else if (stdin) {
setValue = stdin;
} else if (set) {
Expand All @@ -76,27 +75,27 @@ async function app() {
// Qualifying Rules
// - must have a .env file
if (!fs.existsSync(envFilePath)) {
throw new RuleViolation(`.env file not found: ${fullEnvPath}`);
throw new RuleViolationError(`.env file not found: ${fullEnvPath}`);
}
// - cannot have both --json and --set
if (json && setValue) {
throw new RuleViolation('Cannot use --json and --set together');
throw new RuleViolationError('Cannot use --json and --set together');
}
// - must have a key if using --set
if (setValue && !singleKey) {
throw new RuleViolation('Must specify a single key when using --set');
throw new RuleViolationError('Must specify a single key when using --set');
}
// - cannot have both --json and --multiline
if (json && multiline) {
throw new RuleViolation('Cannot use --json and --multiline together');
throw new RuleViolationError('Cannot use --json and --multiline together');
}
// - cannot use --delete with any other options
if (deleteKey && (setValue || json || multiline)) {
throw new RuleViolation('Cannot use --delete with any other options');
throw new RuleViolationError('Cannot use --delete with any other options');
}
// - must have a key if using --delete
if (deleteKey && !singleKey) {
throw new RuleViolation('Must specify a single key when using --delete');
throw new RuleViolationError('Must specify a single key when using --delete');
}

let envObject = parseEnvFile(envFilePath);
Expand All @@ -105,89 +104,18 @@ async function app() {
log.debug('Outputting entire .env file as JSON');
log.info(envObject.toJsonString());
} else if (deleteKey) {
const key: string = keys[0];

log.debug(`Deleting "${key}"`);

if (envObject[key]) {
const lineStart = envObject[key].lineStart;
const lineEnd = envObject[key].lineEnd;
log.debug(`Deleting lines ${lineStart}-${lineEnd}`);

// Read the file and split it into an array of lines
let lines: string[] = fs.readFileSync(envFilePath, 'utf8').split('\n');

// Remove the lines between lineStart and lineEnd
lines.splice(lineStart, lineEnd - lineStart + 1);

// Join the lines back together and write the result back to the file
fs.writeFileSync(envFilePath, lines.join('\n'));
} else {
log.debug(`Environment variable "${key}" not found`);
process.exitCode = 1;
}
handlers.deleteKey(envObject, envFilePath, keys[0]);
} else if (setValue) {
const key: string = keys[0];
const newValue: string = escapeAndQuote(setValue, quoteSet);
const newLines: string = `${key}=${newValue}`;

log.debug(`Updating "${key}"`);

// Do we want to update or append the .env file?
if (envObject[key]) {
log.debug('Updating existing key', envObject[key]);
const lineStart = envObject[key].lineStart;
const lineEnd = envObject[key].lineEnd;
log.debug(`Replacing lines ${lineStart}-${lineEnd}`);

// Split the new lines into an array
let newLinesArray: string[] = newLines.split('\n');

// Read the file and split it into an array of lines
let lines: string[] = fs.readFileSync(envFilePath, 'utf8').split('\n');

// Replace the lines between lineStart and lineEnd
lines.splice(lineStart, lineEnd - lineStart + 1, ...newLinesArray);

// Join the lines back together and write the result back to the file
fs.writeFileSync(envFilePath, lines.join('\n'));
} else {
log.debug(`Appending "${key}" to "${envFilePath}"`);

fs.writeFileSync(envFilePath, `${newLines}\n`, {flag: 'a'});
}
handlers.setValue(envObject, envFilePath, keys[0], setValue, quoteSet);
} else {
let result: string = '';

for (const key of keys) {
log.debug(`Getting "${key}"`);

let value = '';

if (!envObject[key]) {
log.debug(`Environment variable "${key}" not found`);
process.exitCode = 1;
} else {
value = formatValue(envObject[key].value, multiline);
}

value = json ? (value ? `"${value}"` : 'null') : value;
result += json ? `"${key}": ${value},` : `${value}\n`;
}

// Removes trailing newline or comma
result = result.slice(0, -1);
if (json) {
result = `{${result}}`;
}
log.info(result);
handlers.getValue(envObject, keys, json, multiline);
}
}

app().then(() => {
log.debug('done');
}).catch((error) => {
if (error instanceof RuleViolation) {
if (error instanceof RuleViolationError) {
log.error(error.message);
} else {
log.error('An unexpected error occurred:', error);
Expand Down
29 changes: 0 additions & 29 deletions src/envObject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* Used to create an EnvObject that can be used to store environment variables
*/
class EnvValue {
value: string;
lineStart: number;
Expand All @@ -13,31 +10,11 @@ class EnvValue {
}
}

/**
* Used to create an object that can be used to store environment variables
*/
class EnvObject {
[key: string]: EnvValue | any;

/**
* Constructor for the EnvObject class.
*/
constructor() {
return new Proxy(this, {
/**
* GET trap that is used to get the value of EnvObject
*/
// get(target, key: PropertyKey, receiver) {
// if (typeof key === 'string' && typeof target[key] === 'object' && target[key] !== null) {
// console.log('getting', key, target[key]);
// return Reflect.get(target[key], 'value', receiver);
// } else {
// return Reflect.get(target, key, receiver);
// }
// },
/**
* SET trap that is used to set the value of EnvObject
*/
set(target, key: PropertyKey, value, receiver) {
if (typeof key !== 'string') {
key = 'value';
Expand Down Expand Up @@ -73,9 +50,6 @@ class EnvObject {
});
}

/**
* Used to convert the EnvObject to an key/value object
*/
toObj(): { [key: string]: string } {
let obj: { [key: string]: string } = {};

Expand All @@ -90,9 +64,6 @@ class EnvObject {
return obj;
}

/**
* Used to convert the EnvObject to a JSON string
*/
toJsonString(): string {
return JSON.stringify(this.toObj());
}
Expand Down
Loading