@grayspar/cli 0.0.0-stage → 0.0.2

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.
Files changed (47) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +76 -2
  3. package/dist/commands/access.js +103 -0
  4. package/dist/commands/deploy/archive.js +156 -0
  5. package/dist/commands/deploy/await-discovery.js +17 -0
  6. package/dist/commands/deploy/deploy-destination.js +101 -0
  7. package/dist/commands/deploy/deploy-flags.js +60 -0
  8. package/dist/commands/deploy/deploy-target.js +19 -0
  9. package/dist/commands/deploy/deploy.js +209 -0
  10. package/dist/commands/deploy/env-files.js +47 -0
  11. package/dist/commands/deploy/git-state.js +35 -0
  12. package/dist/commands/deploy/project-name.js +13 -0
  13. package/dist/commands/deploy/submit-deploy.js +127 -0
  14. package/dist/commands/deploy/variables-to-send.js +140 -0
  15. package/dist/commands/env.js +71 -0
  16. package/dist/commands/link.js +28 -0
  17. package/dist/commands/login/challenge.js +5 -0
  18. package/dist/commands/login/login.js +59 -0
  19. package/dist/commands/login/open-browser.js +15 -0
  20. package/dist/commands/login/sign-in-callback.js +37 -0
  21. package/dist/commands/logout.js +18 -0
  22. package/dist/commands/logs.js +89 -0
  23. package/dist/commands/open-path.js +45 -0
  24. package/dist/commands/project.js +22 -0
  25. package/dist/commands/restart.js +21 -0
  26. package/dist/commands/service.js +63 -0
  27. package/dist/commands/status.js +15 -0
  28. package/dist/commands/volume.js +87 -0
  29. package/dist/commands/whoami.js +16 -0
  30. package/dist/index.js +58 -0
  31. package/dist/lib/client.js +71 -0
  32. package/dist/lib/command-helpers.js +25 -0
  33. package/dist/lib/credentials.js +49 -0
  34. package/dist/lib/errors.js +12 -0
  35. package/dist/lib/follow-deployment.js +127 -0
  36. package/dist/lib/output.js +40 -0
  37. package/dist/lib/project-link.js +42 -0
  38. package/dist/lib/prompt.js +103 -0
  39. package/dist/lib/resolve-project.js +60 -0
  40. package/dist/lib/resolve-service.js +29 -0
  41. package/dist/lib/resolve-team.js +31 -0
  42. package/dist/lib/retry-transient.js +32 -0
  43. package/dist/lib/services-table.js +29 -0
  44. package/dist/lib/ui.js +58 -0
  45. package/dist/lib/usage-errors.js +43 -0
  46. package/dist/lib/version.js +12 -0
  47. package/package.json +52 -3
@@ -0,0 +1,32 @@
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. */
4
+ import { setTimeout as pause } from 'node:timers/promises';
5
+ import { CliError } from './errors.js';
6
+ const FIRST_DELAY_MS = 1000;
7
+ const LONGEST_DELAY_MS = 15_000;
8
+ const GIVE_UP_AFTER_MS = 2 * 60 * 1000;
9
+ export const realClock = { now: () => Date.now(), sleep: (ms) => pause(ms) };
10
+ export function isTransient(error) {
11
+ return (error instanceof CliError && (error.code === 'unreachable' || error.code === 'unavailable'));
12
+ }
13
+ export async function retryTransient(work, clock = realClock) {
14
+ let failingSince = null;
15
+ for (let attempt = 0;; attempt += 1) {
16
+ const started = clock.now();
17
+ try {
18
+ return await work(failingSince === null
19
+ ? undefined
20
+ : AbortSignal.timeout(Math.max(1, Math.round(failingSince + GIVE_UP_AFTER_MS - started))));
21
+ }
22
+ catch (error) {
23
+ if (!isTransient(error))
24
+ throw error;
25
+ failingSince ??= started;
26
+ if (clock.now() - failingSince >= GIVE_UP_AFTER_MS)
27
+ throw error;
28
+ const delay = Math.min(LONGEST_DELAY_MS, FIRST_DELAY_MS * 2 ** attempt);
29
+ await clock.sleep(delay * (0.5 + Math.random() / 2));
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,29 @@
1
+ import { color, table } from './ui.js';
2
+ const untinted = (text) => text;
3
+ const RUNTIME_STATUS = {
4
+ deploying: { label: 'Deploying', tint: untinted },
5
+ running: { label: 'Running', tint: color.ok },
6
+ restarting: { label: 'Restarting', tint: untinted },
7
+ crashed: { label: 'Crashed', tint: color.bad },
8
+ unknown: { label: 'Unknown', tint: untinted },
9
+ failed: { label: 'Failed', tint: color.bad },
10
+ };
11
+ export function servicesTable(project) {
12
+ const rows = project.services.map((service) => [
13
+ service.name,
14
+ service.pathPrefix === null
15
+ ? 'internal'
16
+ : service.pathPrefix === '/'
17
+ ? project.url
18
+ : `${project.url}${service.pathPrefix}`,
19
+ RUNTIME_STATUS[service.runtime].label,
20
+ ]);
21
+ return table(['SERVICE', 'URL', 'STATUS'], rows, (cell, column, row) => {
22
+ const service = project.services[row];
23
+ if (column === 1 && service?.pathPrefix !== null)
24
+ return color.link(cell);
25
+ if (column === 2 && service !== undefined)
26
+ return RUNTIME_STATUS[service.runtime].tint(cell);
27
+ return cell;
28
+ });
29
+ }
package/dist/lib/ui.js ADDED
@@ -0,0 +1,58 @@
1
+ /* One place owns how text output looks: every line starts at column 0, a result leads with ✓, ! or ✗ and still
2
+ reads without it, grey is kept for what sits beside an answer, and colour never reaches the JSON path or a pipe. */
3
+ import pc from 'picocolors';
4
+ export const color = {
5
+ heading: (text) => pc.bold(text),
6
+ dim: (text) => pc.dim(text),
7
+ ok: (text) => pc.green(text),
8
+ bad: (text) => pc.red(text),
9
+ link: (text) => pc.cyan(text),
10
+ };
11
+ export function receipt(message) {
12
+ return `${pc.green('✓')} ${message}`;
13
+ }
14
+ export function warning(message) {
15
+ return `${pc.yellow('!')} ${message}`;
16
+ }
17
+ export function failure(message) {
18
+ return `${pc.red('✗')} ${message}`;
19
+ }
20
+ export function table(header, rows, style = (cell) => cell) {
21
+ const widths = [];
22
+ for (const row of [header, ...rows]) {
23
+ row.forEach((cell, column) => {
24
+ widths[column] = Math.max(widths[column] ?? 0, cell.length);
25
+ });
26
+ }
27
+ const line = (cells, row) => cells
28
+ .map((cell, column) => {
29
+ const padded = column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0);
30
+ return row === null ? padded : style(padded, column, row);
31
+ })
32
+ .join(' ');
33
+ return [line(header, null), ...rows.map(line)].join('\n');
34
+ }
35
+ export function tableOrEmptyNote(header, rows, whenEmpty, style) {
36
+ return rows.length === 0 ? whenEmpty : table(header, rows, style);
37
+ }
38
+ const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
39
+ const CLEAR_LINE = '\r\x1b[2K';
40
+ export function spinner(text) {
41
+ let shown = text;
42
+ let frame = 0;
43
+ const draw = () => {
44
+ process.stderr.write(`${CLEAR_LINE}${pc.cyan(SPINNER_FRAMES.charAt(frame % SPINNER_FRAMES.length))} ${shown}`);
45
+ frame += 1;
46
+ };
47
+ draw();
48
+ const timer = setInterval(draw, 80);
49
+ return {
50
+ update: (next) => {
51
+ shown = next;
52
+ },
53
+ stop: () => {
54
+ clearInterval(timer);
55
+ process.stderr.write(CLEAR_LINE);
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,43 @@
1
+ import { CliError } from './errors.js';
2
+ import { exitWithError } from './output.js';
3
+ const HELP_LINE = 'Run it with --help to see what it takes.';
4
+ const FLAG_NAME_PATTERN = /^--[\w-]+/;
5
+ function match(error, pattern) {
6
+ return pattern.exec(error.message);
7
+ }
8
+ export function usageErrorFor(error) {
9
+ const flag = match(error, /option '([^']+)'/)?.[1];
10
+ const flagName = flag === undefined ? undefined : FLAG_NAME_PATTERN.exec(flag)?.[0];
11
+ switch (error.code) {
12
+ case 'commander.missingMandatoryOptionValue':
13
+ return new CliError('bad_usage', `This command needs ${flag ?? 'a flag'}.\n${HELP_LINE}`, flagName);
14
+ case 'commander.optionMissingArgument':
15
+ return new CliError('bad_usage', `${flag ?? 'A flag'} needs a value.\n${HELP_LINE}`, flagName);
16
+ case 'commander.invalidArgument': {
17
+ const choices = match(error, /Allowed choices are ([^.]+)/)?.[1];
18
+ return new CliError('bad_usage', `${flag ?? 'That'} ${choices === undefined ? "doesn't take that value" : `takes one of ${choices}`}.\n${HELP_LINE}`, flagName);
19
+ }
20
+ case 'commander.unknownOption':
21
+ return new CliError('bad_usage', `${match(error, /unknown option '([^']+)'/)?.[1] ?? 'That flag'} isn't a flag of this command.\n${HELP_LINE}`);
22
+ case 'commander.unknownCommand':
23
+ return new CliError('bad_usage', `${match(error, /unknown command '([^']+)'/)?.[1] ?? 'That'} isn't a command.\nRun grayspar --help to see the commands.`);
24
+ case 'commander.missingArgument':
25
+ return new CliError('bad_usage', `This command needs ${match(error, /argument '([^']+)'/)?.[1] ?? 'more'} after its name.\n${HELP_LINE}`);
26
+ case 'commander.excessArguments':
27
+ return new CliError('bad_usage', `This command was given more than it takes.\n${HELP_LINE}`);
28
+ default:
29
+ return new CliError('bad_usage', `That command line isn't valid.\n${HELP_LINE}`);
30
+ }
31
+ }
32
+ export function routeUsageErrors(program) {
33
+ const refuse = (error) => {
34
+ if (error.exitCode === 0 || error.code === 'commander.help')
35
+ process.exit(error.exitCode);
36
+ exitWithError(program.opts().json === true ? 'json' : 'text', usageErrorFor(error));
37
+ };
38
+ const route = (command) => {
39
+ command.exitOverride(refuse).configureOutput({ outputError: () => undefined });
40
+ command.commands.forEach(route);
41
+ };
42
+ route(program);
43
+ }
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from 'node:fs';
2
+ function readVersion() {
3
+ const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
4
+ if (typeof manifest === 'object' &&
5
+ manifest !== null &&
6
+ 'version' in manifest &&
7
+ typeof manifest.version === 'string') {
8
+ return manifest.version;
9
+ }
10
+ throw new Error('package.json of the CLI has no version');
11
+ }
12
+ export const cliVersion = readVersion();
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@grayspar/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.2",
4
+ "description": "The cloud for small software. Deploy your applications with one command, and share them with exactly the people you choose.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/grayspar/Grayspar.git",
8
+ "directory": "packages/cli"
9
+ },
10
+ "keywords": [
11
+ "grayspar",
12
+ "deploy",
13
+ "hosting",
14
+ "cloud",
15
+ "cli",
16
+ "private",
17
+ "sharing",
18
+ "access control"
19
+ ],
20
+ "homepage": "https://grayspar.com",
21
+ "bugs": {
22
+ "email": "support@grayspar.com"
23
+ },
24
+ "license": "Apache-2.0",
25
+ "type": "module",
26
+ "bin": {
27
+ "grayspar": "./dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@inquirer/core": "10.3.2",
40
+ "@inquirer/select": "4.4.2",
41
+ "commander": "14.0.2",
42
+ "ignore": "7.0.10",
43
+ "openapi-fetch": "0.17.0",
44
+ "picocolors": "1.1.1",
45
+ "tar": "7.5.22"
46
+ },
47
+ "devDependencies": {
48
+ "@grayspar/api-types": "0.0.0"
49
+ },
50
+ "scripts": {
51
+ "prebuild": "pnpm --filter @grayspar/api generate",
52
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
53
+ "typecheck": "tsc --noEmit"
54
+ }
6
55
  }