diff --git a/packages/cli/.DS_Store b/packages/cli/.DS_Store
new file mode 100644
index 00000000..5d196f03
Binary files /dev/null and b/packages/cli/.DS_Store differ
diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore
new file mode 100644
index 00000000..b0dcebde
--- /dev/null
+++ b/packages/cli/.gitignore
@@ -0,0 +1,80 @@
+# Created by .ignore support plugin (hsz.mobi)
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+
+# Sensitive or high-churn files:
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/dataSources.local.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+### Dart template
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.buildlog
+.packages
+.project
+.pub/
+build/
+**/packages/
+
+# Files created by dart2js
+# (Most Dart developers will use pub build to compile Dart, use/modify these
+# rules if you intend to use dart2js directly
+# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
+# differentiate from explicit Javascript files)
+*.dart.js
+*.part.js
+*.js.deps
+*.js.map
+*.info.json
+
+# Directory created by dartdoc
+doc/api/
+
+# Don't commit pubspec lock file
+# (Library packages only! Remove pattern if developing an application package)
+pubspec.lock
+/sample_project/lib/src/services/
+/sample_project/test/services/
+/sample_project/
+sample_project/
+sample-project
+.dart_tool
\ No newline at end of file
diff --git a/packages/cli/.idea/angel_cli.iml b/packages/cli/.idea/angel_cli.iml
new file mode 100644
index 00000000..6118a7ae
--- /dev/null
+++ b/packages/cli/.idea/angel_cli.iml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/modules.xml b/packages/cli/.idea/modules.xml
new file mode 100644
index 00000000..5f91449b
--- /dev/null
+++ b/packages/cli/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/runConfigurations/Controller.xml b/packages/cli/.idea/runConfigurations/Controller.xml
new file mode 100644
index 00000000..38315109
--- /dev/null
+++ b/packages/cli/.idea/runConfigurations/Controller.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/runConfigurations/Doctor.xml b/packages/cli/.idea/runConfigurations/Doctor.xml
new file mode 100644
index 00000000..bd640ddb
--- /dev/null
+++ b/packages/cli/.idea/runConfigurations/Doctor.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/runConfigurations/Init.xml b/packages/cli/.idea/runConfigurations/Init.xml
new file mode 100644
index 00000000..c0740078
--- /dev/null
+++ b/packages/cli/.idea/runConfigurations/Init.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/runConfigurations/Show_Help.xml b/packages/cli/.idea/runConfigurations/Show_Help.xml
new file mode 100644
index 00000000..009fef31
--- /dev/null
+++ b/packages/cli/.idea/runConfigurations/Show_Help.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/runConfigurations/Update.xml b/packages/cli/.idea/runConfigurations/Update.xml
new file mode 100644
index 00000000..75cab2d3
--- /dev/null
+++ b/packages/cli/.idea/runConfigurations/Update.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.idea/runConfigurations/Version.xml b/packages/cli/.idea/runConfigurations/Version.xml
new file mode 100644
index 00000000..cd70052b
--- /dev/null
+++ b/packages/cli/.idea/runConfigurations/Version.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cli/.travis.yml b/packages/cli/.travis.yml
new file mode 100644
index 00000000..3939d628
--- /dev/null
+++ b/packages/cli/.travis.yml
@@ -0,0 +1 @@
+language: dart
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
new file mode 100644
index 00000000..7e3bf35f
--- /dev/null
+++ b/packages/cli/CHANGELOG.md
@@ -0,0 +1,89 @@
+# 2.1.7+1
+* Fix a bug where new directories were not being created in
+`init`.
+
+# 2.1.7
+* Fix a bug where `ArgResults.arguments` was used in `init` instead of the
+intended `ArgResults.rest`.
+* Stop including `package:angel_model` imports in `make model`.
+* Update dependencies in `make` commands.
+* Fix `make model` to generate ORM + migration by default.
+* Fix `MakerDependency` logic to print missing dependencies.
+
+# 2.1.6
+* Fix a bug where models always defaulted to ORM.
+* Add GraphQL boilerplate.
+* Automatically restore terminal colors on shutdown.
+
+# 2.1.5+1
+* Update to `inflection2`.
+
+# 2.1.5
+* Add `shared` boilerplates.
+* Remove uncecessary `angel_model` imports.
+
+# 2.1.4+1
+* Patch `part of 'path'` renames.
+
+# 2.1.4
+* The `migration` argument to `model` just emits an annotation now.
+* Add the ORM boilerplate.
+
+# 2.1.3
+* Fix generation of ORM models.
+* A `--project-name` to `init` command.
+
+# 2.1.2
+* No migrations-by-default.
+
+# 2.1.1
+* Edit the way `rename` runs, leaving no corner unturned.
+
+# 2.1.0
+* Deprecate `angel install`.
+* Rename projects using `snake_case`.
+* `init` now fetches from `master`.
+* Remove the `1.x` option.
+* Add `make migration` command.
+* Replace `{{oldName}}` in the `rename` command.
+* `pub get` now runs with `inheritStdio`.
+
+# 2.0.1
+* `deploy systemd` now has an `--install` option, where you can immediately
+spawn the service.
+
+# 2.0.0
+* `init` can now produce either 1.x or 2.x projects.
+* Fixed deps for compatibility with Dart2 stable.
+
+# 1.3.4
+* Fix another typo.
+
+# 1.3.3
+* Fix a small typo in the model generator.
+
+# 1.3.2
+* Restore `part` directives in generated models.
+
+# 1.3.1
+* Add `deploy nginx` and `deploy systemd`.
+
+# 1.3.0
+* Focus on Dart2 from here on out.
+* Update `code_builder`.
+* More changes...
+
+# 1.1.5
+Deprecated several commands, in favor of the `make`
+command:
+* `controller`
+* `plugin`
+* `service`
+* `test`
+
+The `rename` command will now replace *all* occurrences
+of the old project names with the new one in `config/`
+YAML files, and also operates on the glob `config/**/*.yaml`.
+
+Changed the call to run `angel start` to run `dart bin/server.dart` instead, after an
+`init` command.
diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE
new file mode 100644
index 00000000..63b4b681
--- /dev/null
+++ b/packages/cli/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) [year] [fullname]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/cli/README.md b/packages/cli/README.md
new file mode 100644
index 00000000..3b96938f
--- /dev/null
+++ b/packages/cli/README.md
@@ -0,0 +1,22 @@
+# angel_cli
+
+
+
+Command-line tools for the Angel framework.
+Includes functionality such as:
+* Project scaffolding
+* Generating service models, plugins, tests and more
+* Renaming projects
+* Much more...
+
+To install:
+
+```bash
+$ pub global activate angel_cli
+```
+
+And then, for information on each command:
+
+```bash
+$ angel help
+```
diff --git a/packages/cli/TODO.md b/packages/cli/TODO.md
new file mode 100644
index 00000000..74835954
--- /dev/null
+++ b/packages/cli/TODO.md
@@ -0,0 +1,7 @@
+# Todo
+
+* `service`
+ * Add tests
+* `migration`
+* `deploy`
+* Call these from Grinder script :)
\ No newline at end of file
diff --git a/packages/cli/analysis_options.yaml b/packages/cli/analysis_options.yaml
new file mode 100644
index 00000000..eae1e42a
--- /dev/null
+++ b/packages/cli/analysis_options.yaml
@@ -0,0 +1,3 @@
+analyzer:
+ strong-mode:
+ implicit-casts: false
\ No newline at end of file
diff --git a/packages/cli/bin/angel.dart b/packages/cli/bin/angel.dart
new file mode 100644
index 00000000..43c62d82
--- /dev/null
+++ b/packages/cli/bin/angel.dart
@@ -0,0 +1,55 @@
+#!/usr/bin/env dart
+library angel_cli.tool;
+
+import "dart:io";
+import "package:args/command_runner.dart";
+import 'package:angel_cli/angel_cli.dart';
+import 'package:io/ansi.dart';
+
+final String DOCTOR = "doctor";
+
+main(List args) async {
+ var runner = new CommandRunner(
+ "angel",
+ asciiArt.trim() +
+ '\n\n' +
+ "Command-line tools for the Angel framework." +
+ '\n\n' +
+ 'https://angel-dart.github.io');
+
+ runner.argParser
+ .addFlag('verbose', help: 'Print verbose output.', negatable: false);
+
+ runner
+ ..addCommand(new DeployCommand())
+ ..addCommand(new DoctorCommand())
+ ..addCommand(new KeyCommand())
+ ..addCommand(new InitCommand())
+ ..addCommand(new InstallCommand())
+ ..addCommand(new RenameCommand())
+ ..addCommand(new MakeCommand());
+
+ return await runner.run(args).catchError((exc, st) {
+ if (exc is String) {
+ stdout.writeln(exc);
+ } else {
+ stderr.writeln("Oops, something went wrong: $exc");
+ if (args.contains('--verbose')) {
+ stderr.writeln(st);
+ }
+ }
+
+ exitCode = 1;
+ }).whenComplete(() {
+ stdout.write(resetAll.wrap(''));
+ });
+}
+
+const String asciiArt = '''
+____________ ________________________
+___ |__ | / /_ ____/__ ____/__ /
+__ /| |_ |/ /_ / __ __ __/ __ /
+_ ___ | /| / / /_/ / _ /___ _ /___
+/_/ |_/_/ |_/ \____/ /_____/ /_____/
+
+''';
diff --git a/packages/cli/example/main.dart b/packages/cli/example/main.dart
new file mode 100644
index 00000000..ffecc0d0
--- /dev/null
+++ b/packages/cli/example/main.dart
@@ -0,0 +1,3 @@
+void main() {
+ // This package isn't usable from code.
+}
diff --git a/packages/cli/lib/angel_cli.dart b/packages/cli/lib/angel_cli.dart
new file mode 100644
index 00000000..843a6734
--- /dev/null
+++ b/packages/cli/lib/angel_cli.dart
@@ -0,0 +1,3 @@
+library angel_cli;
+
+export 'src/commands/commands.dart';
diff --git a/packages/cli/lib/src/commands/commands.dart b/packages/cli/lib/src/commands/commands.dart
new file mode 100644
index 00000000..a6db230d
--- /dev/null
+++ b/packages/cli/lib/src/commands/commands.dart
@@ -0,0 +1,9 @@
+library angel_cli.commands;
+
+export "deploy.dart";
+export "doctor.dart";
+export "key.dart";
+export "init.dart";
+export "install.dart";
+export "make.dart";
+export "rename.dart";
diff --git a/packages/cli/lib/src/commands/deploy.dart b/packages/cli/lib/src/commands/deploy.dart
new file mode 100644
index 00000000..92b5f82e
--- /dev/null
+++ b/packages/cli/lib/src/commands/deploy.dart
@@ -0,0 +1,17 @@
+import 'package:args/command_runner.dart';
+import 'deploy/nginx.dart';
+import 'deploy/systemd.dart';
+
+class DeployCommand extends Command {
+ @override
+ String get name => 'deploy';
+
+ @override
+ String get description =>
+ 'Generates scaffolding + helper functionality for deploying servers. Run this in your project root.';
+
+ DeployCommand() {
+ addSubcommand(new NginxCommand());
+ addSubcommand(new SystemdCommand());
+ }
+}
diff --git a/packages/cli/lib/src/commands/deploy/nginx.dart b/packages/cli/lib/src/commands/deploy/nginx.dart
new file mode 100644
index 00000000..638f70ac
--- /dev/null
+++ b/packages/cli/lib/src/commands/deploy/nginx.dart
@@ -0,0 +1,52 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:io/ansi.dart';
+import 'package:path/path.dart' as p;
+import '../../util.dart';
+
+class NginxCommand extends Command {
+ @override
+ String get name => 'nginx';
+
+ @override
+ String get description =>
+ 'Generates a NGINX configuration for a reverse proxy + static server.';
+
+ NginxCommand() {
+ argParser.addOption('out',
+ abbr: 'o',
+ help:
+ 'An optional output file to write to; otherwise prints to stdout.');
+ }
+
+ @override
+ run() async {
+ var webPath = p.join(p.current, 'web');
+ var nginxText = '''
+server {
+ listen 80 default_server;
+ root ${p.absolute(webPath)}; # Set to your static files directory
+
+ location / {
+ try_files \$uri @proxy; # Try to serve static files; fallback to proxied Angel server
+ }
+
+ location @proxy {
+ proxy_pass http://127.0.0.1:3000;
+ proxy_http_version 1.1; # Important, do not omit
+ }
+}
+ '''
+ .trim();
+
+ if (!argResults.wasParsed('out')) {
+ print(nginxText);
+ } else {
+ var file = new File(argResults['out'] as String);
+ await file.create(recursive: true);
+ await file.writeAsString(nginxText);
+ print(green.wrap(
+ "$checkmark Successfully generated nginx configuration in '${file.path}'."));
+ }
+ }
+}
diff --git a/packages/cli/lib/src/commands/deploy/systemd.dart b/packages/cli/lib/src/commands/deploy/systemd.dart
new file mode 100644
index 00000000..28c496a8
--- /dev/null
+++ b/packages/cli/lib/src/commands/deploy/systemd.dart
@@ -0,0 +1,86 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:io/ansi.dart';
+import 'package:path/path.dart' as p;
+import '../../util.dart';
+
+class SystemdCommand extends Command {
+ @override
+ String get name => 'systemd';
+
+ @override
+ String get description =>
+ 'Generates a systemd service to continuously run your server.';
+
+ SystemdCommand() {
+ argParser
+ ..addOption('install',
+ abbr: 'i', help: 'A name to install this service as on the system.')
+ ..addOption('user',
+ abbr: 'u',
+ defaultsTo: 'web',
+ help: 'The name of the unprivileged account to run the server as.')
+ ..addOption('out',
+ abbr: 'o',
+ help:
+ 'An optional output file to write to; otherwise prints to stdout.');
+ }
+
+ @override
+ run() async {
+ var projectPath = p.absolute(p.current);
+ var pubspec = await loadPubspec();
+ var user = argResults['user'];
+ var systemdText = '''
+[Unit]
+Description=`${pubspec.name}` server
+
+[Service]
+Environment=ANGEL_ENV=production
+User=$user # Name of unprivileged `$user` user
+WorkingDirectory=$projectPath # Path to `${pubspec.name}` project
+ExecStart=${Platform.resolvedExecutable} bin/prod.dart
+Restart=always # Restart process on crash
+
+[Install]
+WantedBy=multi-user.target
+ '''
+ .trim();
+
+ if (!argResults.wasParsed('out') && !argResults.wasParsed('install')) {
+ print(systemdText);
+ } else if (argResults.wasParsed('install')) {
+ var systemdPath = argResults.wasParsed('out')
+ ? argResults['out'] as String
+ : p.join('etc', 'systemd', 'system');
+ var serviceFilename = p.join(systemdPath,
+ p.setExtension(argResults['install'] as String, '.service'));
+ var file = new File(serviceFilename);
+ await file.create(recursive: true);
+ await file.writeAsString(systemdText);
+ print(green.wrap(
+ "$checkmark Successfully generated systemd service in '${file.path}'."));
+
+ // sudo systemctl daemon-reload
+ if (await runCommand('sudo', ['systemctl', 'daemon-reload'])) {
+ // sudo service start
+ if (await runCommand('sudo', [
+ 'service',
+ p.basenameWithoutExtension(serviceFilename),
+ 'start'
+ ])) {
+ } else {
+ print(red.wrap('$ballot Failed to install service system-wide.'));
+ }
+ } else {
+ print(red.wrap('$ballot Failed to install service system-wide.'));
+ }
+ } else {
+ var file = new File(argResults['out'] as String);
+ await file.create(recursive: true);
+ await file.writeAsString(systemdText);
+ print(green.wrap(
+ "$checkmark Successfully generated systemd service in '${file.path}'."));
+ }
+ }
+}
diff --git a/packages/cli/lib/src/commands/doctor.dart b/packages/cli/lib/src/commands/doctor.dart
new file mode 100644
index 00000000..b435b6f0
--- /dev/null
+++ b/packages/cli/lib/src/commands/doctor.dart
@@ -0,0 +1,34 @@
+import "dart:convert";
+import "dart:io";
+import "package:args/command_runner.dart";
+import 'package:io/ansi.dart';
+import '../util.dart';
+
+class DoctorCommand extends Command {
+ @override
+ String get name => "doctor";
+
+ @override
+ String get description =>
+ "Ensures that the current system is capable of running Angel.";
+
+ @override
+ run() async {
+ print("Checking your system for dependencies...");
+ await _checkForGit();
+ }
+
+ _checkForGit() async {
+ try {
+ var git = await Process.start("git", ["--version"]);
+ if (await git.exitCode == 0) {
+ var version = await git.stdout.transform(utf8.decoder).join();
+ print(green.wrap(
+ "$checkmark Git executable found: v${version.replaceAll('git version', '').trim()}"));
+ } else
+ throw new Exception("Git executable exit code not 0");
+ } catch (exc) {
+ print(red.wrap("$ballot Git executable not found"));
+ }
+ }
+}
diff --git a/packages/cli/lib/src/commands/init.dart b/packages/cli/lib/src/commands/init.dart
new file mode 100644
index 00000000..7dafb866
--- /dev/null
+++ b/packages/cli/lib/src/commands/init.dart
@@ -0,0 +1,322 @@
+import 'dart:async';
+import "dart:io";
+import "package:args/command_runner.dart";
+import 'package:io/ansi.dart';
+import 'package:path/path.dart' as p;
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:recase/recase.dart';
+import '../random_string.dart' as rs;
+import '../util.dart';
+import 'key.dart';
+import 'pub.dart';
+import 'rename.dart';
+
+class InitCommand extends Command {
+ final KeyCommand _key = new KeyCommand();
+
+ @override
+ String get name => "init";
+
+ @override
+ String get description =>
+ "Initializes a new Angel project in the current directory.";
+
+ InitCommand() {
+ argParser
+ ..addFlag('offline',
+ help:
+ 'Disable online fetching of boilerplates. Also disables `pub-get`.',
+ negatable: false)
+ ..addFlag('pub-get', defaultsTo: true)
+ ..addOption('project-name',
+ abbr: 'n', help: 'The name for this project.');
+ }
+
+ @override
+ run() async {
+ Directory projectDir =
+ new Directory(argResults.rest.isEmpty ? "." : argResults.rest[0]);
+ print("Creating new Angel project in ${projectDir.absolute.path}...");
+ await _cloneRepo(projectDir);
+ // await preBuild(projectDir);
+ var secret = rs.randomAlphaNumeric(32);
+ print('Generated new development JWT secret: $secret');
+ await _key.changeSecret(
+ new File.fromUri(projectDir.uri.resolve('config/default.yaml')),
+ secret);
+
+ secret = rs.randomAlphaNumeric(32);
+ print('Generated new production JWT secret: $secret');
+ await _key.changeSecret(
+ new File.fromUri(projectDir.uri.resolve('config/production.yaml')),
+ secret);
+
+ var name = argResults.wasParsed('project-name')
+ ? argResults['project-name'] as String
+ : p.basenameWithoutExtension(
+ projectDir.absolute.uri.normalizePath().toFilePath());
+
+ name = ReCase(name).snakeCase;
+ print('Renaming project from "angel" to "$name"...');
+ await renamePubspec(projectDir, 'angel', name);
+ await renameDartFiles(projectDir, 'angel', name);
+
+ if (argResults['pub-get'] != false && argResults['offline'] == false) {
+ print('Now running pub get...');
+ await _pubGet(projectDir);
+ }
+
+ print(green.wrap("$checkmark Successfully initialized Angel project."));
+
+ stdout
+ ..writeln()
+ ..writeln(
+ 'Congratulations! You are ready to start developing with Angel!')
+ ..write('To start the server (with ')
+ ..write(cyan.wrap('hot-reloading'))
+ ..write('), run ')
+ ..write(magenta.wrap('`dart --observe bin/dev.dart`'))
+ ..writeln(' in your terminal.')
+ ..writeln()
+ ..writeln('Find more documentation about Angel:')
+ ..writeln(' * https://angel-dart.github.io')
+ ..writeln(' * https://github.com/angel-dart/angel/wiki')
+ ..writeln(
+ ' * https://www.youtube.com/playlist?list=PLl3P3tmiT-frEV50VdH_cIrA2YqIyHkkY')
+ ..writeln(' * https://medium.com/the-angel-framework')
+ ..writeln(' * https://dart.academy/tag/angel')
+ ..writeln()
+ ..writeln('Happy coding!');
+ }
+
+ _deleteRecursive(FileSystemEntity entity, [bool self = true]) async {
+ if (entity is Directory) {
+ await for (var entity in entity.list(recursive: true)) {
+ try {
+ await _deleteRecursive(entity);
+ } catch (e) {}
+ }
+
+ try {
+ if (self != false) await entity.delete(recursive: true);
+ } catch (e) {}
+ } else if (entity is File) {
+ try {
+ await entity.delete(recursive: true);
+ } catch (e) {}
+ } else if (entity is Link) {
+ var path = await entity.resolveSymbolicLinks();
+ var stat = await FileStat.stat(path);
+
+ switch (stat.type) {
+ case FileSystemEntityType.directory:
+ return await _deleteRecursive(new Directory(path));
+ case FileSystemEntityType.file:
+ return await _deleteRecursive(new File(path));
+ default:
+ break;
+ }
+ }
+ }
+
+ _cloneRepo(Directory projectDir) async {
+ Directory boilerplateDir;
+
+ try {
+ if (await projectDir.exists()) {
+ var shouldDelete = prompts.getBool(
+ "Directory '${projectDir.absolute.path}' already exists. Overwrite it?");
+
+ if (!shouldDelete)
+ throw "Chose not to overwrite existing directory.";
+ else if (projectDir.absolute.uri.normalizePath().toFilePath() !=
+ Directory.current.absolute.uri.normalizePath().toFilePath())
+ await projectDir.delete(recursive: true);
+ else {
+ await _deleteRecursive(projectDir, false);
+ }
+ }
+
+ // var boilerplate = basicBoilerplate;
+ print('Choose a project type before continuing:');
+ var boilerplate = prompts.choose(
+ 'Choose a project type before continuing', boilerplates);
+
+ // Ultimately, we want a clone of every boilerplate locally on the system.
+ var boilerplateRootDir = Directory(p.join(angelDir.path, 'boilerplates'));
+ var boilerplateBasename = p.basenameWithoutExtension(boilerplate.url);
+ if (boilerplate.ref != null) boilerplateBasename += '.${boilerplate.ref}';
+ boilerplateDir =
+ Directory(p.join(boilerplateRootDir.path, boilerplateBasename));
+ await boilerplateRootDir.create(recursive: true);
+
+ var branch = boilerplate.ref ?? 'master';
+
+ // If there is no clone existing, clone it.
+ if (!await boilerplateDir.exists()) {
+ if (argResults['offline'] as bool) {
+ throw Exception(
+ '--offline was selected, but the "${boilerplate.name}" boilerplate has not yet been downloaded.');
+ }
+
+ print(
+ 'Cloning "${boilerplate.name}" boilerplate from "${boilerplate.url}"...');
+ Process git;
+
+ if (boilerplate.ref == null) {
+ print(darkGray.wrap(
+ '\$ git clone --depth 1 ${boilerplate.url} ${boilerplateDir.absolute.path}'));
+ git = await Process.start(
+ "git",
+ [
+ "clone",
+ "--depth",
+ "1",
+ boilerplate.url,
+ boilerplateDir.absolute.path
+ ],
+ mode: ProcessStartMode.inheritStdio,
+ );
+ } else {
+ // git clone --single-branch -b branch host:/dir.git
+ print(darkGray.wrap(
+ '\$ git clone --depth 1 --single-branch -b ${boilerplate.ref} ${boilerplate.url} ${boilerplateDir.absolute.path}'));
+ git = await Process.start(
+ "git",
+ [
+ "clone",
+ "--depth",
+ "1",
+ "--single-branch",
+ "-b",
+ boilerplate.ref,
+ boilerplate.url,
+ boilerplateDir.absolute.path
+ ],
+ mode: ProcessStartMode.inheritStdio,
+ );
+ }
+
+ if (await git.exitCode != 0) {
+ throw new Exception("Could not clone repo.");
+ }
+ }
+
+ // Otherwise, pull from git.
+ else if (!(argResults['offline'] as bool)) {
+ print(darkGray.wrap('\$ git pull origin $branch'));
+ var git = await Process.start("git", ['pull', 'origin', '$branch'],
+ mode: ProcessStartMode.inheritStdio,
+ workingDirectory: boilerplateDir.absolute.path);
+ if (await git.exitCode != 0) {
+ print(yellow.wrap(
+ "Update of $branch failed. Attempting to continue with existing contents."));
+ }
+ } else {
+ print(darkGray.wrap(
+ 'Using existing contents of "${boilerplate.name}" boilerplate.'));
+ }
+
+ // Next, just copy everything into the given directory.
+ await copyDirectory(boilerplateDir, projectDir);
+
+ if (boilerplate.needsPrebuild) {
+ await preBuild(projectDir).catchError((_) => null);
+ }
+
+ var gitDir = new Directory.fromUri(projectDir.uri.resolve(".git"));
+ if (await gitDir.exists()) await gitDir.delete(recursive: true);
+ } catch (e) {
+ await boilerplateDir.delete(recursive: true).catchError((_) => null);
+
+ if (e is! String) {
+ print(red.wrap("$ballot Could not initialize Angel project."));
+ }
+ rethrow;
+ }
+ }
+
+ _pubGet(Directory projectDir) async {
+ var pubPath = resolvePub();
+ print(darkGray.wrap('Running pub at "$pubPath"...'));
+ print(darkGray.wrap('\$ $pubPath get'));
+ var pub = await Process.start(pubPath, ["get"],
+ workingDirectory: projectDir.absolute.path,
+ mode: ProcessStartMode.inheritStdio);
+ var code = await pub.exitCode;
+ print("Pub process exited with code $code");
+ }
+}
+
+Future preBuild(Directory projectDir) async {
+ // Run build
+ // print('Running `pub run build_runner build`...');
+ print(darkGray.wrap('\$ pub run build_runner build'));
+
+ var build = await Process.start(
+ resolvePub(), ['run', 'build_runner', 'build'],
+ workingDirectory: projectDir.absolute.path,
+ mode: ProcessStartMode.inheritStdio);
+
+ var buildCode = await build.exitCode;
+
+ if (buildCode != 0) throw new Exception('Failed to pre-build resources.');
+}
+
+const BoilerplateInfo graphQLBoilerplate = const BoilerplateInfo(
+ 'GraphQL',
+ "A starting point for GraphQL API servers.",
+ 'https://github.com/angel-dart/angel.git',
+ ref: 'graphql',
+);
+
+const BoilerplateInfo ormBoilerplate = const BoilerplateInfo(
+ 'ORM',
+ "A starting point for applications that use Angel's ORM.",
+ 'https://github.com/angel-dart/angel.git',
+ ref: 'orm',
+);
+
+const BoilerplateInfo basicBoilerplate = const BoilerplateInfo(
+ 'Basic',
+ 'Minimal starting point for Angel 2.x - A simple server with only a few additional packages.',
+ 'https://github.com/angel-dart/angel.git');
+
+const BoilerplateInfo legacyBoilerplate = const BoilerplateInfo(
+ 'Legacy',
+ 'Minimal starting point for applications running Angel 1.1.x.',
+ 'https://github.com/angel-dart/angel.git',
+ ref: '1.1.x',
+);
+
+const BoilerplateInfo sharedBoilerplate = const BoilerplateInfo(
+ 'Shared',
+ 'Holds common models and files shared across multiple Dart projects.',
+ 'https://github.com/angel-dart/boilerplate_shared.git');
+
+const BoilerplateInfo sharedOrmBoilerplate = const BoilerplateInfo(
+ 'Shared (ORM)',
+ 'Holds common models and files shared across multiple Dart projects.',
+ 'https://github.com/angel-dart/boilerplate_shared.git',
+ ref: 'orm',
+);
+
+const List boilerplates = const [
+ basicBoilerplate,
+ //legacyBoilerplate,
+ ormBoilerplate,
+ graphQLBoilerplate,
+ sharedBoilerplate,
+ sharedOrmBoilerplate,
+];
+
+class BoilerplateInfo {
+ final String name, description, url, ref;
+ final bool needsPrebuild;
+
+ const BoilerplateInfo(this.name, this.description, this.url,
+ {this.ref, this.needsPrebuild: false});
+
+ @override
+ String toString() => '$name ($description)';
+}
diff --git a/packages/cli/lib/src/commands/install.dart b/packages/cli/lib/src/commands/install.dart
new file mode 100644
index 00000000..d7a9ec37
--- /dev/null
+++ b/packages/cli/lib/src/commands/install.dart
@@ -0,0 +1,248 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:glob/glob.dart';
+import 'package:io/ansi.dart';
+import 'package:mustache4dart/mustache4dart.dart' as mustache;
+import 'package:path/path.dart' as p;
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:yaml/yaml.dart' as yaml;
+import '../util.dart';
+import 'make/maker.dart';
+
+class InstallCommand extends Command {
+ static const String repo = 'https://github.com/angel-dart/install.git';
+ static final Directory installRepo =
+ new Directory.fromUri(homeDir.uri.resolve('./.angel/addons'));
+
+ @override
+ String get name => 'install';
+
+ @override
+ String get description =>
+ 'Installs additional add-ons to minimize boilerplate.';
+
+ InstallCommand() {
+ argParser
+ ..addFlag(
+ 'list',
+ help: 'List all currently-installed add-ons.',
+ negatable: false,
+ defaultsTo: false,
+ )
+ ..addFlag(
+ 'update',
+ help: 'Update the local add-on repository.',
+ negatable: false,
+ defaultsTo: false,
+ )
+ ..addFlag(
+ 'wipe',
+ help: 'Wipe the local add-on repository.',
+ negatable: false,
+ defaultsTo: false,
+ );
+ }
+
+ @override
+ run() async {
+ print(yellow.wrap(
+ 'WARNING: The `install` command is no longer considered necessary, and has been deprecated.\n'
+ 'Expect it to be removed in an upcoming release.\n\n'
+ 'See here: https://github.com/angel-dart/install.git\n\n'
+ 'To stop seeing this, downgrade to `package:angel_cli@<=2.0.0`.'));
+
+ if (argResults['wipe'] as bool) {
+ if (await installRepo.exists()) await installRepo.delete(recursive: true);
+ } else if (argResults['list'] as bool) {
+ var addons = await list();
+ print('${addons.length} add-on(s) installed:');
+
+ for (var addon in addons) {
+ print(' * ${addon.name}@${addon.version}: ${addon.description}');
+ }
+ } else if (argResults['update'] as bool) {
+ await update();
+ } else if (argResults.rest.isNotEmpty) {
+ if (!await installRepo.exists())
+ throw 'No local add-on database exists. Run `angel install --update` first.';
+
+ var pubspec = await loadPubspec();
+
+ for (var packageName in argResults.rest) {
+ var packageDir =
+ new Directory.fromUri(installRepo.uri.resolve(packageName));
+
+ if (!await packageDir.exists())
+ throw 'No add-on named "$packageName" is installed. You might need to run `angel install --update`.';
+ print('Installing $packageName...');
+
+ Map values = {
+ 'project_name': pubspec.name,
+ 'pubspec': pubspec,
+ };
+
+ List globs = [];
+
+ var projectPubspec = await loadPubspec(packageDir);
+ var deps = projectPubspec.dependencies.keys
+ .map((k) {
+ var dep = projectPubspec.dependencies[k];
+ if (dep is HostedDependency)
+ return new MakerDependency(k, dep.version.toString());
+ return null;
+ })
+ .where((d) => d != null)
+ .toList();
+
+ deps.addAll(projectPubspec.devDependencies.keys.map((k) {
+ var dep = projectPubspec.devDependencies[k];
+ if (dep is HostedDependency)
+ return new MakerDependency(k, dep.version.toString(), dev: true);
+ return null;
+ }).where((d) => d != null));
+
+ await depend(deps);
+
+ var promptFile =
+ new File.fromUri(packageDir.uri.resolve('angel_cli.yaml'));
+
+ if (await promptFile.exists()) {
+ var contents = await promptFile.readAsString();
+ var y = yaml.loadYamlDocument(contents);
+ var cfg = y.contents.value as Map;
+
+ // Loads globs
+ if (cfg['templates'] is List) {
+ globs.addAll(
+ (cfg['templates'] as List).map((p) => new Glob(p.toString())));
+ }
+
+ if (cfg['values'] is Map) {
+ var val = cfg['values'] as Map;
+
+ for (var key in val.keys) {
+ var desc = val[key]['description'] ?? key;
+
+ if (val[key]['type'] == 'prompt') {
+ values[key] = prompts.get(desc.toString(),
+ defaultsTo: val[key]['default']?.toString());
+ } else if (val[key]['type'] == 'choice') {
+ values[key] = prompts.choose(
+ desc.toString(), val[key]['choices'] as Iterable);
+ }
+ }
+ }
+ }
+
+ Future merge(Directory src, Directory dst, String prefix) async {
+ if (!await src.exists()) return;
+ print('Copying ${src.absolute.path} into ${dst.absolute.path}...');
+ if (!await dst.exists()) await dst.create(recursive: true);
+
+ await for (var entity in src.list()) {
+ if (entity is Directory) {
+ var name = p.basename(entity.path);
+ var newDir = new Directory.fromUri(dst.uri.resolve(name));
+ await merge(
+ entity, newDir, prefix.isEmpty ? name : '$prefix/$name');
+ } else if (entity is File &&
+ !entity.path.endsWith('angel_cli.yaml')) {
+ var name = p.basename(entity.path);
+ var target = dst.uri.resolve(name);
+ var targetFile = new File.fromUri(target);
+ bool allClear = !await targetFile.exists();
+
+ if (!allClear) {
+ print('The file ${entity.absolute.path} already exists.');
+ allClear = prompts.getBool('Overwrite the existing file?');
+ if (allClear) await targetFile.delete();
+ }
+
+ if (allClear) {
+ try {
+ var path = prefix.isEmpty ? name : '$prefix/$name';
+
+ if (globs.any((g) => g.matches(path))) {
+ print(
+ 'Rendering Mustache template from ${entity.absolute.path} to ${targetFile.absolute.path}...');
+ var contents = await entity.readAsString();
+ var renderer = mustache.compile(contents);
+ var generated = renderer(values);
+ await targetFile.writeAsString(generated.toString());
+ } else {
+ print(
+ 'Copying ${entity.absolute.path} to ${targetFile.absolute.path}...');
+ await targetFile.parent.create(recursive: true);
+ await entity.copy(targetFile.absolute.path);
+ }
+ } catch (_) {
+ print('Failed to copy.');
+ }
+ } else {
+ print('Skipped ${entity.absolute.path}.');
+ }
+ }
+ }
+ }
+
+ await merge(new Directory.fromUri(packageDir.uri.resolve('files')),
+ Directory.current, '');
+ print('Successfully installed $packageName@${projectPubspec.version}.');
+ }
+ } else {
+ print('No add-ons were specified to be installed.');
+ }
+ }
+
+ Future> list() async {
+ if (!await installRepo.exists()) {
+ throw 'No local add-on database exists. Run `angel install --update` first.';
+ } else {
+ List repos = [];
+
+ await for (var entity in installRepo.list()) {
+ if (entity is Directory) {
+ try {
+ repos.add(await loadPubspec(entity));
+ } catch (_) {
+ // Ignore failures...
+ }
+ }
+ }
+
+ return repos;
+ }
+ }
+
+ Future update() async {
+ Process git;
+
+ if (!await installRepo.exists()) {
+ git = await Process.start('git', [
+ 'clone',
+ repo,
+ installRepo.absolute.path,
+ ]);
+ } else {
+ git = await Process.start(
+ 'git',
+ [
+ 'pull',
+ 'origin',
+ 'master',
+ ],
+ workingDirectory: installRepo.absolute.path,
+ );
+ }
+
+ git..stdout.listen(stdout.add)..stderr.listen(stderr.add);
+
+ var code = await git.exitCode;
+
+ if (code != 0) {
+ throw 'git exited with code $code.';
+ }
+ }
+}
diff --git a/packages/cli/lib/src/commands/key.dart b/packages/cli/lib/src/commands/key.dart
new file mode 100644
index 00000000..cab986f8
--- /dev/null
+++ b/packages/cli/lib/src/commands/key.dart
@@ -0,0 +1,30 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import '../random_string.dart' as rs;
+
+class KeyCommand extends Command {
+ @override
+ String get name => 'key';
+
+ @override
+ String get description => 'Generates a new `angel_auth` key.';
+
+ @override
+ run() async {
+ var secret = rs.randomAlphaNumeric(32);
+ print('Generated new development JWT secret: $secret');
+ await changeSecret(new File('config/default.yaml'), secret);
+
+ secret = rs.randomAlphaNumeric(32);
+ print('Generated new production JWT secret: $secret');
+ await changeSecret(new File('config/production.yaml'), secret);
+ }
+
+ changeSecret(File file, String secret) async {
+ if (await file.exists()) {
+ var contents = await file.readAsString();
+ contents = contents.replaceAll(new RegExp(r'jwt_secret:[^\n]+\n?'), '');
+ await file.writeAsString(contents.trim() + '\njwt_secret: "$secret"');
+ }
+ }
+}
diff --git a/packages/cli/lib/src/commands/make.dart b/packages/cli/lib/src/commands/make.dart
new file mode 100644
index 00000000..219fedf1
--- /dev/null
+++ b/packages/cli/lib/src/commands/make.dart
@@ -0,0 +1,25 @@
+import 'package:args/command_runner.dart';
+import 'make/controller.dart';
+import 'make/migration.dart';
+import 'make/model.dart';
+import 'make/plugin.dart';
+import 'make/service.dart';
+import 'make/test.dart';
+
+class MakeCommand extends Command {
+ @override
+ String get name => 'make';
+
+ @override
+ String get description =>
+ 'Generates common code for your project, such as projects and controllers.';
+
+ MakeCommand() {
+ addSubcommand(new ControllerCommand());
+ addSubcommand(new MigrationCommand());
+ addSubcommand(new ModelCommand());
+ addSubcommand(new PluginCommand());
+ addSubcommand(new TestCommand());
+ addSubcommand(new ServiceCommand());
+ }
+}
diff --git a/packages/cli/lib/src/commands/make/controller.dart b/packages/cli/lib/src/commands/make/controller.dart
new file mode 100644
index 00000000..209aa401
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/controller.dart
@@ -0,0 +1,123 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:recase/recase.dart';
+import '../../util.dart';
+import 'maker.dart';
+
+class ControllerCommand extends Command {
+ @override
+ String get name => 'controller';
+
+ @override
+ String get description => 'Generates a controller class.';
+
+ ControllerCommand() {
+ argParser
+ ..addFlag('websocket',
+ abbr: 'w',
+ help:
+ 'Generates a WebSocketController, instead of an HTTP controller.',
+ negatable: false)
+ ..addOption('name',
+ abbr: 'n', help: 'Specifies a name for the model class.')
+ ..addOption('output-dir',
+ help: 'Specifies a directory to create the controller class in.',
+ defaultsTo: 'lib/src/routes/controllers');
+ }
+
+ @override
+ run() async {
+ var pubspec = await loadPubspec();
+ String name;
+ if (argResults.wasParsed('name')) name = argResults['name'] as String;
+
+ if (name?.isNotEmpty != true) {
+ name = prompts.get('Name of controller class');
+ }
+
+ List deps = [
+ const MakerDependency('angel_framework', '^2.0.0')
+ ];
+
+ // ${pubspec.name}.src.models.${rc.snakeCase}
+
+ var rc = new ReCase(name);
+ var controllerLib = new Library((controllerLib) {
+ if (argResults['websocket'] as bool) {
+ deps.add(const MakerDependency('angel_websocket', '^2.0.0'));
+ controllerLib.directives
+ .add(new Directive.import('package:angel_websocket/server.dart'));
+ } else {
+ controllerLib.directives.add(new Directive.import(
+ 'package:angel_framework/angel_framework.dart'));
+ }
+
+ controllerLib.body.add(new Class((clazz) {
+ clazz
+ ..name = '${rc.pascalCase}Controller'
+ ..extend = refer(argResults['websocket'] as bool
+ ? 'WebSocketController'
+ : 'Controller');
+
+ if (argResults['websocket'] as bool) {
+ // XController(AngelWebSocket ws) : super(ws);
+ clazz.constructors.add(new Constructor((b) {
+ b
+ ..requiredParameters.add(new Parameter((b) => b
+ ..name = 'ws'
+ ..type = refer('AngelWebSocket')))
+ ..initializers.add(new Code('super(ws)'));
+ }));
+
+ clazz.methods.add(new Method((meth) {
+ meth
+ ..name = 'hello'
+ ..returns = refer('void')
+ ..annotations
+ .add(refer('ExposeWs').call([literal('get_${rc.snakeCase}')]))
+ ..requiredParameters.add(new Parameter((b) => b
+ ..name = 'socket'
+ ..type = refer('WebSocketContext')))
+ ..body = new Block((block) {
+ block.addExpression(refer('socket').property('send').call([
+ literal('got_${rc.snakeCase}'),
+ literalMap({'message': literal('Hello, world!')}),
+ ]));
+ });
+ }));
+ } else {
+ clazz
+ ..annotations
+ .add(refer('Expose').call([literal('/${rc.snakeCase}')]))
+ ..methods.add(new Method((meth) {
+ meth
+ ..name = 'hello'
+ ..returns = refer('String')
+ ..body = literal('Hello, world').returned.statement
+ ..annotations.add(refer('Expose').call([
+ literal('/'),
+ ]));
+ }));
+ }
+ }));
+ });
+
+ var outputDir = new Directory.fromUri(
+ Directory.current.uri.resolve(argResults['output-dir'] as String));
+ var controllerFile =
+ new File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart'));
+ if (!await controllerFile.exists())
+ await controllerFile.create(recursive: true);
+ await controllerFile.writeAsString(new DartFormatter()
+ .format(controllerLib.accept(new DartEmitter()).toString()));
+
+ print(green.wrap(
+ '$checkmark Created controller file "${controllerFile.absolute.path}"'));
+
+ if (deps.isNotEmpty) await depend(deps);
+ }
+}
diff --git a/packages/cli/lib/src/commands/make/maker.dart b/packages/cli/lib/src/commands/make/maker.dart
new file mode 100644
index 00000000..09bf1bd9
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/maker.dart
@@ -0,0 +1,82 @@
+import 'dart:async';
+import 'package:io/ansi.dart';
+import '../../util.dart';
+
+class MakerDependency implements Comparable {
+ final String name, version;
+ final bool dev;
+
+ const MakerDependency(this.name, this.version, {this.dev: false});
+
+ @override
+ int compareTo(MakerDependency other) => name.compareTo(other.name);
+}
+
+Future depend(Iterable deps) async {
+ var pubspec = await loadPubspec();
+ var missing = [];
+
+ for (var dep in deps) {
+ var isPresent = false;
+ if (dep.dev)
+ isPresent = pubspec.devDependencies.containsKey(dep.name);
+ else
+ isPresent = pubspec.dependencies.containsKey(dep.name);
+
+ if (!isPresent) {
+ missing.add(dep);
+// TODO: https://github.com/dart-lang/pubspec_parse/issues/17:
+// print('Installing ${dep.name}@${dep.version}...');
+//
+// if (dep.dev) {
+// pubspec.devDependencies[dep.name] = new HostedDependency(
+// version: new VersionConstraint.parse(dep.version),
+// );
+// } else {
+// pubspec.dependencies[dep.name] = new HostedDependency(
+// version: new VersionConstraint.parse(dep.version),
+// );
+// }
+ }
+ }
+
+ var missingDeps = missing.where((d) => !d.dev).toList()..sort();
+ var missingDevDeps = missing.where((d) => d.dev).toList()..sort();
+ var totalCount = missingDeps.length + missingDevDeps.length;
+
+ if (totalCount > 0) {
+ print(yellow.wrap(totalCount == 1
+ ? 'You are missing one dependency.'
+ : 'You are missing $totalCount dependencies.'));
+ print(yellow.wrap(
+ 'Update your `pubspec.yaml` to add the following dependencies:\n'));
+
+ void printMissing(String type, Iterable deps) {
+ if (deps.isNotEmpty) {
+ print(yellow.wrap(' $type:'));
+ for (var dep in deps) {
+ print(yellow.wrap(' ${dep.name}: ${dep.version}'));
+ }
+ }
+ }
+
+ printMissing('dependencies', missingDeps);
+ printMissing('dev_dependencies', missingDevDeps);
+ print('\n');
+ }
+
+// if (isPresent) {
+// TODO: https://github.com/dart-lang/pubspec_parse/issues/17
+// await savePubspec(pubspec);
+// var pubPath = resolvePub();
+//
+// print('Now running `$pubPath get`...');
+//
+// var pubGet = await Process.start(pubPath, ['get']);
+// pubGet.stdout.listen(stdout.add);
+// pubGet.stderr.listen(stderr.add);
+//
+// var code = await pubGet.exitCode;
+//
+// if (code != 0) throw 'pub get terminated with exit code $code';
+}
diff --git a/packages/cli/lib/src/commands/make/migration.dart b/packages/cli/lib/src/commands/make/migration.dart
new file mode 100644
index 00000000..a5792b0c
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/migration.dart
@@ -0,0 +1,133 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:inflection2/inflection2.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:recase/recase.dart';
+import '../../util.dart';
+import 'maker.dart';
+
+class MigrationCommand extends Command {
+ @override
+ String get name => 'migration';
+
+ @override
+ String get description => 'Generates a migration class.';
+
+ MigrationCommand() {
+ argParser
+ ..addOption('name',
+ abbr: 'n', help: 'Specifies a name for the model class.')
+ ..addOption('output-dir',
+ help: 'Specifies a directory to create the migration class in.',
+ defaultsTo: 'tool/migrations');
+ }
+
+ @override
+ FutureOr run() async {
+ String name;
+ if (argResults.wasParsed('name')) name = argResults['name'] as String;
+
+ if (name?.isNotEmpty != true) {
+ name = prompts.get('Name of model class');
+ }
+
+ var deps = [const MakerDependency('angel_migration', '^2.0.0')];
+ var rc = new ReCase(name);
+
+ var migrationLib = new Library((migrationLib) {
+ migrationLib
+ ..directives.add(new Directive.import(
+ 'package:angel_migration.dart/angel_migration.dart'))
+ ..body.add(new Class((migrationClazz) {
+ migrationClazz
+ ..name = '${rc.pascalCase}Migration'
+ ..extend = refer('Migration');
+
+ var tableName = pluralize(rc.snakeCase);
+
+ // up()
+ migrationClazz.methods.add(new Method((up) {
+ up
+ ..name = 'up'
+ ..returns = refer('void')
+ ..annotations.add(refer('override'))
+ ..requiredParameters.add(new Parameter((b) => b
+ ..name = 'schema'
+ ..type = refer('Schema')))
+ ..body = new Block((block) {
+ // (table) { ... }
+ var callback = new Method((callback) {
+ callback
+ ..requiredParameters
+ .add(new Parameter((b) => b..name = 'table'))
+ ..body = new Block((block) {
+ var table = refer('table');
+
+ block.addExpression(
+ (table.property('serial').call([literal('id')]))
+ .property('primaryKey')
+ .call([]),
+ );
+
+ block.addExpression(
+ table.property('date').call([
+ literal('created_at'),
+ ]),
+ );
+
+ block.addExpression(
+ table.property('date').call([
+ literal('updated_at'),
+ ]),
+ );
+ });
+ });
+
+ block.addExpression(refer('schema').property('create').call([
+ literal(tableName),
+ callback.closure,
+ ]));
+ });
+ }));
+
+ // down()
+ migrationClazz.methods.add(new Method((down) {
+ down
+ ..name = 'down'
+ ..returns = refer('void')
+ ..annotations.add(refer('override'))
+ ..requiredParameters.add(new Parameter((b) => b
+ ..name = 'schema'
+ ..type = refer('Schema')))
+ ..body = new Block((block) {
+ block.addExpression(
+ refer('schema').property('drop').call([
+ literal(tableName),
+ ]),
+ );
+ });
+ }));
+ }));
+ });
+
+ // Save migration file
+ var migrationDir = new Directory.fromUri(
+ Directory.current.uri.resolve(argResults['output-dir'] as String));
+ var migrationFile =
+ new File.fromUri(migrationDir.uri.resolve('${rc.snakeCase}.dart'));
+ if (!await migrationFile.exists())
+ await migrationFile.create(recursive: true);
+
+ await migrationFile.writeAsString(new DartFormatter()
+ .format(migrationLib.accept(new DartEmitter()).toString()));
+
+ print(green.wrap(
+ '$checkmark Created migration file "${migrationFile.absolute.path}".'));
+
+ await depend(deps);
+ }
+}
diff --git a/packages/cli/lib/src/commands/make/model.dart b/packages/cli/lib/src/commands/make/model.dart
new file mode 100644
index 00000000..04e964c8
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/model.dart
@@ -0,0 +1,122 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:recase/recase.dart';
+import '../../util.dart';
+import 'maker.dart';
+
+class ModelCommand extends Command {
+ @override
+ String get name => 'model';
+
+ @override
+ String get description => 'Generates a model class.';
+
+ ModelCommand() {
+ argParser
+ ..addFlag('migration',
+ abbr: 'm',
+ help: 'Generate migrations when running `build_runner`.',
+ defaultsTo: true)
+ ..addFlag('orm', help: 'Generate angel_orm code.', negatable: false)
+ ..addFlag('serializable',
+ help: 'Generate angel_serialize annotations.', defaultsTo: true)
+ ..addOption('name',
+ abbr: 'n', help: 'Specifies a name for the model class.')
+ ..addOption('output-dir',
+ help: 'Specifies a directory to create the model class in.',
+ defaultsTo: 'lib/src/models');
+ }
+
+ @override
+ run() async {
+ String name;
+ if (argResults.wasParsed('name')) name = argResults['name'] as String;
+
+ if (name?.isNotEmpty != true) {
+ name = prompts.get('Name of model class');
+ }
+
+ List deps = [
+ const MakerDependency('angel_model', '^1.0.0'),
+ ];
+
+ var rc = new ReCase(name);
+
+ var modelLib = new Library((modelLib) {
+ if (argResults['orm'] as bool && argResults['migration'] as bool) {
+ modelLib.directives.addAll([
+ new Directive.import('package:angel_migration/angel_migration.dart'),
+ ]);
+ }
+
+ var needsSerialize =
+ argResults['serializable'] as bool || argResults['orm'] as bool;
+ // argResults['migration'] as bool;
+
+ if (needsSerialize) {
+ modelLib.directives.add(new Directive.import(
+ 'package:angel_serialize/angel_serialize.dart'));
+ deps.add(const MakerDependency('angel_serialize', '^2.0.0'));
+ deps.add(const MakerDependency('angel_serialize_generator', '^2.0.0'));
+ deps.add(const MakerDependency('build_runner', '^1.0.0'));
+ }
+
+ // else {
+ // modelLib.directives
+ // .add(new Directive.import('package:angel_model/angel_model.dart'));
+ // deps.add(const MakerDependency('angel_model', '^1.0.0'));
+ // }
+
+ if (argResults['orm'] as bool) {
+ modelLib.directives.addAll([
+ new Directive.import('package:angel_orm/angel_orm.dart'),
+ ]);
+ deps.add(const MakerDependency('angel_orm', '^2.0.0'));
+ }
+
+ modelLib.body.addAll([
+ new Code("part '${rc.snakeCase}.g.dart';"),
+ ]);
+
+ modelLib.body.add(new Class((modelClazz) {
+ modelClazz
+ ..abstract = true
+ ..name = needsSerialize ? '_${rc.pascalCase}' : rc.pascalCase
+ ..extend = refer('Model');
+
+ if (needsSerialize) {
+ // modelLib.addDirective(new PartBuilder('${rc.snakeCase}.g.dart'));
+ modelClazz.annotations.add(refer('serializable'));
+ }
+
+ if (argResults['orm'] as bool) {
+ if (argResults['migration'] as bool) {
+ modelClazz.annotations.add(refer('orm'));
+ } else {
+ modelClazz.annotations.add(
+ refer('Orm').call([], {'generateMigration': literalFalse}));
+ }
+ }
+ }));
+ });
+
+ // Save model file
+ var outputDir = new Directory.fromUri(
+ Directory.current.uri.resolve(argResults['output-dir'] as String));
+ var modelFile =
+ new File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart'));
+ if (!await modelFile.exists()) await modelFile.create(recursive: true);
+
+ await modelFile.writeAsString(new DartFormatter()
+ .format(modelLib.accept(new DartEmitter()).toString()));
+
+ print(green
+ .wrap('$checkmark Created model file "${modelFile.absolute.path}".'));
+
+ if (deps.isNotEmpty) await depend(deps);
+ }
+}
diff --git a/packages/cli/lib/src/commands/make/plugin.dart b/packages/cli/lib/src/commands/make/plugin.dart
new file mode 100644
index 00000000..4f979777
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/plugin.dart
@@ -0,0 +1,69 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:recase/recase.dart';
+import '../../util.dart';
+import 'maker.dart';
+
+class PluginCommand extends Command {
+ @override
+ String get name => "plugin";
+
+ @override
+ String get description => "Creates a new plug-in within the given project.";
+
+ PluginCommand() {
+ argParser
+ ..addOption('name',
+ abbr: 'n', help: 'Specifies a name for the plug-in class.')
+ ..addOption('output-dir',
+ help: 'Specifies a directory to create the plug-in class in.',
+ defaultsTo: 'lib/src/config/plugins');
+ }
+
+ @override
+ run() async {
+ var pubspec = await loadPubspec();
+ String name;
+ if (argResults.wasParsed('name')) name = argResults['name'] as String;
+
+ if (name?.isNotEmpty != true) {
+ name = prompts.get('Name of plug-in class');
+ }
+
+ List deps = [
+ const MakerDependency('angel_framework', '^2.0.0')
+ ];
+
+ var rc = new ReCase(name);
+ final pluginDir = new Directory.fromUri(
+ Directory.current.uri.resolve(argResults['output-dir'] as String));
+ final pluginFile =
+ new File.fromUri(pluginDir.uri.resolve("${rc.snakeCase}.dart"));
+ if (!await pluginFile.exists()) await pluginFile.create(recursive: true);
+ await pluginFile.writeAsString(
+ new DartFormatter().format(_generatePlugin(pubspec, rc)));
+
+ if (deps.isNotEmpty) await depend(deps);
+
+ print(green.wrap(
+ '$checkmark Successfully generated plug-in file "${pluginFile.absolute.path}".'));
+ }
+
+ String _generatePlugin(Pubspec pubspec, ReCase rc) {
+ return '''
+library ${pubspec.name}.src.config.plugins.${rc.snakeCase};
+
+import 'package:angel_framework/angel_framework.dart';
+
+AngelConfigurer ${rc.camelCase}() {
+ return (Angel app) async {
+ // Work some magic...
+ };
+}
+ ''';
+ }
+}
diff --git a/packages/cli/lib/src/commands/make/service.dart b/packages/cli/lib/src/commands/make/service.dart
new file mode 100644
index 00000000..a21d07f1
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/service.dart
@@ -0,0 +1,136 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:inflection2/inflection2.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:recase/recase.dart';
+import '../service_generators/service_generators.dart';
+import '../../util.dart';
+import 'maker.dart';
+
+class ServiceCommand extends Command {
+ @override
+ String get name => 'service';
+
+ @override
+ String get description => 'Generates an Angel service.';
+
+ ServiceCommand() {
+ argParser
+ ..addFlag('typed',
+ abbr: 't',
+ help: 'Wrap the generated service in a `TypedService` instance.',
+ negatable: false)
+ ..addOption('name',
+ abbr: 'n', help: 'Specifies a name for the service file.')
+ ..addOption('output-dir',
+ help: 'Specifies a directory to create the service file.',
+ defaultsTo: 'lib/src/services');
+ }
+
+ @override
+ run() async {
+ var pubspec = await loadPubspec();
+ String name;
+ if (argResults.wasParsed('name')) name = argResults['name'] as String;
+
+ if (name?.isNotEmpty != true) {
+ name = prompts.get('Name of service');
+ }
+
+ List deps = [
+ const MakerDependency('angel_framework', '^2.0.0')
+ ];
+
+ // '${pubspec.name}.src.services.${rc.snakeCase}'
+ var rc = new ReCase(name);
+ var serviceLib = new Library((serviceLib) {
+ var generator = prompts.choose(
+ 'Choose which type of service to create', serviceGenerators);
+
+// if (generator == null) {
+// _pen.red();
+// _pen('${Icon.BALLOT_X} \'$type\' services are not yet implemented. :(');
+// _pen();
+// throw 'Unrecognized service type: "$type".';
+// }
+
+ for (var dep in generator.dependencies) {
+ if (!deps.any((d) => d.name == dep.name)) deps.add(dep);
+ }
+
+ if (generator.goesFirst) {
+ generator.applyToLibrary(serviceLib, name, rc.snakeCase);
+ serviceLib.directives.add(new Directive.import(
+ 'package:angel_framework/angel_framework.dart'));
+ } else {
+ serviceLib.directives.add(new Directive.import(
+ 'package:angel_framework/angel_framework.dart'));
+ generator.applyToLibrary(serviceLib, name, rc.snakeCase);
+ }
+
+ if (argResults['typed'] as bool) {
+ serviceLib.directives
+ .add(new Directive.import('../models/${rc.snakeCase}.dart'));
+ }
+
+ // configureServer() {}
+ serviceLib.body.add(new Method((configureServer) {
+ configureServer
+ ..name = 'configureServer'
+ ..returns = refer('AngelConfigurer');
+
+ configureServer.body = new Block((block) {
+ generator.applyToConfigureServer(
+ serviceLib, configureServer, block, name, rc.snakeCase);
+
+ // return (Angel app) async {}
+ var closure = new Method((closure) {
+ closure
+ ..modifier = MethodModifier.async
+ ..requiredParameters.add(new Parameter((b) => b
+ ..name = 'app'
+ ..type = refer('Angel')));
+ closure.body = new Block((block) {
+ generator.beforeService(serviceLib, block, name, rc.snakeCase);
+
+ // app.use('/api/todos', new MapService());
+ var service = generator.createInstance(
+ serviceLib, closure, name, rc.snakeCase);
+
+ if (argResults['typed'] as bool) {
+ var tb = new TypeReference((b) => b
+ ..symbol = 'TypedService'
+ ..types.add(refer(rc.pascalCase)));
+ service = tb.newInstance([service]);
+ }
+
+ block.addExpression(refer('app').property('use').call([
+ literal('/api/${pluralize(rc.snakeCase)}'),
+ service,
+ ]));
+ });
+ });
+
+ block.addExpression(closure.closure.returned);
+ });
+ }));
+ });
+
+ final outputDir = new Directory.fromUri(
+ Directory.current.uri.resolve(argResults['output-dir'] as String));
+ final serviceFile =
+ new File.fromUri(outputDir.uri.resolve("${rc.snakeCase}.dart"));
+ if (!await serviceFile.exists()) await serviceFile.create(recursive: true);
+ await serviceFile.writeAsString(new DartFormatter()
+ .format(serviceLib.accept(new DartEmitter()).toString()));
+
+ print(green.wrap(
+ '$checkmark Successfully generated service file "${serviceFile.absolute.path}".'));
+
+ if (deps.isNotEmpty) await depend(deps);
+ }
+}
diff --git a/packages/cli/lib/src/commands/make/test.dart b/packages/cli/lib/src/commands/make/test.dart
new file mode 100644
index 00000000..32adcd7f
--- /dev/null
+++ b/packages/cli/lib/src/commands/make/test.dart
@@ -0,0 +1,110 @@
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompter;
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:recase/recase.dart';
+import '../../util.dart';
+import 'maker.dart';
+
+class TestCommand extends Command {
+ @override
+ String get name => "test";
+
+ @override
+ String get description => "Creates a new test within the given project.";
+
+ TestCommand() {
+ argParser
+ ..addFlag('run-configuration',
+ help: 'Generate a run configuration for JetBrains IDE\'s.',
+ defaultsTo: true)
+ ..addOption('name',
+ abbr: 'n', help: 'Specifies a name for the plug-in class.')
+ ..addOption('output-dir',
+ help: 'Specifies a directory to create the plug-in class in.',
+ defaultsTo: 'test');
+ }
+
+ @override
+ run() async {
+ var pubspec = await loadPubspec();
+ String name;
+ if (argResults.wasParsed('name')) name = argResults['name'] as String;
+
+ if (name?.isNotEmpty != true) {
+ name = prompter.get('Name of test');
+ }
+
+ List deps = [
+ const MakerDependency('angel_framework', '^2.0.0'),
+ const MakerDependency('angel_test', '^2.0.0', dev: true),
+ const MakerDependency('test', '^1.0.0', dev: true),
+ ];
+
+ var rc = new ReCase(name);
+ final testDir = new Directory.fromUri(
+ Directory.current.uri.resolve(argResults['output-dir'] as String));
+ final testFile =
+ new File.fromUri(testDir.uri.resolve("${rc.snakeCase}_test.dart"));
+ if (!await testFile.exists()) await testFile.create(recursive: true);
+ await testFile
+ .writeAsString(new DartFormatter().format(_generateTest(pubspec, rc)));
+
+ if (deps.isNotEmpty) await depend(deps);
+
+ print(green.wrap(
+ '$checkmark Successfully generated test file "${testFile.absolute.path}".'));
+
+ if (argResults['run-configuration'] as bool) {
+ final runConfig = new File.fromUri(Directory.current.uri
+ .resolve('.idea/runConfigurations/${name}_Tests.xml'));
+
+ if (!await runConfig.exists()) await runConfig.create(recursive: true);
+ await runConfig.writeAsString(_generateRunConfiguration(name, rc));
+
+ print(green.wrap(
+ '$checkmark Successfully generated run configuration "$name Tests" at "${runConfig.absolute.path}".'));
+ }
+ }
+
+ String _generateRunConfiguration(String name, ReCase rc) {
+ return '''
+
+
+
+
+
+
+'''
+ .trim();
+ }
+
+ String _generateTest(Pubspec pubspec, ReCase rc) {
+ return '''
+import 'dart:io';
+import 'package:${pubspec.name}/${pubspec.name}.dart' as ${pubspec.name};
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:test/test.dart';
+
+main() async {
+ TestClient client;
+
+ setUp(() async {
+ var app = new Angel();
+ await app.configure(${pubspec.name}.configureServer);
+ client = await connectTo(app);
+ });
+
+ tearDown(() => client.close());
+
+ test('${rc.snakeCase}', () async {
+ final response = await client.get('/${rc.snakeCase}');
+ expect(response, hasStatus(HttpStatus.ok));
+ });
+}
+ ''';
+ }
+}
diff --git a/packages/cli/lib/src/commands/pub.dart b/packages/cli/lib/src/commands/pub.dart
new file mode 100644
index 00000000..1f3a74ce
--- /dev/null
+++ b/packages/cli/lib/src/commands/pub.dart
@@ -0,0 +1,12 @@
+import 'dart:io';
+
+final RegExp _leadingSlashes = new RegExp(r'^/+');
+
+String resolvePub() {
+ var exec = new File(Platform.resolvedExecutable);
+ var pubPath = exec.parent.uri.resolve('pub').path;
+ if (Platform.isWindows)
+ pubPath = pubPath.replaceAll(_leadingSlashes, '') + '.bat';
+ pubPath = Uri.decodeFull(pubPath);
+ return pubPath;
+}
diff --git a/packages/cli/lib/src/commands/rename.dart b/packages/cli/lib/src/commands/rename.dart
new file mode 100644
index 00000000..ae11b5e5
--- /dev/null
+++ b/packages/cli/lib/src/commands/rename.dart
@@ -0,0 +1,183 @@
+import 'dart:io';
+import 'package:analyzer/analyzer.dart';
+import 'package:args/command_runner.dart';
+import 'package:dart_style/dart_style.dart';
+import 'package:glob/glob.dart';
+import 'package:io/ansi.dart';
+import 'package:prompts/prompts.dart' as prompts;
+import 'package:recase/recase.dart';
+import '../util.dart';
+import 'pub.dart';
+
+class RenameCommand extends Command {
+ @override
+ String get name => 'rename';
+
+ @override
+ String get description => 'Renames the current project.';
+
+ @override
+ String get invocation => '$name ';
+
+ @override
+ run() async {
+ String newName;
+
+ if (argResults.rest.isNotEmpty)
+ newName = argResults.rest.first;
+ else {
+ newName = prompts.get('Rename project to');
+ }
+
+ newName = new ReCase(newName).snakeCase;
+
+ var choice = prompts.getBool('Rename the project to `$newName`?');
+
+ if (choice) {
+ print('Renaming project to `$newName`...');
+ var pubspecFile =
+ new File.fromUri(Directory.current.uri.resolve('pubspec.yaml'));
+
+ if (!await pubspecFile.exists()) {
+ throw new Exception('No pubspec.yaml found in current directory.');
+ } else {
+ var pubspec = await loadPubspec();
+ var oldName = pubspec.name;
+ await renamePubspec(Directory.current, oldName, newName);
+ await renameDartFiles(Directory.current, oldName, newName);
+ print('Now running `pub get`...');
+ var pubPath = resolvePub();
+ print('Pub path: $pubPath');
+ var pub = await Process.start(pubPath, ['get']);
+ stdout.addStream(pub.stdout);
+ stderr.addStream(pub.stderr);
+ await pub.exitCode;
+ }
+ }
+ }
+}
+
+renamePubspec(Directory dir, String oldName, String newName) async {
+// var pubspec = await loadPubspec(dir);
+ print(cyan.wrap('Renaming your project to `$newName.`'));
+
+ var pubspecFile = new File.fromUri(dir.uri.resolve('pubspec.yaml'));
+
+ if (await pubspecFile.exists()) {
+ var contents = await pubspecFile.readAsString(), oldContents = contents;
+ contents =
+ contents.replaceAll(new RegExp('name:\\s*$oldName'), 'name: $newName');
+
+ if (contents != oldContents) {
+ await pubspecFile.writeAsString(contents);
+ }
+ }
+
+// print(cyan
+// .wrap('Note that this does not actually modify your `pubspec.yaml`.'));
+// TODO: https://github.com/dart-lang/pubspec_parse/issues/17
+// var newPubspec = new Pubspec.fromJson(pubspec.toJson()..['name'] = newName);
+// await newPubspec.save(dir);
+}
+
+renameDartFiles(Directory dir, String oldName, String newName) async {
+ if (!await dir.exists()) return;
+
+ // Try to replace MongoDB URL
+ var configGlob = new Glob('config/**/*.yaml');
+
+ try {
+ await for (var yamlFile in configGlob.list(root: dir.absolute.path)) {
+ if (yamlFile is File) {
+ print(
+ 'Replacing occurrences of "$oldName" with "$newName" in file "${yamlFile.absolute.path}"...');
+ var contents = await yamlFile.readAsString();
+ contents = contents.replaceAll(oldName, newName);
+ await yamlFile.writeAsString(contents);
+ }
+ }
+ } catch (_) {}
+
+ var entry = new File.fromUri(dir.uri.resolve('lib/$oldName.dart'));
+
+ if (await entry.exists()) {
+ await entry.rename(dir.uri.resolve('lib/$newName.dart').toFilePath());
+ print('Renaming library file `${entry.absolute.path}`...');
+ }
+
+ var fmt = new DartFormatter();
+ await for (FileSystemEntity file in dir.list(recursive: true)) {
+ if (file is File && file.path.endsWith('.dart')) {
+ var contents = await file.readAsString();
+ var ast = parseCompilationUnit(contents);
+ var visitor = new RenamingVisitor(oldName, newName)
+ ..visitCompilationUnit(ast);
+
+ if (visitor.replace.isNotEmpty) {
+ visitor.replace.forEach((range, replacement) {
+ if (range.first is int) {
+ contents = contents.replaceRange(
+ range.first as int, range.last as int, replacement);
+ } else if (range.first is String) {
+ contents = contents.replaceAll(range.first as String, replacement);
+ }
+ });
+
+ await file.writeAsString(fmt.format(contents));
+ print('Updated file `${file.absolute.path}`.');
+ }
+ }
+ }
+}
+
+class RenamingVisitor extends RecursiveAstVisitor {
+ final String oldName, newName;
+ final Map replace = {};
+
+ RenamingVisitor(this.oldName, this.newName) {
+ replace[['{{$oldName}}']] = newName;
+ }
+
+ String updateUri(String uri) {
+ if (uri == 'package:$oldName/$oldName.dart') {
+ return 'package:$newName/$newName.dart';
+ } else if (uri.startsWith('package:$oldName/')) {
+ return 'package:$newName/' + uri.replaceFirst('package:$oldName/', '');
+ } else
+ return uri;
+ }
+
+ @override
+ visitExportDirective(ExportDirective ctx) {
+ var uri = ctx.uri.stringValue, updated = updateUri(uri);
+ if (uri != updated) replace[[uri]] = updated;
+ }
+
+ @override
+ visitImportDirective(ImportDirective ctx) {
+ var uri = ctx.uri.stringValue, updated = updateUri(uri);
+ if (uri != updated) replace[[uri]] = updated;
+ }
+
+ @override
+ visitLibraryDirective(LibraryDirective ctx) {
+ var name = ctx.name.name;
+
+ if (name.startsWith(oldName)) {
+ replace[[ctx.offset, ctx.end]] =
+ 'library ' + name.replaceFirst(oldName, newName) + ';';
+ }
+ }
+
+ @override
+ visitPartOfDirective(PartOfDirective ctx) {
+ if (ctx.libraryName != null) {
+ var name = ctx.libraryName.name;
+
+ if (name.startsWith(oldName)) {
+ replace[[ctx.offset, ctx.end]] =
+ 'part of ' + name.replaceFirst(oldName, newName) + ';';
+ }
+ }
+ }
+}
diff --git a/packages/cli/lib/src/commands/service_generators/custom.dart b/packages/cli/lib/src/commands/service_generators/custom.dart
new file mode 100644
index 00000000..d4134602
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/custom.dart
@@ -0,0 +1,27 @@
+import 'package:code_builder/code_builder.dart';
+import 'generator.dart';
+
+class CustomServiceGenerator extends ServiceGenerator {
+ @override
+ bool get createsModel => false;
+
+ @override
+ bool get createsValidator => false;
+
+ const CustomServiceGenerator() : super('Custom');
+
+ @override
+ void applyToLibrary(LibraryBuilder library, String name, String lower) {
+ library.body.add(new Class((clazz) {
+ clazz
+ ..name = '${name}Service'
+ ..extend = refer('Service');
+ }));
+ }
+
+ @override
+ Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
+ String name, String lower) {
+ return refer('${name}Service').newInstance([]);
+ }
+}
diff --git a/packages/cli/lib/src/commands/service_generators/file_service.dart b/packages/cli/lib/src/commands/service_generators/file_service.dart
new file mode 100644
index 00000000..39e1086e
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/file_service.dart
@@ -0,0 +1,48 @@
+import 'generator.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:inflection2/inflection2.dart';
+import '../make/maker.dart';
+
+class FileServiceGenerator extends ServiceGenerator {
+ const FileServiceGenerator() : super('Persistent JSON File');
+
+ @override
+ List get dependencies =>
+ const [const MakerDependency('angel_file_service', '^2.0.0')];
+
+ @override
+ bool get goesFirst => true;
+
+ @override
+ void applyToConfigureServer(
+ LibraryBuilder library,
+ MethodBuilder configureServer,
+ BlockBuilder block,
+ String name,
+ String lower) {
+ configureServer.requiredParameters.add(new Parameter((b) => b
+ ..name = 'dbDirectory'
+ ..type = refer('Directory')));
+ }
+
+ @override
+ void applyToLibrary(LibraryBuilder library, String name, String lower) {
+ library.directives.addAll([
+ new Directive.import(
+ 'package:angel_file_service/angel_file_service.dart'),
+ ]);
+ }
+
+ @override
+ Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
+ String name, String lower) {
+ library.directives.addAll([
+ new Directive.import('package:file/file.dart'),
+ ]);
+ return refer('JsonFileService').newInstance([
+ refer('dbDirectory')
+ .property('childFile')
+ .call([literal(pluralize(lower) + '_db.json')])
+ ]);
+ }
+}
diff --git a/packages/cli/lib/src/commands/service_generators/generator.dart b/packages/cli/lib/src/commands/service_generators/generator.dart
new file mode 100644
index 00000000..63289581
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/generator.dart
@@ -0,0 +1,46 @@
+import 'package:code_builder/code_builder.dart';
+import '../make/maker.dart';
+
+class ServiceGenerator {
+ final String name;
+
+ const ServiceGenerator(this.name);
+
+ List get dependencies => [];
+
+ @deprecated
+ bool get createsModel => true;
+
+ @deprecated
+ bool get createsValidator => true;
+
+ @deprecated
+ bool get exportedInServiceLibrary => true;
+
+ @deprecated
+ bool get injectsSingleton => false;
+
+ @deprecated
+ bool get shouldRunBuild => false;
+
+ bool get goesFirst => false;
+
+ void applyToLibrary(LibraryBuilder library, String name, String lower) {}
+
+ void beforeService(LibraryBuilder library, BlockBuilder builder, String name,
+ String lower) {}
+
+ void applyToConfigureServer(
+ LibraryBuilder library,
+ MethodBuilder configureServer,
+ BlockBuilder block,
+ String name,
+ String lower) {}
+
+ Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
+ String name, String lower) =>
+ literal(null);
+
+ @override
+ String toString() => name;
+}
diff --git a/packages/cli/lib/src/commands/service_generators/map.dart b/packages/cli/lib/src/commands/service_generators/map.dart
new file mode 100644
index 00000000..545961df
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/map.dart
@@ -0,0 +1,15 @@
+import 'generator.dart';
+import 'package:code_builder/code_builder.dart';
+
+class MapServiceGenerator extends ServiceGenerator {
+ const MapServiceGenerator() : super('In-Memory');
+
+ @override
+ bool get createsModel => false;
+
+ @override
+ Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
+ String name, String lower) {
+ return refer('MapService').newInstance([]);
+ }
+}
diff --git a/packages/cli/lib/src/commands/service_generators/mongo.dart b/packages/cli/lib/src/commands/service_generators/mongo.dart
new file mode 100644
index 00000000..d8d2f0d3
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/mongo.dart
@@ -0,0 +1,43 @@
+import 'generator.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:inflection2/inflection2.dart';
+import '../make/maker.dart';
+
+class MongoServiceGenerator extends ServiceGenerator {
+ const MongoServiceGenerator() : super('MongoDB');
+
+ @override
+ List get dependencies =>
+ const [const MakerDependency('angel_mongo', '^2.0.0')];
+
+ @override
+ bool get createsModel => false;
+
+ @override
+ void applyToConfigureServer(
+ LibraryBuilder library,
+ MethodBuilder configureServer,
+ BlockBuilder block,
+ String name,
+ String lower) {
+ configureServer.requiredParameters.add(new Parameter((b) => b
+ ..name = 'db'
+ ..type = refer('Db')));
+ }
+
+ @override
+ void applyToLibrary(LibraryBuilder library, String name, String lower) {
+ library.directives.addAll([
+ new Directive.import('package:angel_mongo/angel_mongo.dart'),
+ new Directive.import('package:mongo_dart/mongo_dart.dart'),
+ ]);
+ }
+
+ @override
+ Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
+ String name, String lower) {
+ return refer('MongoService').newInstance([
+ refer('db').property('collection').call([literal(pluralize(lower))])
+ ]);
+ }
+}
diff --git a/packages/cli/lib/src/commands/service_generators/rethink.dart b/packages/cli/lib/src/commands/service_generators/rethink.dart
new file mode 100644
index 00000000..57059844
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/rethink.dart
@@ -0,0 +1,49 @@
+import 'generator.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:inflection2/inflection2.dart';
+import '../make/maker.dart';
+
+class RethinkServiceGenerator extends ServiceGenerator {
+ const RethinkServiceGenerator() : super('RethinkDB');
+
+ @override
+ List get dependencies =>
+ const [const MakerDependency('angel_rethink', '^2.0.0')];
+
+ @override
+ bool get createsModel => false;
+
+ @override
+ void applyToConfigureServer(
+ LibraryBuilder library,
+ MethodBuilder configureServer,
+ BlockBuilder block,
+ String name,
+ String lower) {
+ configureServer.requiredParameters.addAll([
+ new Parameter((b) => b
+ ..name = 'connection'
+ ..type = refer('Connection')),
+ new Parameter((b) => b
+ ..name = 'r'
+ ..type = refer('Rethinkdb')),
+ ]);
+ }
+
+ @override
+ void applyToLibrary(LibraryBuilder library, String name, String lower) {
+ library.directives.addAll([
+ 'package:angel_rethink/angel_rethink.dart',
+ 'package:rethinkdb_driver/rethinkdb_driver.dart'
+ ].map((str) => new Directive.import(str)));
+ }
+
+ @override
+ Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
+ String name, String lower) {
+ return refer('RethinkService').newInstance([
+ refer('connection'),
+ refer('r').property('table').call([literal(pluralize(lower))])
+ ]);
+ }
+}
diff --git a/packages/cli/lib/src/commands/service_generators/service_generators.dart b/packages/cli/lib/src/commands/service_generators/service_generators.dart
new file mode 100644
index 00000000..813adc18
--- /dev/null
+++ b/packages/cli/lib/src/commands/service_generators/service_generators.dart
@@ -0,0 +1,15 @@
+import 'custom.dart';
+import 'file_service.dart';
+import 'generator.dart';
+import 'map.dart';
+import 'mongo.dart';
+import 'rethink.dart';
+export 'generator.dart';
+
+const List serviceGenerators = const [
+ const MapServiceGenerator(),
+ const FileServiceGenerator(),
+ const MongoServiceGenerator(),
+ const RethinkServiceGenerator(),
+ const CustomServiceGenerator()
+];
diff --git a/packages/cli/lib/src/random_string.dart b/packages/cli/lib/src/random_string.dart
new file mode 100644
index 00000000..c089e870
--- /dev/null
+++ b/packages/cli/lib/src/random_string.dart
@@ -0,0 +1,15 @@
+import 'dart:math';
+
+const String _valid =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+final Random _rnd = new Random.secure();
+
+String randomAlphaNumeric(int length) {
+ var b = new StringBuffer();
+
+ for (int i = 0; i < length; i++) {
+ b.writeCharCode(_valid.codeUnitAt(_rnd.nextInt(_valid.length)));
+ }
+
+ return b.toString();
+}
diff --git a/packages/cli/lib/src/util.dart b/packages/cli/lib/src/util.dart
new file mode 100644
index 00000000..a294d40c
--- /dev/null
+++ b/packages/cli/lib/src/util.dart
@@ -0,0 +1,77 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:io/ansi.dart';
+import 'package:path/path.dart' as p;
+import 'package:pubspec_parse/pubspec_parse.dart';
+//import 'package:yamlicious/yamlicious.dart';
+
+final String checkmark = ansiOutputEnabled ? '\u2714' : '[Success]';
+
+final String ballot = ansiOutputEnabled ? '\u2717' : '[Failure]';
+
+String get homeDirPath =>
+ Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
+
+Directory get homeDir => new Directory(homeDirPath);
+
+Directory get angelDir => Directory(p.join(homeDir.path, '.angel'));
+
+Future loadPubspec([Directory directory]) {
+ directory ??= Directory.current;
+ var file = new File.fromUri(directory.uri.resolve('pubspec.yaml'));
+ return file
+ .readAsString()
+ .then((yaml) => new Pubspec.parse(yaml, sourceUrl: file.uri));
+}
+
+// From: https://gist.github.com/tobischw/98dcd2563eec9a2a87bda8299055358a
+Future copyDirectory(Directory source, Directory destination) async {
+ // if (!topLevel) stdout.write('\r');
+ // print(darkGray
+ // .wrap('Copying dir "${source.path}" -> "${destination.path}..."'));
+
+ await for (var entity in source.list(recursive: false)) {
+ if (p.basename(entity.path) == '.git') continue;
+ if (entity is Directory) {
+ var newDirectory =
+ Directory(p.join(destination.absolute.path, p.basename(entity.path)));
+ await newDirectory.create(recursive: true);
+ await copyDirectory(entity.absolute, newDirectory);
+ } else if (entity is File) {
+ var newPath = p.join(destination.path, p.basename(entity.path));
+ // print(darkGray.wrap('\rCopying file "${entity.path}" -> "$newPath"'));
+ await File(newPath).create(recursive: true);
+ await entity.copy(newPath);
+ }
+ }
+
+ // print('\rCopied "${source.path}" -> "${destination.path}.');
+}
+
+Future savePubspec(Pubspec pubspec) async {
+ // TODO: Save pubspec for real?
+ //var text = toYamlString(pubspec);
+}
+
+Future runCommand(String exec, List args) async {
+ var s = '$exec ${args.join(' ')}'.trim();
+ stdout.write(darkGray.wrap('Running `$s`... '));
+
+ try {
+ var p = await Process.start(exec, args);
+ var code = await p.exitCode;
+
+ if (code == 0) {
+ print(green.wrap(checkmark));
+ return true;
+ } else {
+ print(red.wrap(ballot));
+ await stdout.addStream(p.stdout);
+ await stderr.addStream(p.stderr);
+ return false;
+ }
+ } catch (e) {
+ print(red.wrap('$ballot Failed to run process.'));
+ return false;
+ }
+}
diff --git a/packages/cli/pubspec.yaml b/packages/cli/pubspec.yaml
new file mode 100644
index 00000000..34ab6e6c
--- /dev/null
+++ b/packages/cli/pubspec.yaml
@@ -0,0 +1,28 @@
+author: Tobe O
+description: Command-line tools for the Angel framework, including scaffolding.
+homepage: https://github.com/angel-dart/angel_cli
+name: angel_cli
+version: 2.1.7+1
+dependencies:
+ analyzer: ">=0.32.0 <2.0.0"
+ args: ^1.0.0
+ code_builder: ^3.0.0
+ dart_style: ^1.0.0
+ glob: ^1.1.0
+ http: ^0.12.0
+ io: ^0.3.2
+ inflection2: ^0.4.2
+ mustache4dart: ^3.0.0-dev.1.0
+ path: ^1.0.0
+ prompts: ^1.0.0
+ pubspec_parse: ^0.1.2
+ quiver: ^2.0.0
+ recase: ^2.0.0
+ shutdown: ^0.4.0
+ watcher: ^0.9.7
+ yaml: ^2.0.0
+ #yamlicious: ^0.0.5
+environment:
+ sdk: ">=2.0.0-dev <3.0.0"
+executables:
+ angel: angel
diff --git a/packages/cli/screenshots/screenshot.png b/packages/cli/screenshots/screenshot.png
new file mode 100644
index 00000000..fe8c9652
Binary files /dev/null and b/packages/cli/screenshots/screenshot.png differ