Skip to content

Commit

Permalink
feat(cli): added enclosed cli (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh authored Aug 31, 2024
1 parent bbce3c5 commit 2c23a17
Show file tree
Hide file tree
Showing 21 changed files with 830 additions and 27 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ A live instance is available at [enclosed.cc](https://enclosed.cc).
- **Responsive Design**: Works on all devices, from desktops to mobile phones.
- **Open Source**: The source code is available under the Apache 2.0 License.
- **Self-Hostable**: Run your instance of Enclosed for private note sharing.
- **CLI**: A command-line interface for creating notes from the terminal.

## Self host

Expand Down Expand Up @@ -107,13 +108,35 @@ You can configure the application using environment variables. Here are the avai

This ensures that the note remains securely encrypted during transmission and storage, with decryption only possible by those with the correct link and (if applicable) password.

## CLI

The Enclosed CLI allows you to create notes from the terminal. You can install it globally using npm, yarn, or pnpm.

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

# with yarn
yarn global add @enclosed/cli

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


```bash



## Project Structure

This project is organized as a monorepo using `pnpm` workspaces. The structure is as follows:

- **[packages/app-client](./packages/app-client/)**: Frontend application built with SolidJS.
- **[packages/app-server](./packages/app-server/)**: Backend application using HonoJS.
- **[packages/deploy-cloudflare](./packages/deploy-cloudflare/)**: Cloudflare Pages build scripts and configuration.
- **[packages/lib](./packages/lib/)**: Core functionalities of Enclosed.
- **[packages/cli](./packages/cli/)**: Command-line interface for Enclosed.

## Contributing

Expand Down
61 changes: 61 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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:

### 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
```

## 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);
93 changes: 93 additions & 0 deletions packages/cli/src/config/config.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { defineCommand } from 'citty';
import picocolors from 'picocolors';
import { keys, map } from 'lodash-es';
import { configDefinition } from './config.constants';
import { deleteConfig, getConfig, resetConfig, setConfig } from './config.models';

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`,
},
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`,
},
args: {
key: {
description: `Configuration key (${keysList})`,
type: 'positional',

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

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

if (value) {
// eslint-disable-next-line no-console
console.log(value ?? '');
}
},
}),

delete: defineCommand({
meta: {
name: 'delete',
description: `Delete a configuration value`,
},
args: {
key: {
description: `Configuration key (${keysList})`,
type: 'positional',
},
},
run: async ({ args }) => {
const { key } = args;

deleteConfig({ key: String(key) });
},
}),

reset: defineCommand({
meta: {
name: 'reset',
description: `Reset the whole configuration`,
},
run: async () => {
resetConfig();
},
}),

},
});
8 changes: 8 additions & 0 deletions packages/cli/src/config/config.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from 'zod';

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

export { createConfigBindings, setConfig, getConfig, deleteConfig, resetConfig };

type ConfigKey = keyof typeof configDefinition | (string & {});
const config = new Conf<Record<ConfigKey, string>>({ projectName: 'enclosed' });

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 deleteConfig({ key }: { key: ConfigKey }) {
config.delete(key);
}

function resetConfig() {
config.clear();
}

function createConfigBindings({ key }: { key: ConfigKey }) {
return {
get: () => {
return getConfig({ key });
},
set: (value: string) => {
setConfig({ key, value });
},
};
}
Loading

0 comments on commit 2c23a17

Please sign in to comment.