@grayspar/cli 0.0.4 → 0.0.5

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.
@@ -87,17 +87,17 @@ accessCommand
87
87
  accessCommand
88
88
  .command('add')
89
89
  .argument('<targets...>', 'email addresses, or @company.example for everyone at a company')
90
- .description('let named people or a whole company open a project')
90
+ .description('let people or a whole company open a project')
91
91
  .action(commandAction((format, _command, targets) => addAccess(targets, format)));
92
92
  accessCommand
93
93
  .command('remove')
94
94
  .argument('<target>', 'an email address, or @company.example')
95
- .description('take someone or a company off a project')
95
+ .description("remove a person or company from a project's list")
96
96
  .action(commandAction((format, _command, target) => removeAccess(target, format)));
97
97
  accessCommand
98
98
  .command('set')
99
99
  .addOption(new Option('--mode <mode>', 'restricted lets in the people and companies on the list, open lets in anyone with the link')
100
100
  .choices(['restricted', 'open'])
101
101
  .makeOptionMandatory())
102
- .description('choose whether the list decides who can open a project')
102
+ .description('choose whether only the list or anyone with the link can open a project')
103
103
  .action(commandAction((format, command) => setGeneralAccess(command.opts().mode, format)));
@@ -179,7 +179,7 @@ async function printOutcome(api, project, outcome) {
179
179
  }
180
180
  export const deployCommand = new Command('deploy')
181
181
  .argument('[folder]', 'deploy only the files under this folder of the project', '.')
182
- .description('build a project folder and put it online')
182
+ .description('build a folder and put it online as a project')
183
183
  .option('-y, --yes', 'take the plan as shown without asking')
184
184
  .option('--name <name>', 'create a new project with this name')
185
185
  .option('--no-env', 'deploy without reading any .env file')
@@ -61,11 +61,11 @@ envCommand
61
61
  .command('set')
62
62
  .argument('<pairs...>', 'variables written as KEY=VALUE')
63
63
  .addOption(serviceOption('which service to set them on'))
64
- .description('add or change environment variables')
64
+ .description('set variables on a service')
65
65
  .action(commandAction((format, command, pairs) => setVariables(pairs, serviceNameOf(command), format)));
66
66
  envCommand
67
67
  .command('remove')
68
68
  .argument('<keys...>', 'the names of the variables to remove')
69
69
  .addOption(serviceOption('which service to change'))
70
- .description('remove environment variables')
70
+ .description('remove variables from a service')
71
71
  .action(commandAction((format, command, keys) => removeVariables(keys, serviceNameOf(command), format)));
@@ -21,8 +21,8 @@ async function unlink(format) {
21
21
  printResult(format, { status: 'ok' }, () => receipt(`Unlinked ${found.folder}`));
22
22
  }
23
23
  export const linkCommand = new Command('link')
24
- .description('bind this folder to a project')
24
+ .description('link this folder to a project')
25
25
  .action(commandAction((format) => link(format)));
26
26
  export const unlinkCommand = new Command('unlink')
27
- .description('unbind this folder from its project')
27
+ .description('unlink this folder from its project')
28
28
  .action(commandAction((format) => unlink(format)));
@@ -1,6 +1,5 @@
1
- /* Listens on a port the machine picks, catches exactly one redirect, and closes. The browser carries
2
- a code; the verifier that makes it usable never leaves this process. The browser hears nothing until
3
- the CLI has traded the code, so what its page says is true. */
1
+ /* Listens on a port the machine picks for one redirect, then closes. The verifier that makes the browser's code usable
2
+ never leaves this process, and the browser hears nothing until the code is traded, so its page tells the truth. */
4
3
  import { createServer } from 'node:http';
5
4
  function browserPage(mark, markColor, title, detail) {
6
5
  return `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="light dark">
@@ -14,5 +14,5 @@ async function logout(format) {
14
14
  : `${receipt('Signed out on this machine')}\n${warning("Grayspar couldn't be reached, so this sign-in stays valid there. Sign it out under Ways to sign in in the dashboard.")}`);
15
15
  }
16
16
  export const logoutCommand = new Command('logout')
17
- .description('forget the saved sign-in on this machine')
17
+ .description('sign out on this machine')
18
18
  .action(commandAction((format) => logout(format)));
@@ -98,7 +98,7 @@ async function printBuildLogs(api, format, follow) {
98
98
  }
99
99
  }
100
100
  export const logsCommand = new Command('logs')
101
- .description('show what a service printed while running, or with --build the last build log')
101
+ .description("show a service's logs, or with --build its last build log")
102
102
  .addOption(serviceOption('which service to read'))
103
103
  .option('--build', 'show the log from the last build instead')
104
104
  .option('-f', 'keep printing new lines as they arrive')
@@ -16,6 +16,6 @@ async function restart(serviceName, format) {
16
16
  printResult(format, { status: 'restarting', project: project.name, service: service.name }, () => `Restarting ${service.name}.\nTo follow it, run\n${follow}`);
17
17
  }
18
18
  export const restartCommand = new Command('restart')
19
- .description('run a service again in place, keeping its volume')
19
+ .description("start a fresh copy of a service's running version")
20
20
  .addOption(serviceOption('which service to restart'))
21
21
  .action(commandAction((format, command) => restart(serviceNameOf(command), format)));
@@ -4,6 +4,7 @@ import { dataOrThrow, signedInClient } from '../lib/client.js';
4
4
  import { commandAction } from '../lib/command-helpers.js';
5
5
  import { CliError } from '../lib/errors.js';
6
6
  import { printResult } from '../lib/output.js';
7
+ import { askText } from '../lib/prompt.js';
7
8
  import { resolveService } from '../lib/resolve-service.js';
8
9
  import { receipt } from '../lib/ui.js';
9
10
  function receiptsFor(service, change) {
@@ -38,7 +39,21 @@ async function setService(serviceName, change, format) {
38
39
  ];
39
40
  printResult(format, { status: 'ok' }, () => lines.join('\n'));
40
41
  }
41
- export const serviceCommand = new Command('service').description('change where a service answers and how it is built');
42
+ async function removeService(serviceName, skipAsking, format) {
43
+ const api = await signedInClient();
44
+ const { project, service } = await resolveService(api, serviceName, '<name>');
45
+ if (!skipAsking) {
46
+ const typed = await askText(`Type ${service.name} to remove it with its volume and variables`, null, '--yes');
47
+ if (typed !== service.name) {
48
+ throw new CliError('name_mismatch', `That isn't ${service.name}. The service stays.`);
49
+ }
50
+ }
51
+ await dataOrThrow(api.DELETE('/api/projects/{projectId}/services/{serviceId}', {
52
+ params: { path: { projectId: project.id, serviceId: service.id } },
53
+ }));
54
+ printResult(format, { status: 'removed', service: service.name, project: project.name }, () => `${receipt(`Removed service ${service.name} from ${project.name}`)}\nAny future grayspar deploy will redeploy the ${service.name} service.`);
55
+ }
56
+ export const serviceCommand = new Command('service').description('change where a service answers and how it is built, or remove it');
42
57
  serviceCommand
43
58
  .command('set')
44
59
  .argument('[name]', 'the service to change, left out when the project has one')
@@ -61,3 +76,9 @@ serviceCommand
61
76
  };
62
77
  return setService(name, change, format);
63
78
  }));
79
+ serviceCommand
80
+ .command('remove')
81
+ .argument('<name>', 'the service to remove')
82
+ .option('-y, --yes', 'remove it without asking')
83
+ .description('remove a service with its volume and variables')
84
+ .action(commandAction((format, command, name) => removeService(name, command.opts().yes === true, format)));
@@ -11,5 +11,5 @@ async function status(format) {
11
11
  printResult(format, project, () => `${color.heading(project.name)}\n\n${servicesTable(project)}`);
12
12
  }
13
13
  export const statusCommand = new Command('status')
14
- .description('show what a project is doing')
14
+ .description("show each service's address and status")
15
15
  .action(commandAction((format) => status(format)));
@@ -66,22 +66,22 @@ export async function removeVolume(mountPath, serviceName, skipAsking, format) {
66
66
  receipts: [`Removed volume ${mountPath} from ${service.name} and deleted its data`],
67
67
  });
68
68
  }
69
- export const volumeCommand = new Command('volume').description('keep a folder across deploys');
69
+ export const volumeCommand = new Command('volume').description('keep folders across deploys with volumes');
70
70
  volumeCommand
71
71
  .command('list', { isDefault: true })
72
72
  .addOption(serviceOption('which service to read'))
73
- .description('list the folders a service keeps')
73
+ .description('list the volumes of a project')
74
74
  .action(commandAction((format, command) => listVolumes(serviceNameOf(command), format)));
75
75
  volumeCommand
76
76
  .command('add')
77
77
  .argument('<path>', 'the folder inside the service to keep, such as /app/data')
78
78
  .addOption(serviceOption('which service keeps it'))
79
- .description('keep a folder across deploys')
79
+ .description('add a volume to a service')
80
80
  .action(commandAction((format, command, path) => addVolume(path, serviceNameOf(command), format)));
81
81
  volumeCommand
82
82
  .command('remove')
83
83
  .argument('<path>', 'a folder the service keeps')
84
84
  .addOption(serviceOption('which service keeps it'))
85
85
  .option('-y, --yes', 'remove it without asking')
86
- .description('stop keeping a folder, and throw its data away')
86
+ .description('delete a volume and its data')
87
87
  .action(commandAction((format, command, path) => removeVolume(path, serviceNameOf(command), command.opts().yes === true, format)));
@@ -12,5 +12,5 @@ async function whoami(format) {
12
12
  printResult(format, { ...profile, teams }, () => `Signed in as ${profile.email}\n\n${table(['TEAM', 'SLUG'], teams.map((team) => [team.name, team.slug]))}`);
13
13
  }
14
14
  export const whoamiCommand = new Command('whoami')
15
- .description('show who this machine is signed in as')
15
+ .description('show which account this machine is signed in with')
16
16
  .action(commandAction((format) => whoami(format)));
package/dist/index.js CHANGED
@@ -1,60 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /* Every command is a singular noun and one of four verbs, or a single flat verb. `.claude/rules/cli.md` holds the
3
- grammar, and each command's own file holds its flags; here are only the flags they all share. */
4
- import { Command } from 'commander';
5
- import { accessCommand } from './commands/access.js';
6
- import { deployCommand } from './commands/deploy/deploy.js';
7
- import { envCommand } from './commands/env.js';
8
- import { linkCommand, unlinkCommand } from './commands/link.js';
9
- import { loginCommand } from './commands/login/login.js';
10
- import { logoutCommand } from './commands/logout.js';
11
- import { logsCommand } from './commands/logs.js';
12
- import { openPathCommand } from './commands/open-path.js';
13
- import { projectCommand } from './commands/project.js';
14
- import { restartCommand } from './commands/restart.js';
15
- import { serviceCommand } from './commands/service.js';
16
- import { statusCommand } from './commands/status.js';
17
- import { volumeCommand } from './commands/volume.js';
18
- import { whoamiCommand } from './commands/whoami.js';
19
- import { setJsonOutput } from './lib/prompt.js';
20
- import { setNamedProject } from './lib/resolve-project.js';
21
- import { setNamedTeam } from './lib/resolve-team.js';
22
- import { routeUsageErrors } from './lib/usage-errors.js';
23
- import { cliVersion } from './lib/version.js';
24
- const program = new Command()
25
- .name('grayspar')
26
- .description('deploy your applications and share them with exactly the people you choose')
27
- .version(cliVersion, '--version', 'show the version')
28
- .option('--json', 'answer in JSON, for scripts and agents')
29
- .option('--team <slug>', 'which of your teams to work in')
30
- .option('--project <name>', 'which project to act on, instead of the linked one')
31
- .option('--no-color', 'print without colour')
32
- .hook('preAction', (_program, command) => {
33
- const { json, team, project } = command.optsWithGlobals();
34
- setJsonOutput(json === true);
35
- setNamedTeam(team);
36
- setNamedProject(project);
37
- });
38
- for (const command of [
39
- loginCommand,
40
- logoutCommand,
41
- whoamiCommand,
42
- deployCommand,
43
- projectCommand,
44
- accessCommand,
45
- openPathCommand,
46
- envCommand,
47
- volumeCommand,
48
- serviceCommand,
49
- linkCommand,
50
- unlinkCommand,
51
- statusCommand,
52
- logsCommand,
53
- restartCommand,
54
- ]) {
55
- program.addCommand(command);
56
- }
57
- routeUsageErrors(program);
2
+ import { program } from './program.js';
58
3
  // A reader such as `head` can leave early, which ends the output and nothing more.
59
4
  process.stdout.on('error', (error) => {
60
5
  if (error.code === 'EPIPE')
@@ -1,6 +1,5 @@
1
- /* A request that never arrived or failed without Grayspar's own answer is tried again with a growing, jittered wait,
2
- for as long as failures follow one another without a break, then the last failure is let through. The two minutes
3
- count from the start of the first failing attempt, and each later attempt is cut off when they are up. */
1
+ /* A request that never arrived or failed without Grayspar's answer retries with growing, jittered waits while failures
2
+ run unbroken, for two minutes from the first failing attempt, which also end the last; then the failure stands. */
4
3
  import { setTimeout as pause } from 'node:timers/promises';
5
4
  import { CliError } from './errors.js';
6
5
  const FIRST_DELAY_MS = 1000;
@@ -5,6 +5,8 @@ const RUNTIME_STATUS = {
5
5
  running: { label: 'Running', tint: color.ok },
6
6
  restarting: { label: 'Restarting', tint: untinted },
7
7
  crashed: { label: 'Crashed', tint: color.bad },
8
+ sleeping: { label: 'Sleeping', tint: untinted },
9
+ stopped: { label: 'Stopped', tint: untinted },
8
10
  unknown: { label: 'Unknown', tint: untinted },
9
11
  failed: { label: 'Failed', tint: color.bad },
10
12
  };
@@ -0,0 +1,56 @@
1
+ /* Every command is a singular noun and one of four verbs, or a single flat verb. `.claude/rules/cli.md` holds the
2
+ grammar, and each command's own file holds its flags; here are only the flags they all share. */
3
+ import { Command } from 'commander';
4
+ import { accessCommand } from './commands/access.js';
5
+ import { deployCommand } from './commands/deploy/deploy.js';
6
+ import { envCommand } from './commands/env.js';
7
+ import { linkCommand, unlinkCommand } from './commands/link.js';
8
+ import { loginCommand } from './commands/login/login.js';
9
+ import { logoutCommand } from './commands/logout.js';
10
+ import { logsCommand } from './commands/logs.js';
11
+ import { openPathCommand } from './commands/open-path.js';
12
+ import { projectCommand } from './commands/project.js';
13
+ import { restartCommand } from './commands/restart.js';
14
+ import { serviceCommand } from './commands/service.js';
15
+ import { statusCommand } from './commands/status.js';
16
+ import { volumeCommand } from './commands/volume.js';
17
+ import { whoamiCommand } from './commands/whoami.js';
18
+ import { setJsonOutput } from './lib/prompt.js';
19
+ import { setNamedProject } from './lib/resolve-project.js';
20
+ import { setNamedTeam } from './lib/resolve-team.js';
21
+ import { routeUsageErrors } from './lib/usage-errors.js';
22
+ import { cliVersion } from './lib/version.js';
23
+ export const program = new Command()
24
+ .name('grayspar')
25
+ .description('deploy your applications and share them with exactly the people you choose')
26
+ .version(cliVersion, '--version', 'show the version')
27
+ .option('--json', 'answer in JSON, for scripts and agents')
28
+ .option('--team <slug>', 'which of your teams to work in')
29
+ .option('--project <name>', 'which project to act on, instead of the linked one')
30
+ .option('--no-color', 'print without colour')
31
+ .hook('preAction', (_program, command) => {
32
+ const { json, team, project } = command.optsWithGlobals();
33
+ setJsonOutput(json === true);
34
+ setNamedTeam(team);
35
+ setNamedProject(project);
36
+ });
37
+ for (const command of [
38
+ loginCommand,
39
+ logoutCommand,
40
+ whoamiCommand,
41
+ deployCommand,
42
+ projectCommand,
43
+ accessCommand,
44
+ openPathCommand,
45
+ envCommand,
46
+ volumeCommand,
47
+ serviceCommand,
48
+ linkCommand,
49
+ unlinkCommand,
50
+ statusCommand,
51
+ logsCommand,
52
+ restartCommand,
53
+ ]) {
54
+ program.addCommand(command);
55
+ }
56
+ routeUsageErrors(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grayspar/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "The cloud for small software. Deploy your applications with one command, and share them with exactly the people you choose.",
5
5
  "repository": {
6
6
  "type": "git",