{"action":"created","comment":{"url":"https://api.github.com/repos/joshwcomeau/guppy/pulls/comments/213518121","pull_request_review_id":150365302,"id":213518121,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxMzUxODEyMQ==","diff_hunk":"@@ -0,0 +1,339 @@\n+// @flow\n+import { select, call, put, take, takeEvery } from 'redux-saga/effects';\n+import { buffers, eventChannel, END } from 'redux-saga';\n+import { ipcRenderer } from 'electron';\n+import * as childProcess from 'child_process';\n+import * as path from 'path';\n+import chalkRaw from 'chalk';\n+\n+import {\n+  RUN_TASK,\n+  ABORT_TASK,\n+  COMPLETE_TASK,\n+  LAUNCH_DEV_SERVER,\n+  completeTask,\n+  attachTaskMetadata,\n+  receiveDataFromTaskExecution,\n+  loadDependencyInfoFromDisk,\n+} from '../actions';\n+import { getProjectById } from '../reducers/projects.reducer';\n+import { getPathForProjectId } from '../reducers/paths.reducer';\n+import { isDevServerTask } from '../reducers/tasks.reducer';\n+import findAvailablePort from '../services/find-available-port.service';\n+import killProcessId from '../services/kill-process-id.service';\n+import { isWin, PACKAGE_MANAGER_CMD } from '../services/platform.service';\n+\n+import type { Task, ProjectType } from '../types';\n+import type { Saga } from 'redux-saga';\n+\n+const chalk = new chalkRaw.constructor({ level: 3 });\n+\n+export function* launchDevServer({ task }: { task: Task }): Saga<void> {\n+  const project = yield select(getProjectById, task.projectId);\n+  const projectPath = yield select(getPathForProjectId, task.projectId);\n+\n+  try {\n+    const port = yield call(findAvailablePort);\n+    const { args, env } = yield call(\n+      getDevServerCommand,\n+      task,\n+      project.type,\n+      port\n+    );\n+\n+    const child = yield call(\n+      [childProcess, childProcess.spawn],\n+      PACKAGE_MANAGER_CMD,\n+      args,\n+      {\n+        cwd: projectPath,\n+        env: { ...getBaseProjectEnvironment(projectPath), ...env },\n+      }\n+    );\n+\n+    // Now that we have a port/processId for the server, attach it to\n+    // the task. The port is used for opening the app, the pid is used\n+    // to kill the process\n+    yield put(attachTaskMetadata(task, child.pid, port));\n+\n+    yield call([ipcRenderer, ipcRenderer.send], 'addProcessId', child.pid);\n+\n+    const stdioChannel = createStdioChannel(child, {\n+      stdout: emitter => data => {\n+        // Ok so, unfortunately, failure-to-compile is still pushed\n+        // through stdout, not stderr. We want that message specifically\n+        // to trigger an error state, and so we need to parse it.\n+        const text = data.toString();\n+\n+        const isError = text.includes('Failed to compile');\n+\n+        emitter({ channel: 'stdout', text, isError });\n+      },\n+      stderr: emitter => data => {\n+        emitter({ channel: 'stderr', text: data.toString() });\n+      },\n+      exit: emitter => code => {\n+        // For Windows Support\n+        // Windows sends code 1 (I guess its because we force kill??)\n+        const successfulCode = isWin ? 1 : 0;\n+        const wasSuccessful = code === successfulCode || code === null;\n+        const timestamp = new Date();\n+\n+        emitter({ channel: 'exit', timestamp, wasSuccessful });\n+        // calling emitter(END) will break out of the try block of any\n+        // actively listening subscribers when they take() it\n+        emitter(END);\n+      },\n+    });\n+\n+    try {\n+      while (true) {\n+        const message = yield take(stdioChannel);\n+\n+        // eslint-disable-next-line default-case\n+        switch (message.channel) {\n+          case 'stdout':\n+            yield put(\n+              receiveDataFromTaskExecution(task, message.text, message.isError)\n+            );\n+            break;\n+          case 'stderr':\n+            yield put(receiveDataFromTaskExecution(task, message.text));\n+            break;\n+          case 'exit':\n+            yield call(displayTaskComplete, task);\n+            yield put(\n+              completeTask(task, message.timestamp, message.wasSuccessful)\n+            );\n+            break;\n+        }\n+      }\n+    } finally {\n+      /* child process has completed */\n+    }\n+  } catch (err) {\n+    // TODO: Error handling (this can happen if the first 15 ports are occupied,\n+    // or if there's some generic Node error\n+    console.error(err);\n+  }\n+}\n+\n+export function* taskRun({ task }: { task: Task }): Saga<void> {\n+  const project = yield select(getProjectById, task.projectId);\n+  const projectPath = yield select(getPathForProjectId, task.projectId);\n+  const { name } = task;\n+\n+  // TEMPORARY HACK\n+  // By default, create-react-app runs tests in interactive watch mode.\n+  // This is a brilliant way to do it, but it's interactive, which won't\n+  // work as-is.\n+  // In the future, I expect \"Tests\" to get its own module on the project\n+  // page, in which case we can support the interactive mode, except with\n+  // descriptive buttons instead of cryptic letters!\n+  // Alas, this would be mucho work, and this is an MVP. So for now, I'm\n+  // disabling watch mode, and doing \"just run all the tests once\" mode.\n+  // This is bad, and I feel bad, but it's a corner that needs to be cut,\n+  // for now.\n+  const additionalArgs = [];\n+  if (project.type === 'create-react-app' && name === 'test') {\n+    additionalArgs.push('--coverage');\n+  }\n+\n+  const child = yield call(\n+    [childProcess, childProcess.spawn],\n+    PACKAGE_MANAGER_CMD,\n+    ['run', name, ...additionalArgs],\n+    {\n+      cwd: projectPath,\n+      env: getBaseProjectEnvironment(projectPath),\n+    }\n+  );\n+\n+  // TODO: Does the renderer process still need to know about the child\n+  // processId?\n+  yield put(attachTaskMetadata(task, child.pid));\n+\n+  // When this application exits, we want to kill this process.\n+  // Send it up to the main process.\n+  yield call([ipcRenderer, ipcRenderer.send], 'addProcessId', child.pid);\n+\n+  const stdioChannel = createStdioChannel(child, {\n+    stdout: emitter => data => {\n+      // The 'eject' task prompts the user, to ask if they're sure.\n+      // We can bypass this prompt, as our UI already has an alert that\n+      // confirms this action.\n+      // TODO: Eject deserves its own Redux action, to avoid cluttering up\n+      // this generic \"RUN_TASK\" action.\n+      // TODO: Is there a way to \"future-proof\" this, in case the CRA\n+      // confirmation copy changes?\n+      const isEjectPrompt = data\n+        .toString()\n+        .includes('Are you sure you want to eject? This action is permanent');\n+\n+      if (isEjectPrompt) {\n+        sendCommandToProcess(child, 'y');\n+      }\n+\n+      emitter({ channel: 'stdout', text: data.toString() });\n+    },\n+    stderr: emitter => data => {\n+      emitter({ channel: 'stderr', text: data.toString() });\n+    },\n+    exit: emitter => code => {\n+      const timestamp = new Date();\n+\n+      emitter({ channel: 'exit', timestamp, wasSuccessful: code === 0 });\n+      emitter(END);\n+    },\n+  });\n+\n+  try {\n+    while (true) {\n+      const message = yield take(stdioChannel);\n+\n+      // eslint-disable-next-line default-case\n+      switch (message.channel) {\n+        case 'stdout':\n+          yield put(receiveDataFromTaskExecution(task, message.text));\n+          break;\n+        case 'stderr':\n+          yield put(receiveDataFromTaskExecution(task, message.text));\n+          break;\n+        case 'exit':\n+          yield call(displayTaskComplete, task);\n+          yield put(\n+            completeTask(task, message.timestamp, message.wasSuccessful)\n+          );\n+          if (task.name === 'eject') {\n+            yield put(loadDependencyInfoFromDisk(project.id, project.path));\n+          }\n+          break;\n+      }\n+    }\n+  } finally {\n+    /* child process has completed */\n+  }\n+}\n+\n+export function* taskAbort({ task }: { task: Task }): Saga<void> {\n+  const { processId, name } = task;\n+\n+  yield call(killProcessId, processId);\n+  yield call([ipcRenderer, ipcRenderer.send], 'removeProcessId', processId);\n+\n+  // Once the children are killed, we should dispatch a notification\n+  // so that the terminal shows something about this update.\n+  // My initial thought was that all tasks would have the same message,\n+  // but given that we're treating `start` as its own special thing,\n+  // I'm realizing that it should vary depending on the task type.\n+  // TODO: Find a better place for this to live.\n+  const abortMessage = isDevServerTask(name)\n+    ? 'Server stopped'\n+    : 'Task aborted';\n+\n+  yield put(receiveDataFromTaskExecution(task, chalk.bold.red(abortMessage)));\n+}\n+\n+export function* displayTaskComplete(task: Task): Saga<void> {\n+  // Send a message to add info to the terminal about the task being done.\n+  // TODO: ASCII fish art?\n+\n+  const message = 'Task completed';\n+\n+  yield put(receiveDataFromTaskExecution(task, chalk.bold.green(message)));\n+}\n+\n+export function* taskComplete({ task }: { task: Task }): Saga<void> {\n+  if (task.processId) {\n+    yield call(\n+      [ipcRenderer, ipcRenderer.send],\n+      'removeProcessId',\n+      task.processId\n+    );\n+  }\n+\n+  // The `eject` task is special; after running it, its dependencies will\n+  // have changed.\n+  // TODO: We should really have a `EJECT_PROJECT_COMPLETE` action that does\n+  // this instead.\n+  if (task.name === 'eject') {\n+    const project = yield select(getProjectById, task.projectId);\n+\n+    yield put(loadDependencyInfoFromDisk(project.id, project.path));\n+  }\n+}\n+\n+const createStdioChannel = (\n+  child: any,\n+  handlers: {\n+    stdout: (emitter: any) => (data: string) => void,\n+    stderr: (emitter: any) => (data: string) => void,\n+    exit: (emitter: any) => (code: number) => void,\n+  }\n+) => {\n+  return eventChannel(emitter => {\n+    child.stdout.on('data', handlers.stdout(emitter));\n+    child.stderr.on('data', handlers.stderr(emitter));\n+    child.on('exit', handlers.exit(emitter));\n+\n+    return () => {\n+      /* unsubscribe any listeners */\n+    };\n+\n+    // use an expanding buffer because we don't want to lose any information\n+    // passed up by the child process. initialize it at a length of 2 because\n+    // at bare minimum we expect to have 2 messages queued at some point (as\n+    // the exit channel completes, it should emit the return code of the process\n+    // and then immediately END\n+  }, buffers.expanding(2));\n+};\n+\n+export const getBaseProjectEnvironment = (projectPath: string) => ({\n+  // Forward the host env, and append the\n+  // project's .bin directory to PATH to allow\n+  // package scripts to function properly.\n+  ...window.process.env,\n+  PATH:\n+    window.process.env.PATH +\n+    path.delimiter +\n+    path.join(projectPath, 'node_modules', '.bin'),\n+});","path":"src/sagas/task.saga.js","position":300,"original_position":300,"commit_id":"8158a21902901d4e90b8787f4f2f37cd60d2b292","original_commit_id":"8158a21902901d4e90b8787f4f2f37cd60d2b292","user":{"login":"superhawk610","id":18172185,"node_id":"MDQ6VXNlcjE4MTcyMTg1","avatar_url":"https://avatars1.githubusercontent.com/u/18172185?v=4","gravatar_id":"","url":"https://api.github.com/users/superhawk610","html_url":"https://github.com/superhawk610","followers_url":"https://api.github.com/users/superhawk610/followers","following_url":"https://api.github.com/users/superhawk610/following{/other_user}","gists_url":"https://api.github.com/users/superhawk610/gists{/gist_id}","starred_url":"https://api.github.com/users/superhawk610/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/superhawk610/subscriptions","organizations_url":"https://api.github.com/users/superhawk610/orgs","repos_url":"https://api.github.com/users/superhawk610/repos","events_url":"https://api.github.com/users/superhawk610/events{/privacy}","received_events_url":"https://api.github.com/users/superhawk610/received_events","type":"User","site_admin":false},"body":"Yeah that definitely makes more sense.","created_at":"2018-08-29T01:08:33Z","updated_at":"2018-08-29T01:30:17Z","html_url":"https://github.com/joshwcomeau/guppy/pull/154#discussion_r213518121","pull_request_url":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/joshwcomeau/guppy/pulls/comments/213518121"},"html":{"href":"https://github.com/joshwcomeau/guppy/pull/154#discussion_r213518121"},"pull_request":{"href":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154"}},"in_reply_to_id":212613948},"pull_request":{"url":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154","id":210333508,"node_id":"MDExOlB1bGxSZXF1ZXN0MjEwMzMzNTA4","html_url":"https://github.com/joshwcomeau/guppy/pull/154","diff_url":"https://github.com/joshwcomeau/guppy/pull/154.diff","patch_url":"https://github.com/joshwcomeau/guppy/pull/154.patch","issue_url":"https://api.github.com/repos/joshwcomeau/guppy/issues/154","number":154,"state":"open","locked":false,"title":"Move middlewares to sagas","user":{"login":"superhawk610","id":18172185,"node_id":"MDQ6VXNlcjE4MTcyMTg1","avatar_url":"https://avatars1.githubusercontent.com/u/18172185?v=4","gravatar_id":"","url":"https://api.github.com/users/superhawk610","html_url":"https://github.com/superhawk610","followers_url":"https://api.github.com/users/superhawk610/followers","following_url":"https://api.github.com/users/superhawk610/following{/other_user}","gists_url":"https://api.github.com/users/superhawk610/gists{/gist_id}","starred_url":"https://api.github.com/users/superhawk610/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/superhawk610/subscriptions","organizations_url":"https://api.github.com/users/superhawk610/orgs","repos_url":"https://api.github.com/users/superhawk610/repos","events_url":"https://api.github.com/users/superhawk610/events{/privacy}","received_events_url":"https://api.github.com/users/superhawk610/received_events","type":"User","site_admin":false},"body":"**Related PR:** #97, #111, #117, #122\r\n\r\n**Summary:**\r\nIn the words of @mik639,\r\n> redux-saga helps to write flat sync code, and it also has one more advantage it's pretty testable :)\r\n\r\nInstead of watching for actions in the middle of the redux chain, redux-saga allows all side-effects to be managed independently at the end of the chain. Sagas leverage ES6 generators to allow for writing code that's both easy to read and easy to test.\r\n\r\nThis PR adds redux-saga, complete with flow typing, and replaces the 3 existing middlewares with 3 fully-tested sagas. It also includes a few minor tweaks to existing selectors and tests to account for the new syntax. Additional minor changes include moving `projects.reducer.test.js` up a level from the `tests` directory to live alongside its reducer, and adding a couple missing assertions in `create-project.service.test.js` (as well as renaming it from `create-project-service.test.js` to conform to existing naming conventions).","created_at":"2018-08-23T05:22:01Z","updated_at":"2018-08-29T01:30:17Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9bf26398f3774019b0f54139ec0a966d71576c9f","assignee":null,"assignees":[],"requested_reviewers":[{"login":"mik639","id":7389005,"node_id":"MDQ6VXNlcjczODkwMDU=","avatar_url":"https://avatars0.githubusercontent.com/u/7389005?v=4","gravatar_id":"","url":"https://api.github.com/users/mik639","html_url":"https://github.com/mik639","followers_url":"https://api.github.com/users/mik639/followers","following_url":"https://api.github.com/users/mik639/following{/other_user}","gists_url":"https://api.github.com/users/mik639/gists{/gist_id}","starred_url":"https://api.github.com/users/mik639/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mik639/subscriptions","organizations_url":"https://api.github.com/users/mik639/orgs","repos_url":"https://api.github.com/users/mik639/repos","events_url":"https://api.github.com/users/mik639/events{/privacy}","received_events_url":"https://api.github.com/users/mik639/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154/commits","review_comments_url":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154/comments","review_comment_url":"https://api.github.com/repos/joshwcomeau/guppy/pulls/comments{/number}","comments_url":"https://api.github.com/repos/joshwcomeau/guppy/issues/154/comments","statuses_url":"https://api.github.com/repos/joshwcomeau/guppy/statuses/8158a21902901d4e90b8787f4f2f37cd60d2b292","head":{"label":"joshwcomeau:move-middlewares-to-sagas","ref":"move-middlewares-to-sagas","sha":"8158a21902901d4e90b8787f4f2f37cd60d2b292","user":{"login":"joshwcomeau","id":6692932,"node_id":"MDQ6VXNlcjY2OTI5MzI=","avatar_url":"https://avatars0.githubusercontent.com/u/6692932?v=4","gravatar_id":"","url":"https://api.github.com/users/joshwcomeau","html_url":"https://github.com/joshwcomeau","followers_url":"https://api.github.com/users/joshwcomeau/followers","following_url":"https://api.github.com/users/joshwcomeau/following{/other_user}","gists_url":"https://api.github.com/users/joshwcomeau/gists{/gist_id}","starred_url":"https://api.github.com/users/joshwcomeau/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshwcomeau/subscriptions","organizations_url":"https://api.github.com/users/joshwcomeau/orgs","repos_url":"https://api.github.com/users/joshwcomeau/repos","events_url":"https://api.github.com/users/joshwcomeau/events{/privacy}","received_events_url":"https://api.github.com/users/joshwcomeau/received_events","type":"User","site_admin":false},"repo":{"id":134185621,"node_id":"MDEwOlJlcG9zaXRvcnkxMzQxODU2MjE=","name":"guppy","full_name":"joshwcomeau/guppy","owner":{"login":"joshwcomeau","id":6692932,"node_id":"MDQ6VXNlcjY2OTI5MzI=","avatar_url":"https://avatars0.githubusercontent.com/u/6692932?v=4","gravatar_id":"","url":"https://api.github.com/users/joshwcomeau","html_url":"https://github.com/joshwcomeau","followers_url":"https://api.github.com/users/joshwcomeau/followers","following_url":"https://api.github.com/users/joshwcomeau/following{/other_user}","gists_url":"https://api.github.com/users/joshwcomeau/gists{/gist_id}","starred_url":"https://api.github.com/users/joshwcomeau/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshwcomeau/subscriptions","organizations_url":"https://api.github.com/users/joshwcomeau/orgs","repos_url":"https://api.github.com/users/joshwcomeau/repos","events_url":"https://api.github.com/users/joshwcomeau/events{/privacy}","received_events_url":"https://api.github.com/users/joshwcomeau/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/joshwcomeau/guppy","description":"🐠A friendly application manager and task runner for React.js","fork":false,"url":"https://api.github.com/repos/joshwcomeau/guppy","forks_url":"https://api.github.com/repos/joshwcomeau/guppy/forks","keys_url":"https://api.github.com/repos/joshwcomeau/guppy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joshwcomeau/guppy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joshwcomeau/guppy/teams","hooks_url":"https://api.github.com/repos/joshwcomeau/guppy/hooks","issue_events_url":"https://api.github.com/repos/joshwcomeau/guppy/issues/events{/number}","events_url":"https://api.github.com/repos/joshwcomeau/guppy/events","assignees_url":"https://api.github.com/repos/joshwcomeau/guppy/assignees{/user}","branches_url":"https://api.github.com/repos/joshwcomeau/guppy/branches{/branch}","tags_url":"https://api.github.com/repos/joshwcomeau/guppy/tags","blobs_url":"https://api.github.com/repos/joshwcomeau/guppy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joshwcomeau/guppy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joshwcomeau/guppy/git/refs{/sha}","trees_url":"https://api.github.com/repos/joshwcomeau/guppy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joshwcomeau/guppy/statuses/{sha}","languages_url":"https://api.github.com/repos/joshwcomeau/guppy/languages","stargazers_url":"https://api.github.com/repos/joshwcomeau/guppy/stargazers","contributors_url":"https://api.github.com/repos/joshwcomeau/guppy/contributors","subscribers_url":"https://api.github.com/repos/joshwcomeau/guppy/subscribers","subscription_url":"https://api.github.com/repos/joshwcomeau/guppy/subscription","commits_url":"https://api.github.com/repos/joshwcomeau/guppy/commits{/sha}","git_commits_url":"https://api.github.com/repos/joshwcomeau/guppy/git/commits{/sha}","comments_url":"https://api.github.com/repos/joshwcomeau/guppy/comments{/number}","issue_comment_url":"https://api.github.com/repos/joshwcomeau/guppy/issues/comments{/number}","contents_url":"https://api.github.com/repos/joshwcomeau/guppy/contents/{+path}","compare_url":"https://api.github.com/repos/joshwcomeau/guppy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joshwcomeau/guppy/merges","archive_url":"https://api.github.com/repos/joshwcomeau/guppy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joshwcomeau/guppy/downloads","issues_url":"https://api.github.com/repos/joshwcomeau/guppy/issues{/number}","pulls_url":"https://api.github.com/repos/joshwcomeau/guppy/pulls{/number}","milestones_url":"https://api.github.com/repos/joshwcomeau/guppy/milestones{/number}","notifications_url":"https://api.github.com/repos/joshwcomeau/guppy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joshwcomeau/guppy/labels{/name}","releases_url":"https://api.github.com/repos/joshwcomeau/guppy/releases{/id}","deployments_url":"https://api.github.com/repos/joshwcomeau/guppy/deployments","created_at":"2018-05-20T20:49:50Z","updated_at":"2018-08-29T01:08:06Z","pushed_at":"2018-08-29T01:08:06Z","git_url":"git://github.com/joshwcomeau/guppy.git","ssh_url":"git@github.com:joshwcomeau/guppy.git","clone_url":"https://github.com/joshwcomeau/guppy.git","svn_url":"https://github.com/joshwcomeau/guppy","homepage":"","size":12109,"stargazers_count":2284,"watchers_count":2284,"language":"JavaScript","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":100,"mirror_url":null,"archived":false,"open_issues_count":48,"license":{"key":"isc","name":"ISC License","spdx_id":"ISC","url":"https://api.github.com/licenses/isc","node_id":"MDc6TGljZW5zZTEw"},"forks":100,"open_issues":48,"watchers":2284,"default_branch":"master"}},"base":{"label":"joshwcomeau:master","ref":"master","sha":"73f30bf54a51744a15f6e6d83fcf970192a155f9","user":{"login":"joshwcomeau","id":6692932,"node_id":"MDQ6VXNlcjY2OTI5MzI=","avatar_url":"https://avatars0.githubusercontent.com/u/6692932?v=4","gravatar_id":"","url":"https://api.github.com/users/joshwcomeau","html_url":"https://github.com/joshwcomeau","followers_url":"https://api.github.com/users/joshwcomeau/followers","following_url":"https://api.github.com/users/joshwcomeau/following{/other_user}","gists_url":"https://api.github.com/users/joshwcomeau/gists{/gist_id}","starred_url":"https://api.github.com/users/joshwcomeau/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshwcomeau/subscriptions","organizations_url":"https://api.github.com/users/joshwcomeau/orgs","repos_url":"https://api.github.com/users/joshwcomeau/repos","events_url":"https://api.github.com/users/joshwcomeau/events{/privacy}","received_events_url":"https://api.github.com/users/joshwcomeau/received_events","type":"User","site_admin":false},"repo":{"id":134185621,"node_id":"MDEwOlJlcG9zaXRvcnkxMzQxODU2MjE=","name":"guppy","full_name":"joshwcomeau/guppy","owner":{"login":"joshwcomeau","id":6692932,"node_id":"MDQ6VXNlcjY2OTI5MzI=","avatar_url":"https://avatars0.githubusercontent.com/u/6692932?v=4","gravatar_id":"","url":"https://api.github.com/users/joshwcomeau","html_url":"https://github.com/joshwcomeau","followers_url":"https://api.github.com/users/joshwcomeau/followers","following_url":"https://api.github.com/users/joshwcomeau/following{/other_user}","gists_url":"https://api.github.com/users/joshwcomeau/gists{/gist_id}","starred_url":"https://api.github.com/users/joshwcomeau/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshwcomeau/subscriptions","organizations_url":"https://api.github.com/users/joshwcomeau/orgs","repos_url":"https://api.github.com/users/joshwcomeau/repos","events_url":"https://api.github.com/users/joshwcomeau/events{/privacy}","received_events_url":"https://api.github.com/users/joshwcomeau/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/joshwcomeau/guppy","description":"🐠A friendly application manager and task runner for React.js","fork":false,"url":"https://api.github.com/repos/joshwcomeau/guppy","forks_url":"https://api.github.com/repos/joshwcomeau/guppy/forks","keys_url":"https://api.github.com/repos/joshwcomeau/guppy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joshwcomeau/guppy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joshwcomeau/guppy/teams","hooks_url":"https://api.github.com/repos/joshwcomeau/guppy/hooks","issue_events_url":"https://api.github.com/repos/joshwcomeau/guppy/issues/events{/number}","events_url":"https://api.github.com/repos/joshwcomeau/guppy/events","assignees_url":"https://api.github.com/repos/joshwcomeau/guppy/assignees{/user}","branches_url":"https://api.github.com/repos/joshwcomeau/guppy/branches{/branch}","tags_url":"https://api.github.com/repos/joshwcomeau/guppy/tags","blobs_url":"https://api.github.com/repos/joshwcomeau/guppy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joshwcomeau/guppy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joshwcomeau/guppy/git/refs{/sha}","trees_url":"https://api.github.com/repos/joshwcomeau/guppy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joshwcomeau/guppy/statuses/{sha}","languages_url":"https://api.github.com/repos/joshwcomeau/guppy/languages","stargazers_url":"https://api.github.com/repos/joshwcomeau/guppy/stargazers","contributors_url":"https://api.github.com/repos/joshwcomeau/guppy/contributors","subscribers_url":"https://api.github.com/repos/joshwcomeau/guppy/subscribers","subscription_url":"https://api.github.com/repos/joshwcomeau/guppy/subscription","commits_url":"https://api.github.com/repos/joshwcomeau/guppy/commits{/sha}","git_commits_url":"https://api.github.com/repos/joshwcomeau/guppy/git/commits{/sha}","comments_url":"https://api.github.com/repos/joshwcomeau/guppy/comments{/number}","issue_comment_url":"https://api.github.com/repos/joshwcomeau/guppy/issues/comments{/number}","contents_url":"https://api.github.com/repos/joshwcomeau/guppy/contents/{+path}","compare_url":"https://api.github.com/repos/joshwcomeau/guppy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joshwcomeau/guppy/merges","archive_url":"https://api.github.com/repos/joshwcomeau/guppy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joshwcomeau/guppy/downloads","issues_url":"https://api.github.com/repos/joshwcomeau/guppy/issues{/number}","pulls_url":"https://api.github.com/repos/joshwcomeau/guppy/pulls{/number}","milestones_url":"https://api.github.com/repos/joshwcomeau/guppy/milestones{/number}","notifications_url":"https://api.github.com/repos/joshwcomeau/guppy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joshwcomeau/guppy/labels{/name}","releases_url":"https://api.github.com/repos/joshwcomeau/guppy/releases{/id}","deployments_url":"https://api.github.com/repos/joshwcomeau/guppy/deployments","created_at":"2018-05-20T20:49:50Z","updated_at":"2018-08-29T01:08:06Z","pushed_at":"2018-08-29T01:08:06Z","git_url":"git://github.com/joshwcomeau/guppy.git","ssh_url":"git@github.com:joshwcomeau/guppy.git","clone_url":"https://github.com/joshwcomeau/guppy.git","svn_url":"https://github.com/joshwcomeau/guppy","homepage":"","size":12109,"stargazers_count":2284,"watchers_count":2284,"language":"JavaScript","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":100,"mirror_url":null,"archived":false,"open_issues_count":48,"license":{"key":"isc","name":"ISC License","spdx_id":"ISC","url":"https://api.github.com/licenses/isc","node_id":"MDc6TGljZW5zZTEw"},"forks":100,"open_issues":48,"watchers":2284,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154"},"html":{"href":"https://github.com/joshwcomeau/guppy/pull/154"},"issue":{"href":"https://api.github.com/repos/joshwcomeau/guppy/issues/154"},"comments":{"href":"https://api.github.com/repos/joshwcomeau/guppy/issues/154/comments"},"review_comments":{"href":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154/comments"},"review_comment":{"href":"https://api.github.com/repos/joshwcomeau/guppy/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/joshwcomeau/guppy/pulls/154/commits"},"statuses":{"href":"https://api.github.com/repos/joshwcomeau/guppy/statuses/8158a21902901d4e90b8787f4f2f37cd60d2b292"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"superhawk610"}}