Skip to content

Commit

Permalink
feat(cli): added enclosed cli
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh committed Aug 31, 2024
1 parent bbce3c5 commit 83084ea
Show file tree
Hide file tree
Showing 20 changed files with 773 additions and 28 deletions.
58 changes: 58 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Enclosed CLI

This package contains the CLI for [Enclosed](https://enclosed.cc), an open-source project that aims to provide a simple and secure way to share e2e encrypted notes.

## Getting Started

To install the CLI, run the following command:

```bash
# with npm
npm install -g @enclosed/cli

# with yarn
yarn global add @enclosed/cli

# with pnpm
pnpm add -g @enclosed/cli
```

You can also run the CLI using `npx`:

```bash
npx @enclosed/cli <command>
```

## Usage

```bash
enclosed <command> [options]
```

### Create a note

```bash
# Basic usage
enclosed create "Hello, World!"

# Using stdin
cat file.txt | enclosed create

# With full options
enclosed create --deleteAfterReading --password "password" --ttl 3600 "Hello, World!"
```

### Configure the enclosed instance to use

```bash
# By default, the CLI uses the public instance at enclosed.cc
enclosed config set instance-url https://enclosed.cc
```

## License

This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for more information.

## Credits and Acknowledgements

This project is crafted with ❤️ by [Corentin Thomasset](https://corentin.tech).
4 changes: 4 additions & 0 deletions packages/cli/bin/enclosed.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict'

import '../dist/cli.mjs'
11 changes: 11 additions & 0 deletions packages/cli/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
entries: [
'src/cli',
],
clean: true,
rollup: {
emitCJS: true,
},
});
21 changes: 21 additions & 0 deletions packages/cli/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import antfu from '@antfu/eslint-config';

export default antfu({
stylistic: {
semi: true,
},

rules: {
// To allow export on top of files
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'curly': ['error', 'all'],
'vitest/consistent-test-it': ['error', { fn: 'test' }],
'ts/consistent-type-definitions': ['error', 'type'],
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'unused-imports/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
});
55 changes: 55 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@enclosed/cli",
"type": "module",
"version": "0.0.2",
"packageManager": "[email protected]",
"description": "Enclosed cli to create secure notes.",
"author": "Corentin Thomasset <[email protected]> (https://corentin.tech)",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/CorentinTh/enclosed"
},
"main": "./dist/cli.cjs",
"module": "./dist/cli.mjs",
"bin": {
"enclosed": "./bin/enclosed.mjs"
},
"files": [
"dist"
],
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"dev": "tsx ./src/cli.ts",
"build": "unbuild",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "pnpm run test:unit",
"test:unit": "vitest run",
"test:unit:watch": "vitest watch",
"prepublishOnly": "pnpm run build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@enclosed/lib": "workspace:*",
"citty": "^0.1.6",
"conf": "^13.0.1",
"lodash-es": "^4.17.21",
"ora": "^8.1.0",
"picocolors": "^1.0.1",
"tsx": "^4.17.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@antfu/eslint-config": "^2.27.0",
"@types/lodash-es": "^4.17.12",
"@vitest/coverage-v8": "^2.0.5",
"dotenv": "^16.4.5",
"eslint": "^9.9.0",
"typescript": "^5.5.4",
"unbuild": "^2.0.0",
"vitest": "^2.0.5"
}
}
16 changes: 16 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineCommand, runMain } from 'citty';
import { createNoteCommand } from './create-note/create-note.command';
import { configCommand } from './config/config.command';

const main = defineCommand({
meta: {
name: 'enclosed',
description: 'Create and view private and secure notes',
},
subCommands: {
create: createNoteCommand,
config: configCommand,
},
});

runMain(main);
67 changes: 67 additions & 0 deletions packages/cli/src/config/config.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { defineCommand } from 'citty';
import picocolors from 'picocolors';
import { keys, map } from 'lodash-es';
import { ENDPOINT_URL_CONFIGURATION_KEY, configDefinition } from './config.constants';
import { getConfig, setConfig } from './config.models';

const configKeyDescriptions = map(configDefinition, ({ description }, key) => `${key} - ${description}`).join(', ');
const keysList = keys(configDefinition).join(', ');

export const configCommand = defineCommand({
meta: {
name: 'config',
description: 'Manage cli configuration',
},
subCommands: {
set: defineCommand({
meta: {
name: 'set',
description: `Set a configuration value, available keys:\n${configKeyDescriptions}`,
},
args: {
key: {
description: `Configuration key (${keysList})`,
type: 'positional',
},
value: {
description: 'Configuration value',
type: 'positional',
},
},
run: async ({ args }) => {
const { key, value } = args;

setConfig({
key: String(key),
value: String(value),
});
},
}),
get: defineCommand({
meta: {
name: 'get',
description: `Get a configuration value, available keys:\n${configKeyDescriptions}`,
},
args: {
key: {
description: `Configuration key (${keysList})`,
type: 'positional',

},
},
run: async ({ args }) => {
const { key } = args;

const value = getConfig({ key: String(key) });

if (!value) {
console.error(picocolors.red(`Configuration key not found: ${key}`));
return;
}

// eslint-disable-next-line no-console
console.log(value);
},
}),
},
});
10 changes: 10 additions & 0 deletions packages/cli/src/config/config.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod';

export const ENDPOINT_URL_CONFIGURATION_KEY = 'endpoint-url';

export const configDefinition = {
'instance-url': {
description: 'Instance URL',
schema: z.string().url(),
},
} as const;
67 changes: 67 additions & 0 deletions packages/cli/src/config/config.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Conf from 'conf';
import picocolors from 'picocolors';
import { type ENDPOINT_URL_CONFIGURATION_KEY, configDefinition } from './config.constants';

export { createConfigBindings, setConfig, getConfig };

const config = new Conf<{ [ENDPOINT_URL_CONFIGURATION_KEY]?: string }>({ projectName: 'enclosed' });

type ConfigKey = keyof typeof configDefinition | (string & {});

function setConfig({ key, value }: { key: ConfigKey ; value: string }) {
const definition = configDefinition[key as keyof typeof configDefinition];

if (!definition) {
console.error(picocolors.red(`Invalid configuration key: ${key}`));
return;
}

const { schema } = definition;

const parsedValue = schema.safeParse(value);

if (!parsedValue.success) {
const errorMessage = parsedValue.error.errors.map(({ message }) => message).join('\n');
console.error(picocolors.red(`Invalid value for ${key}: ${errorMessage}`));
return;
}

config.set(key, value);
}

function getConfig({ key }: { key: ConfigKey }) {
const definition = configDefinition[key as keyof typeof configDefinition];

if (!definition) {
throw new Error(`Invalid configuration key: ${key}`);
}

const value = config.get(key);

if (!value) {
return;
}

const { schema } = definition;

const parsedValue = schema.safeParse(value);

if (!parsedValue.success) {
const errorMessage = parsedValue.error.errors.map(({ message }) => message).join('\n');
console.error(picocolors.red(`Invalid value for ${key}: ${errorMessage}`));
return;
}

return parsedValue.data;
}

function createConfigBindings({ key }: { key: ConfigKey }) {
return {
get: () => {
return getConfig({ key });
},
set: (value: string) => {
setConfig({ key, value });
},
};
}
6 changes: 6 additions & 0 deletions packages/cli/src/config/config.usecases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createConfigBindings } from './config.models';

export const {
get: getInstanceUrl,
set: setInstanceUrl,
} = createConfigBindings({ key: 'instance-url' });
Loading

0 comments on commit 83084ea

Please sign in to comment.