Skip to content

Commit

Permalink
Implement the init command (#3)
Browse files Browse the repository at this point in the history
## Description

Implements the init command. When a student runs the init command it will do the following:

- Ask for the batch ID and the API secret (since the CLI package will be public, it's still better if the secret is not hardcoded)
- Check that the `Workspace` directory exists in the HOME of the machine
- Clone the exercises directory as a cache in the CLI config root (`~/.config/sparta/exercises`)
- Initialize the student's exercises repo in her `Workspace`
- Display instructions for GitHub and open the new repository creation page in the browser

## Related Issue

[Cu-7rp5br]

## Motivation and Context

It will help students initialize their workspace for an easier start

## How Has This Been Tested?

Manually for now. We're a little time bound. Tests will be added later once we're sure the CLI is ready for the upcoming session.

## Screenshots

[![asciicast](https://asciinema.org/a/TIAby4UJNfzk69P4XHc03JhZo.svg)](https://asciinema.org/a/TIAby4UJNfzk69P4XHc03JhZo)

## Types of changes

- ~Chore (non-breaking change which refactors / improves the existing code base)~
- ~Bug fix (non-breaking change which fixes an issue)~
- New feature (non-breaking change which adds functionality)
- ~Breaking change (fix or feature that would cause existing functionality to
  change)~

## Checklist:

- ✅ My code follows the code style of this project.
- ✅ My change requires a change to the documentation.
- ✅ I have updated the documentation accordingly.
- ✅ I have read the [**CONTRIBUTING**][CONTRIBUTING_FILE] document.
- 🔴 I have added tests to cover my changes.
- 🔴 All new and existing tests passed.

[CONTRIBUTING_FILE]: https://github.com/fewlinesco/guidelines/blob/master/CONTRIBUTING.adoc
  • Loading branch information
Yann IRBAH authored Sep 2, 2020
1 parent bea6e16 commit 469fe2b
Show file tree
Hide file tree
Showing 16 changed files with 428 additions and 58 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
*-debug.log
*-error.log
/.nyc_output
Expand Down
55 changes: 49 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,58 @@
sparta
======

Sparta CLI
======

## Installation
# Installation

```
$ asdf install
$ yarn install
```

## Usage
# Usage
<!-- usage -->
```sh-session
$ npm install -g sparta
$ sparta COMMAND
running command...
$ sparta (-v|--version|version)
sparta/1.0.0 darwin-x64 node-v14.6.0
$ sparta --help [COMMAND]
USAGE
$ sparta COMMAND
...
```
<!-- usagestop -->
# Commands
<!-- commands -->
* [`sparta help [COMMAND]`](#sparta-help-command)
* [`sparta init`](#sparta-init)

## `sparta help [COMMAND]`

display help for sparta

```
USAGE
$ sparta help [COMMAND]
ARGUMENTS
COMMAND command to show help for
## Commands
OPTIONS
--all see all commands in CLI
```

_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_

## `sparta init`

Initializes the Sparta workspace

```
USAGE
$ sparta init
EXAMPLE
$ sparta init
```
<!-- commandsstop -->
3 changes: 0 additions & 3 deletions bin/run.cmd

This file was deleted.

16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/plugin-help": "^3",
"fs-extra": "^9.0.1",
"marked": "^1.1.1",
"marked-terminal": "^4.1.0",
"node-emoji": "^1.10.0",
"simple-git": "^2.20.1",
"tslib": "^1"
},
"devDependencies": {
"@fewlines/eslint-config": "^3.0.0",
"@oclif/dev-cli": "^1",
"@types/fs-extra": "9.0.1",
"@types/jest": "^26.0.10",
"@types/marked": "1.1.0",
"@types/marked-terminal": "3.1.1",
"@types/node": "^14",
"@types/node-emoji": "^1.8.1",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"eslint": "^7.5.0",
Expand Down Expand Up @@ -61,9 +71,9 @@
"oclif"
],
"license": "MIT",
"main": "lib/index.js",
"main": "dist/index.js",
"oclif": {
"commands": "./lib/commands",
"commands": "./dist/commands",
"bin": "sparta",
"plugins": [
"@oclif/plugin-help"
Expand All @@ -73,7 +83,7 @@
"scripts": {
"postpack": "rm -f oclif.manifest.json",
"posttest": "eslint . --ext .ts",
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
"prepack": "rm -rf dist && tsc -b && oclif-dev manifest && oclif-dev readme",
"test": "echo NO TESTS",
"version": "oclif-dev readme && git add README.md"
},
Expand Down
9 changes: 9 additions & 0 deletions src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Command, { flags } from "@oclif/command";

import { SpartaError } from "./services/errors/sparta-error";

export default abstract class extends Command {
async catch(error: SpartaError): Promise<void> {
this.error(error.message, { suggestions: error.suggestions });
}
}
31 changes: 0 additions & 31 deletions src/commands/hello.ts

This file was deleted.

58 changes: 58 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import cli from "cli-ux";
import * as emoji from "node-emoji";

import Command from "../base";
import { loadConfig, ConfigInput, writeConfig } from "../config/config";
import initInstuctions from "../instructions/init";
import checkWorkspace from "../services/check-workspace";
import initExercicesRepository from "../services/init-exercises-repository";
import renderInstructions from "../services/render-instructions";
import updateExercisesRepoCache from "../services/update-exercises-repo-cache";

export default class Init extends Command {
static description = "Initializes the Sparta workspace";

static examples = ["$ sparta init"];

async run(): Promise<void> {
const configDir = this.config.configDir;
const userInput = await getUserInput();

writeConfig(configDir, userInput);

const config = loadConfig(configDir);

this.log(emoji.emojify(":crossed_fingers: Checking Workspace directory"));
checkWorkspace(config);

this.log(emoji.emojify(":robot_face: Initializing exercises repository"));
await initExercicesRepository(config);

cli.action.start(
emoji.emojify(":robot_face: Preparing the Sparta configuration"),
);
await updateExercisesRepoCache(configDir, { delete: true });
cli.action.stop();

this.log(emoji.emojify(":rocket: All Good! Follow the instructions now"));
this.log(renderInstructions(initInstuctions));

await cli.anykey(
"Press a key when you are ready to create your GitHub repository",
);

await cli.open("https://github.com/new");
}
}

async function getUserInput(): Promise<ConfigInput> {
const batchID = await cli.prompt("What is the ID of your batch ?");
const sharedSecret = await cli.prompt("Enter the Sparta secret token", {
type: "hide",
});

return {
batchID,
sharedSecret,
};
}
49 changes: 49 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as fs from "fs-extra";
import * as path from "path";

export interface Config {
workspaceDir: string;
exercicesDir: string;
batchID: string;
sharedSecret: string;
}

export interface ConfigInput {
batchID: string;
sharedSecret: string;
}

export function loadConfig(configDir: string): Config {
const configPath = path.join(configDir, "config.json");
fs.ensureFileSync(configPath);

const writtenConfig: ConfigInput = fs.readJSONSync(configPath);

const homeDir = process.env.HOME;

if (!homeDir) {
throw new Error("HOME env variable not set");
}

const workspaceDir = path.join(homeDir, "Workspace");
const exercicesDir = path.join(
workspaceDir,
"fewlines-education",
"exercices",
);

return {
...writtenConfig,
workspaceDir,
exercicesDir,
};
}

export function writeConfig(configDir: string, input: ConfigInput): void {
const configPath = path.join(configDir, "config.json");

fs.ensureFileSync(configPath);
fs.writeJsonSync(configPath, input, {
spaces: 2,
});
}
25 changes: 25 additions & 0 deletions src/instructions/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default `
---
# Initialize your GitHub repository
## Create the repository on GitHub
When pressing a key, the GitHub repository creation will open.
Create a \`public\` repository and fill the requested information.
Check the \`Add .gitignore\` checkbox
## Bind the repository to your local directory
Go to your exercises directory and add your GitHub repo as a remote:
\`\`\`bash
$ git remote add origin git@github.com:<your-github-username>/<your-repository-name>.git
$ git pull origin master
\`\`\`
Congratulations! Your exercises directory is now ready to be used.
---
`;
20 changes: 20 additions & 0 deletions src/services/check-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as fs from "fs-extra";

import { Config } from "../config/config";
import { SpartaError } from "./errors/sparta-error";

export class WorkspaceMissingError extends SpartaError {
constructor(directory: string) {
const name = "WorkspaceMissingError";
const message = `Workspace not found.`;
const suggestions = [`Make sure the "${directory}" directory exists.`];

super(name, message, suggestions);
}
}

export default function checkWorkspace(config: Config): void {
if (!fs.existsSync(config.workspaceDir)) {
throw new WorkspaceMissingError(config.workspaceDir);
}
}
12 changes: 12 additions & 0 deletions src/services/errors/sparta-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class SpartaError extends Error {
name: string;

suggestions: string[];

constructor(name: string, message: string, suggestions: string[]) {
super(message);

this.name = name;
this.suggestions = suggestions;
}
}
39 changes: 39 additions & 0 deletions src/services/init-exercises-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as fs from "fs-extra";
import simpleGit, { SimpleGit, SimpleGitOptions } from "simple-git";

import { Config } from "../config/config";
import { SpartaError } from "./errors/sparta-error";

export class ExercisesDirectoryExistsError extends SpartaError {
constructor(directory: string) {
const name = "ExercisesDirectoryExistsError";
const message = "Exercises directory already exists";
const suggestions = [
`Delete the ${directory} directory if you want to start over`,
`Rename the ${directory} directory (e.g. ${directory}-backup) if you want to keep your progress`,
];

super(name, message, suggestions);
}
}

export default async function initExercicesRepository(
config: Config,
): Promise<void> {
const directory = config.exercicesDir;

if (fs.existsSync(directory)) {
throw new ExercisesDirectoryExistsError(directory);
}

fs.ensureDirSync(directory);

const gitOptions: SimpleGitOptions = {
baseDir: directory,
binary: "git",
maxConcurrentProcesses: 6,
};

const git: SimpleGit = simpleGit(gitOptions);
await git.init();
}
10 changes: 10 additions & 0 deletions src/services/render-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as marked from "marked";
import * as TerminalRenderer from "marked-terminal";

export default function renderInstructions(markdown: string): string {
marked.setOptions({
renderer: new TerminalRenderer(),
});

return marked(markdown);
}
Loading

0 comments on commit 469fe2b

Please sign in to comment.