puter-cli 1.8.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,18 +3,11 @@ import chalk from 'chalk';
3
3
  import Conf from 'conf';
4
4
  import { execCommand, getPrompt } from '../executor.js';
5
5
  import { PROJECT_NAME } from '../commons.js';
6
- import SetContextModule from '../modules/SetContextModule.js';
7
- import ErrorModule from '../modules/ErrorModule.js';
8
- import ProfileModule from '../modules/ProfileModule.js';
9
- import putility from '@heyputer/putility';
6
+ import { getProfileModule } from '../modules/ProfileModule.js';
10
7
 
11
8
  const config = new Conf({ projectName: PROJECT_NAME });
12
9
 
13
- export const rl = readline.createInterface({
14
- input: process.stdin,
15
- output: process.stdout,
16
- prompt: null
17
- });
10
+ export let rl;
18
11
 
19
12
  /**
20
13
  * Update the current shell prompt
@@ -28,26 +21,21 @@ export function updatePrompt(currentPath) {
28
21
  * Start the interactive shell
29
22
  */
30
23
  export async function startShell(command) {
31
- const modules = [
32
- SetContextModule,
33
- ErrorModule,
34
- ProfileModule,
35
- ];
24
+ const profileModule = getProfileModule();
25
+ await profileModule.checkLogin();
36
26
 
37
- const context = new putility.libs.context.Context({
38
- events: new putility.libs.event.Emitter(),
39
- });
40
-
41
- for ( const module of modules ) module({ context });
42
-
43
- await context.events.emit('check-login', {});
44
-
45
27
  // This argument enables the `puter <subcommand>` commands
46
- if ( command ) {
47
- await execCommand(context, command);
28
+ if (command) {
29
+ await execCommand(command);
48
30
  process.exit(0);
49
31
  }
50
32
 
33
+ rl = readline.createInterface({
34
+ input: process.stdin,
35
+ output: process.stdout,
36
+ prompt: null
37
+ })
38
+
51
39
  try {
52
40
  console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
53
41
  rl.setPrompt(getPrompt());
@@ -57,7 +45,7 @@ export async function startShell(command) {
57
45
  const trimmedLine = line.trim();
58
46
  if (trimmedLine) {
59
47
  try {
60
- await execCommand(context, trimmedLine);
48
+ await execCommand(trimmedLine);
61
49
  } catch (error) {
62
50
  console.error(chalk.red(error.message));
63
51
  }
@@ -1,23 +1,18 @@
1
1
  import chalk from 'chalk';
2
- import fetch from 'node-fetch';
3
2
  import Table from 'cli-table3';
4
3
  import { getCurrentUserName, getCurrentDirectory } from './auth.js';
5
- import { API_BASE, getHeaders, generateAppName, resolveRemotePath, isValidAppName } from '../commons.js';
4
+ import { resolveRemotePath, isValidAppName } from '../commons.js';
6
5
  import { displayNonNullValues, formatDate, isValidAppUuid } from '../utils.js';
7
- import { getSubdomains, createSubdomain, deleteSubdomain } from './subdomains.js';
8
- import { ErrorAPI } from '../modules/ErrorModule.js';
9
-
6
+ import { getSubdomains, createSubdomain, deleteSubdomain, updateSubdomain } from './subdomains.js';
7
+ import { getPuter } from '../modules/PuterModule.js';
8
+ import { report } from '../modules/ErrorModule.js';
10
9
 
11
10
  /**
12
11
  * Listing subdomains
13
12
  */
14
- export async function listSites(args = {}, context) {
13
+ export async function listSites(args = {}) {
15
14
  try {
16
- const data = await getSubdomains(args);
17
-
18
- if (!data.success || !Array.isArray(data.result)) {
19
- throw new Error('Failed to fetch subdomains');
20
- }
15
+ const result = await getSubdomains(args);
21
16
 
22
17
  // Create table instance
23
18
  const table = new Table({
@@ -35,7 +30,7 @@ export async function listSites(args = {}, context) {
35
30
 
36
31
  // Format and add data to table
37
32
  let i = 0;
38
- data.result.forEach(domain => {
33
+ result.forEach(domain => {
39
34
  let appDir = domain?.root_dir?.path.split('/').pop().split('-');
40
35
  table.push([
41
36
  i++,
@@ -49,16 +44,16 @@ export async function listSites(args = {}, context) {
49
44
  });
50
45
 
51
46
  // Print table
52
- if (data.result.length === 0) {
47
+ if (result.length === 0) {
53
48
  console.log(chalk.yellow('No subdomains found'));
54
49
  } else {
55
50
  console.log(chalk.bold('\nYour Sites:'));
56
51
  console.log(table.toString());
57
- console.log(chalk.dim(`Total Sites: ${data.result.length}`));
52
+ console.log(chalk.dim(`Total Sites: ${result.length}`));
58
53
  }
59
54
 
60
55
  } catch (error) {
61
- context.events.emit('error', { error });
56
+ report(error);
62
57
  console.error(chalk.red('Error listing sites:'), error.message);
63
58
  throw error;
64
59
  }
@@ -73,26 +68,11 @@ export async function infoSite(args = []) {
73
68
  console.log(chalk.red('Usage: site <siteUID>'));
74
69
  return;
75
70
  }
76
- for (const subdomainId of args)
71
+ const puter = getPuter();
72
+ for (const subdomain of args)
77
73
  try {
78
- const response = await fetch(`${API_BASE}/drivers/call`, {
79
- method: 'POST',
80
- headers: getHeaders(),
81
- body: JSON.stringify({
82
- interface: 'puter-subdomains',
83
- method: 'read',
84
- args: { uid: subdomainId }
85
- })
86
- });
87
-
88
- if (!response.ok) {
89
- throw new Error('Failed to fetch subdomains.');
90
- }
91
- const data = await response.json();
92
- if (!data.success || !data.result) {
93
- throw new Error(`Failed to get site info: ${data.error?.message}`);
94
- }
95
- displayNonNullValues(data.result);
74
+ const result = await puter.hosting.get(subdomain);
75
+ displayNonNullValues(result);
96
76
  } catch (error) {
97
77
  console.error(chalk.red('Error getting site info:'), error.message);
98
78
  }
@@ -100,43 +80,14 @@ export async function infoSite(args = []) {
100
80
 
101
81
  /**
102
82
  * Delete hosted web site
103
- * @param {any[]} args Array of site uuid
83
+ * @param {any[]} args Array of subdomain
104
84
  */
105
85
  export async function deleteSite(args = []) {
106
86
  if (args.length < 1){
107
- console.log(chalk.red('Usage: site:delete <siteUUID>'));
108
- return false;
109
- }
110
- for (const uuid of args)
111
- try {
112
- if (!uuid){
113
- console.log(chalk.yellow(`We could not find the site ID: ${uuid}`));
114
- return false;
115
- }
116
- // The uuid must be prefixed with: 'subdomainObj-'
117
- const response = await fetch(`${API_BASE}/delete-site`, {
118
- headers: getHeaders(),
119
- method: 'POST',
120
- body: JSON.stringify({
121
- site_uuid: uuid
122
- })
123
- });
124
-
125
- if (!response.ok) {
126
- throw new Error(`Failed to delete site (Status: ${response.status})`);
127
- }
128
-
129
- const data = await response.json();
130
- if (Object.keys(data).length==0) {
131
- console.log(chalk.green(`Site ID: "${uuid}" has been deleted.`));
132
- return;
133
- }
134
-
135
- console.log(chalk.yellow(`Site ID: "${uuid}" should be deleted.`));
136
- } catch (error) {
137
- console.error(chalk.red('Error deleting site:'), error.message);
87
+ console.log(chalk.red('Usage: site:delete <subdomain>'));
138
88
  return false;
139
89
  }
90
+ await deleteSubdomain(args);
140
91
  return true;
141
92
  }
142
93
 
@@ -171,12 +122,7 @@ export async function infoSite(args = []) {
171
122
  }
172
123
 
173
124
  // Step 2: Check if the subdomain already exists
174
- const data = await getSubdomains();
175
- if (!data.success || !Array.isArray(data.result)) {
176
- throw new Error('Failed to fetch subdomains');
177
- }
178
-
179
- const subdomains = data.result;
125
+ const subdomains = await getSubdomains();;
180
126
  const subdomainObj = subdomains.find(sd => sd.subdomain === subdomain);
181
127
  if (subdomainObj) {
182
128
  console.error(chalk.cyan(`The subdomain "${subdomain}" is already in use and owned by: "${subdomainObj.owner['username']}"`));
@@ -188,35 +134,31 @@ export async function infoSite(args = []) {
188
134
  return;
189
135
  } else {
190
136
  console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
191
- console.log(chalk.cyan(`We'll try to unlink this subdomain from that directory...`));
192
- const result = await deleteSubdomain([subdomainObj?.uid]);
137
+ console.log(chalk.cyan(`Updating this subdomain directory...`));
138
+ const result = await updateSubdomain(subdomain, remoteDir);
193
139
  if (result) {
194
- console.log(chalk.green('Looks like this subdomain is free again, please try again.'));
140
+ console.log(chalk.green('Updating subdomain directory successful.'));
195
141
  return;
196
142
  } else {
197
- console.log(chalk.red('Could not release this subdomain.'));
143
+ console.log(chalk.red('Could not update this subdomain directory.'));
144
+ return;
198
145
  }
199
146
  }
200
147
  }
201
148
  }
202
- // else {
203
- // console.log(chalk.yellow(`The subdomain: "${subdomain}" is already taken, so let's generate a new random one:`));
204
- // subdomain = generateAppName(); // Generate a random subdomain
205
- // console.log(chalk.cyan(`New generated subdomain: "${subdomain}" will be used.`));
206
- // }
207
149
 
208
150
  // Use the chosen "subdomain"
209
151
  console.log(chalk.cyan(`New generated subdomain: "${subdomain}" will be used if its not already in use.`));
210
152
 
211
153
  // Step 3: Host the current directory under the subdomain
212
- console.log(chalk.cyan(`Hosting app "${appName}" under subdomain "${subdomain}"...`));
154
+ console.log(chalk.cyan(`Hosting site "${appName}" under subdomain "${subdomain}"...`));
213
155
  const site = await createSubdomain(subdomain, remoteDir);
214
156
  if (!site){
215
157
  console.error(chalk.red(`Failed to create subdomain: "${chalk.red(subdomain)}"`));
216
158
  return;
217
159
  }
218
160
 
219
- console.log(chalk.green(`App ${chalk.dim(appName)} created successfully and accessible at:`));
161
+ console.log(chalk.green(`Site ${chalk.dim(appName)} created successfully and accessible at:`));
220
162
  console.log(chalk.cyan(`https://${site.subdomain}.puter.site`));
221
163
  return site;
222
164
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import fetch from 'node-fetch';
3
3
  import { API_BASE, getHeaders } from '../commons.js';
4
+ import { getPuter } from '../modules/PuterModule.js';
4
5
 
5
6
  /**
6
7
  * Get list of subdomains.
@@ -8,20 +9,16 @@ import { API_BASE, getHeaders } from '../commons.js';
8
9
  * @returns {Array} - Array of subdomains.
9
10
  */
10
11
  export async function getSubdomains(args = {}) {
11
- const response = await fetch(`${API_BASE}/drivers/call`, {
12
- method: 'POST',
13
- headers: getHeaders(),
14
- body: JSON.stringify({
15
- interface: 'puter-subdomains',
16
- method: 'select',
17
- args: args
18
- })
19
- });
12
+ const puter = getPuter();
13
+ let result;
20
14
 
21
- if (!response.ok) {
22
- throw new Error('Failed to fetch subdomains.');
15
+ try {
16
+ result = await puter.hosting.list();
17
+ } catch (error) {
18
+ console.log(chalk.red(`Error when getting subdomains.\nError: ${error?.message}`));
23
19
  }
24
- return await response.json();
20
+
21
+ return result;
25
22
  }
26
23
 
27
24
  /**
@@ -34,32 +31,22 @@ export async function deleteSubdomain(args = []) {
34
31
  console.log(chalk.red('Usage: domain:delete <subdomain_id>'));
35
32
  return false;
36
33
  }
34
+ const puter = getPuter();
37
35
  const subdomains = args;
38
- for (const subdomainId of subdomains)
36
+ for (const subdomain of subdomains)
39
37
  try {
40
- const response = await fetch(`${API_BASE}/drivers/call`, {
41
- headers: getHeaders(),
42
- method: 'POST',
43
- body: JSON.stringify({
44
- interface: 'puter-subdomains',
45
- method: 'delete',
46
- args: {
47
- id: { subdomain: subdomainId }
48
- }
49
- })
50
- });
38
+ const success = await puter.hosting.delete(subdomain);
51
39
 
52
- const data = await response.json();
53
- if (!data.success) {
54
- if (data.error?.code === 'entity_not_found') {
55
- console.log(chalk.red(`Subdomain ID: "${subdomainId}" not found`));
56
- return false;
57
- }
40
+ if (!success) {
58
41
  console.log(chalk.red(`Failed to delete subdomain: ${data.error?.message}`));
59
42
  return false;
60
43
  }
61
44
  console.log(chalk.green('Subdomain deleted successfully'));
62
45
  } catch (error) {
46
+ if (error.error?.code === 'entity_not_found') {
47
+ console.log(chalk.red(`Subdomain: "${subdomain}" not found`));
48
+ return false;
49
+ }
63
50
  console.error(chalk.red('Error deleting subdomain:'), error.message);
64
51
  }
65
52
  return true;
@@ -72,33 +59,37 @@ export async function deleteSubdomain(args = []) {
72
59
  * @returns {Object} - Hosting details (e.g., subdomain).
73
60
  */
74
61
  export async function createSubdomain(subdomain, remoteDir) {
75
- const response = await fetch(`${API_BASE}/drivers/call`, {
76
- method: 'POST',
77
- headers: getHeaders(),
78
- body: JSON.stringify({
79
- interface: 'puter-subdomains',
80
- method: 'create',
81
- args: {
82
- object: {
83
- subdomain: subdomain,
84
- root_dir: remoteDir
85
- }
86
- }
87
- })
88
- });
62
+ const puter = getPuter();
63
+ let result;
89
64
 
90
- if (!response.ok) {
91
- throw new Error('Failed to host directory.');
92
- }
93
- const data = await response.json();
94
- if (!data.success || !data.result) {
95
- if (data.error?.code === 'already_in_use') {
96
- // data.error?.status===409
97
- console.log(chalk.yellow(`Subdomain already taken!\nMessage: ${data?.error?.message}`));
65
+ try {
66
+ result = await puter.hosting.create(subdomain, remoteDir);
67
+ } catch (error) {
68
+ if (error?.error?.code === 'already_in_use') {
69
+ console.log(chalk.yellow(`Subdomain already taken!\nMessage: ${error?.error?.message}`));
98
70
  return false;
99
71
  }
100
- console.log(chalk.red(`Error when creating "${subdomain}".\nError: ${data?.error?.message}\nCode: ${data.error?.code}`));
101
- return false;
72
+ console.log(chalk.red(`Error when creating "${subdomain}".\nError: ${error?.error?.message}\nCode: ${error?.error?.code}`));
102
73
  }
103
- return data.result;
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Update a subdomain into remote directory
79
+ * @param {string} subdomain - Subdomain name.
80
+ * @param {string} remoteDir - Remote directory path.
81
+ * @returns {Object} - Hosting details (e.g., subdomain).
82
+ */
83
+ export async function updateSubdomain(subdomain, remoteDir) {
84
+ const puter = getPuter();
85
+ let result;
86
+
87
+ try {
88
+ result = await puter.hosting.update(subdomain, remoteDir);
89
+ } catch (error) {
90
+ console.log(chalk.red(`Error when updating "${subdomain}".\nError: ${error?.message}`));
91
+ return null;
92
+ }
93
+
94
+ return result;
104
95
  }
package/src/commons.js CHANGED
@@ -45,7 +45,7 @@ export function getHeaders(contentType = 'application/json') {
45
45
  * @see: [randName](https://github.com/HeyPuter/puter/blob/06a67a3b223a6cbd7ec2e16853b6d2304f621a88/src/puter-js/src/index.js#L389)
46
46
  */
47
47
  export function generateAppName(separateWith = '-'){
48
- console.log(chalk.cyan('Generating random app name...'));
48
+ console.log(chalk.cyan('Generating random name...'));
49
49
  try {
50
50
  const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
51
51
  'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
@@ -63,7 +63,7 @@ export function generateAppName(separateWith = '-'){
63
63
  // return a random combination of first_adj + noun + number (between 0 and 9999)
64
64
  // e.g. clever-idea-123
65
65
  const appName = first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
66
- console.log(chalk.green(`AppName: "${appName}"`));
66
+ console.log(chalk.green(`Name: "${appName}"`));
67
67
  return appName;
68
68
  } catch (error) {
69
69
  console.error(`Error: ${error.message}`);
package/src/executor.js CHANGED
@@ -2,10 +2,12 @@ import chalk from 'chalk';
2
2
  import Conf from 'conf';
3
3
  import { listApps, appInfo, createApp, updateApp, deleteApp } from './commands/apps.js';
4
4
  import { listSites, createSite, deleteSite, infoSite } from './commands/sites.js';
5
- import { listFiles, makeDirectory, renameFileOrDirectory,
6
- removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
7
- getInfo, getDiskUsage, createFile, readFile, uploadFile,
8
- downloadFile, copyFile, syncDirectory, editFile } from './commands/files.js';
5
+ import {
6
+ listFiles, makeDirectory, renameFileOrDirectory,
7
+ removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
8
+ getInfo, getDiskUsage, createFile, readFile, uploadFile,
9
+ downloadFile, copyFile, syncDirectory, editFile
10
+ } from './commands/files.js';
9
11
  import { getUserInfo, getUsageInfo, login } from './commands/auth.js';
10
12
  import { deploy } from './commands/deploy.js';
11
13
  import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
@@ -13,7 +15,7 @@ import inquirer from 'inquirer';
13
15
  import { exec } from 'node:child_process';
14
16
  import { parseArgs, getSystemEditor } from './utils.js';
15
17
  import { rl } from './commands/shell.js';
16
- import { ErrorAPI } from './modules/ErrorModule.js';
18
+ import { showLast } from './modules/ErrorModule.js'
17
19
 
18
20
  const config = new Conf({ projectName: PROJECT_NAME });
19
21
 
@@ -39,8 +41,8 @@ const commands = {
39
41
  whoami: getUserInfo,
40
42
  stat: getInfo,
41
43
  apps: async (args) => {
42
- await listApps({
43
- statsPeriod: args[0] || 'all'
44
+ await listApps({
45
+ statsPeriod: args[0] || 'all'
44
46
  });
45
47
  },
46
48
  app: appInfo,
@@ -64,9 +66,7 @@ const commands = {
64
66
  rl.write(commandToCopy);
65
67
  }
66
68
  },
67
- 'last-error': async (_, context) => {
68
- context[ErrorAPI].showLast();
69
- },
69
+ 'last-error': showLast,
70
70
  'app:create': async (rawArgs) => {
71
71
  try {
72
72
  const args = parseArgs(rawArgs.join(' '));
@@ -88,8 +88,8 @@ const commands = {
88
88
  },
89
89
  'app:update': async (args) => {
90
90
  if (args.length < 1) {
91
- console.log(chalk.red('Usage: app:update <name> <remote_dir>'));
92
- return;
91
+ console.log(chalk.red('Usage: app:update <name> <remote_dir>'));
92
+ return;
93
93
  }
94
94
  await updateApp(args);
95
95
  },
@@ -102,25 +102,25 @@ const commands = {
102
102
  }
103
103
  });
104
104
  if (args._.length < 1) {
105
- console.log(chalk.red('You must specify the app name:'));
106
- console.log(chalk.yellow('Example: app:delete <name>'));
107
- return;
105
+ console.log(chalk.red('You must specify the app name:'));
106
+ console.log(chalk.yellow('Example: app:delete <name>'));
107
+ return;
108
108
  }
109
109
  const name = args._[0];
110
110
  const force = !!args.f;
111
111
 
112
- if (!force){
112
+ if (!force) {
113
113
  const { confirm } = await inquirer.prompt([
114
114
  {
115
- type: 'confirm',
116
- name: 'confirm',
117
- message: chalk.yellow(`Are you sure you want to delete "${name}"?`),
118
- default: false
115
+ type: 'confirm',
116
+ name: 'confirm',
117
+ message: chalk.yellow(`Are you sure you want to delete "${name}"?`),
118
+ default: false
119
119
  }
120
120
  ]);
121
121
  if (!confirm) {
122
- console.log(chalk.yellow('Operation cancelled.'));
123
- return false;
122
+ console.log(chalk.yellow('Operation cancelled.'));
123
+ return false;
124
124
  }
125
125
  }
126
126
  await deleteApp(name);
@@ -155,10 +155,9 @@ const commands = {
155
155
  * Execute a command
156
156
  * @param {string} input The command line input
157
157
  */
158
- export async function execCommand(context, input) {
159
- const [cmd, ...args] = input.split(' ');
158
+ export async function execCommand(input) {
159
+ const [cmd, ...args] = input ? input.split(' ') : [];
160
160
 
161
-
162
161
  // Add the command to history (skip the "history" command itself)
163
162
  if (cmd !== 'history') {
164
163
  commandHistory.push(input);
@@ -189,7 +188,7 @@ export async function execCommand(context, input) {
189
188
  }
190
189
  if (commands[cmd]) {
191
190
  try {
192
- await commands[cmd](args, context);
191
+ await commands[cmd](args);
193
192
  } catch (error) {
194
193
  console.error(chalk.red(`Error executing command: ${error.message}`));
195
194
  }
@@ -355,9 +354,9 @@ function showHelp(command) {
355
354
  Example: site:create mywebsite /path/to/dir --subdomain=mywebsite
356
355
  `,
357
356
  'site:deploy': `
358
- ${chalk.cyan('site:deploy [<valid_name_app>] [<remote_dir>] [--subdomain=<subdomain>]')}
357
+ ${chalk.cyan('site:deploy [<remote_dir>] [--subdomain=<subdomain>]')}
359
358
  Deploy a local web project to Puter.
360
- Example: site:deploy my-app ./my-app --subdomain my-app
359
+ Example: site:deploy ./my-app --subdomain my-app
361
360
  `,
362
361
  '!': `
363
362
  ${chalk.cyan('!<command>')}
@@ -1,33 +1,20 @@
1
- const ERROR_BUFFER_LIMIT = 20;
1
+ export const ERROR_BUFFER_LIMIT = 20;
2
2
 
3
- export const ErrorAPI = Symbol('ErrorAPI');
3
+ export const errors = [];
4
4
 
5
- export default ({ context }) => {
6
- // State Variables
7
- const errors = [];
8
-
9
- context.events.on('error', (error) => {
10
- context[ErrorAPI].report(error);
11
- });
12
-
13
- // Module Methods
14
- context[ErrorAPI] = {
15
- // Add an error to the error history
16
- report (error) {
17
- errors.push(error);
18
- if (errors.length > ERROR_BUFFER_LIMIT) {
19
- errors = errors.slice(errors.length - ERROR_BUFFER_LIMIT);
20
- }
21
- },
22
- // Print the last error from the error history,
23
- // and remove it from the history
24
- showLast () {
25
- const err = errors.pop();
26
- if (err) {
27
- console.error(err);
28
- } else {
29
- console.log('No errors to report');
30
- }
31
- }
32
- };
33
- };
5
+ export const report = (error) => {
6
+ errors.push(error);
7
+ if (errors.length > ERROR_BUFFER_LIMIT) {
8
+ errors.splice(0, errors.length - ERROR_BUFFER_LIMIT)
9
+ }
10
+ }
11
+ export const showLast = () => {
12
+ // Print the last error from the error history,
13
+ // and remove it from the history
14
+ const err = errors.pop();
15
+ if (err) {
16
+ console.error(err);
17
+ } else {
18
+ console.log('No errors to report');
19
+ }
20
+ }