@shuffl/cli 0.3.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { chmod, mkdir, readFile, rename, rm, writeFile, } from 'node:fs/promises
3
3
  import { createRequire } from 'node:module';
4
4
  import { homedir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
- import { createShuffl, ORPCError } from '@shuffl/sdk';
6
+ import { createShuffl, createShufflCompany, ORPCError } from '@shuffl/sdk';
7
7
  import { commands } from './commands.generated.js';
8
8
  // package.json sits one level above both src/ and dist/.
9
9
  const { version } = createRequire(import.meta.url)('../package.json');
@@ -43,7 +43,7 @@ function parse(args) {
43
43
  const [name, ...value] = arg.slice(2).split('=');
44
44
  if (!name || name in flags)
45
45
  throw new Error(`Duplicate or invalid flag: ${arg}`);
46
- if (['help', 'json', 'token-stdin', 'version'].includes(name)) {
46
+ if (['company', 'help', 'json', 'token-stdin', 'version'].includes(name)) {
47
47
  if (value.length)
48
48
  throw new Error(`--${name} takes no value`);
49
49
  flags[name] = true;
@@ -72,6 +72,31 @@ function scalar(raw, schema) {
72
72
  return JSON.parse(raw);
73
73
  return raw;
74
74
  }
75
+ async function save(file, credentials) {
76
+ await mkdir(dirname(file), { mode: 0o700, recursive: true });
77
+ const temporary = `${file}.${randomUUID()}.tmp`;
78
+ try {
79
+ await writeFile(temporary, JSON.stringify(credentials) + '\n', {
80
+ flag: 'wx',
81
+ mode: 0o600,
82
+ });
83
+ await rename(temporary, file);
84
+ await chmod(file, 0o600);
85
+ }
86
+ finally {
87
+ await rm(temporary, { force: true });
88
+ }
89
+ }
90
+ async function load(file) {
91
+ try {
92
+ return JSON.parse(await readFile(file, 'utf8'));
93
+ }
94
+ catch (error) {
95
+ if (error.code !== 'ENOENT')
96
+ throw new Error('Cannot read CLI configuration. Run shuffl login again.');
97
+ return undefined;
98
+ }
99
+ }
75
100
  export async function runCli(args, io = {
76
101
  env: process.env,
77
102
  stderr: (s) => process.stderr.write(s),
@@ -81,8 +106,12 @@ export async function runCli(args, io = {
81
106
  const json = args.includes('--json');
82
107
  try {
83
108
  const { positional, flags } = parse(args);
84
- const configFile = join(io.env.SHUFFL_CONFIG_DIR ??
85
- join(io.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'shuffl'), 'config.json');
109
+ const configDirectory = io.env.SHUFFL_CONFIG_DIR ??
110
+ join(io.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'shuffl');
111
+ const configFile = join(configDirectory, 'config.json');
112
+ // #1692: a company key is saved apart from the workspace key, and only
113
+ // `shuffl company` commands send it.
114
+ const companyFile = join(configDirectory, 'company.json');
86
115
  if (flags.version && !positional.length) {
87
116
  io.stdout(json ? `${JSON.stringify({ version })}\n` : `${version}\n`);
88
117
  return 0;
@@ -90,27 +119,38 @@ export async function runCli(args, io = {
90
119
  if (flags.help || !positional.length) {
91
120
  const definition = commands.find((c) => c.command.join(' ') === positional.join(' '));
92
121
  io.stdout(definition
93
- ? `${definition.summary}\nPermission: ${definition.permission} (${definition.access})\n\nshuffl ${definition.command.join(' ')} [--input JSON | --input-file FILE] [--json]\n${Object.entries(definition.input?.properties ?? {})
122
+ ? `${definition.summary}\nPermission: ${definition.permission} (${definition.access})${definition.credential === 'company' ? '\nCredential: a company key (SHUFFL_COMPANY_KEY or shuffl login --company)' : ''}\n\nshuffl ${definition.command.join(' ')} [--input JSON | --input-file FILE] [--json]\n${Object.entries(definition.input?.properties ?? {})
94
123
  .map(([key, value]) => ` --${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)} <${value.type ?? 'json'}>`)
95
124
  .join('\n')}\n`
96
- : `Shuffl CLI ${version}\n\n shuffl login --base-url URL --token-stdin\n shuffl logout\n shuffl --version\n${commands.map((c) => ` shuffl ${c.command.join(' ')} — ${c.summary}`).join('\n')}\n\nUse --help on a command for inputs. Use --json for machine-readable output.\nCreate a key in Settings → API keys and choose the endpoint permissions this CLI needs.\n`);
125
+ : `Shuffl CLI ${version}\n\n shuffl login --base-url URL --token-stdin\n shuffl login --company --base-url URL --token-stdin\n shuffl logout [--company]\n shuffl --version\n${commands.map((c) => ` shuffl ${c.command.join(' ')} — ${c.summary}`).join('\n')}\n\nUse --help on a command for inputs. Use --json for machine-readable output.\nCreate a key in Settings → API keys and choose the endpoint permissions this CLI needs.\nshuffl company commands use a company key from Company settings → API keys instead.\n`);
97
126
  return 0;
98
127
  }
99
128
  if (positional.length === 1 && positional[0] === 'logout') {
100
- if (Object.keys(flags).some((k) => k !== 'json'))
129
+ if (Object.keys(flags).some((k) => !['company', 'json'].includes(k)))
101
130
  throw new Error('Unknown logout flag.');
102
- await rm(configFile, { force: true });
131
+ await rm(flags.company ? companyFile : configFile, { force: true });
103
132
  io.stdout(json
104
133
  ? '{}\n'
105
- : 'Signed out locally. Revoke the token in Settings to disable other copies.\n');
134
+ : `Signed out locally. Revoke the ${flags.company ? 'key in Company settings' : 'token in Settings'} to disable other copies.\n`);
106
135
  return 0;
107
136
  }
108
137
  if (positional.length === 1 && positional[0] === 'login') {
109
- if (Object.keys(flags).some((k) => !['base-url', 'token-stdin', 'json'].includes(k)))
138
+ if (Object.keys(flags).some((k) => !['base-url', 'company', 'token-stdin', 'json'].includes(k)))
110
139
  throw new Error('Unknown login flag.');
111
140
  if (!flags['token-stdin'] || typeof flags['base-url'] !== 'string')
112
141
  throw new Error('Use login --base-url URL --token-stdin. The token is read from standard input.');
113
142
  const token = (await io.stdin()).trim();
143
+ if (flags.company) {
144
+ if (!/^shuffl_company_[A-Za-z0-9_-]{43}$/.test(token))
145
+ throw new Error('Expected a Shuffl company key.');
146
+ const baseUrl = new URL(flags['base-url']).origin;
147
+ const company = await request(() => createShufflCompany({ baseUrl, token }).current());
148
+ await save(companyFile, { baseUrl, token });
149
+ io.stdout(json
150
+ ? JSON.stringify({ company }) + '\n'
151
+ : `Signed in to company ${company.organizationId} with ${company.permissions.length} permission${company.permissions.length === 1 ? '' : 's'}.\n`);
152
+ return 0;
153
+ }
114
154
  if (!/^shuffl_api_[A-Za-z0-9_-]{43}$/.test(token))
115
155
  throw new Error('Expected a Shuffl API token.');
116
156
  const baseUrl = new URL(flags['base-url']).origin;
@@ -119,19 +159,7 @@ export async function runCli(args, io = {
119
159
  token,
120
160
  });
121
161
  const team = await request(() => client.teams.current());
122
- await mkdir(dirname(configFile), { mode: 0o700, recursive: true });
123
- const temporary = `${configFile}.${randomUUID()}.tmp`;
124
- try {
125
- await writeFile(temporary, JSON.stringify({ baseUrl, token }) + '\n', {
126
- flag: 'wx',
127
- mode: 0o600,
128
- });
129
- await rename(temporary, configFile);
130
- await chmod(configFile, 0o600);
131
- }
132
- finally {
133
- await rm(temporary, { force: true });
134
- }
162
+ await save(configFile, { baseUrl, token });
135
163
  io.stdout(json ? JSON.stringify({ team }) + '\n' : `Signed in to ${team.name}.\n`);
136
164
  return 0;
137
165
  }
@@ -165,25 +193,26 @@ export async function runCli(args, io = {
165
193
  throw new Error(`Input ${key} was provided more than once.`);
166
194
  input[key] = scalar(flags[flag], fields[key]);
167
195
  }
168
- let saved;
169
- try {
170
- saved = JSON.parse(await readFile(configFile, 'utf8'));
171
- }
172
- catch (error) {
173
- if (error.code !== 'ENOENT')
174
- throw new Error('Cannot read CLI configuration. Run shuffl login again.');
175
- }
176
- const token = io.env.SHUFFL_TOKEN ?? saved?.token;
196
+ const company = command.credential === 'company';
197
+ const environmentToken = company
198
+ ? io.env.SHUFFL_COMPANY_KEY
199
+ : io.env.SHUFFL_TOKEN;
200
+ const saved = await load(company ? companyFile : configFile);
201
+ const token = environmentToken ?? saved?.token;
177
202
  const baseUrl = typeof flags['base-url'] === 'string'
178
203
  ? flags['base-url']
179
204
  : (io.env.SHUFFL_BASE_URL ?? saved?.baseUrl);
180
205
  if (!token || !baseUrl)
181
- throw new Error('Run shuffl login, or set SHUFFL_TOKEN and SHUFFL_BASE_URL.');
182
- if (!io.env.SHUFFL_TOKEN &&
206
+ throw new Error(company
207
+ ? 'Run shuffl login --company, or set SHUFFL_COMPANY_KEY and SHUFFL_BASE_URL.'
208
+ : 'Run shuffl login, or set SHUFFL_TOKEN and SHUFFL_BASE_URL.');
209
+ if (!environmentToken &&
183
210
  saved &&
184
211
  new URL(baseUrl).origin !== new URL(saved.baseUrl).origin)
185
212
  throw new Error('Saved credentials belong to a different server. Sign in to the target server first.');
186
- let method = createShuffl({ baseUrl, token });
213
+ let method = company
214
+ ? createShufflCompany({ baseUrl, token })
215
+ : createShuffl({ baseUrl, token });
187
216
  for (const name of command.path)
188
217
  method = method[name];
189
218
  const output = await request(() => method(input));
package/dist/types.d.ts CHANGED
@@ -13,4 +13,6 @@ export interface CommandDefinition {
13
13
  summary: string;
14
14
  scope: string | null;
15
15
  input: JsonSchema | null;
16
+ /** company: a company key command, under `shuffl company` (#1692). */
17
+ credential?: 'company';
16
18
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "@orpc/client": "1.15.3",
4
- "@shuffl/api-contracts": "0.3.0",
5
- "@shuffl/sdk": "0.3.0"
4
+ "@shuffl/api-contracts": "0.5.0",
5
+ "@shuffl/sdk": "0.5.0"
6
6
  },
7
7
  "description": "Shuffl command line: every public API operation as a scriptable command with JSON output.",
8
8
  "engines": {
@@ -33,7 +33,7 @@
33
33
  "sideEffects": false,
34
34
  "type": "module",
35
35
  "types": "./dist/index.d.ts",
36
- "version": "0.3.0",
36
+ "version": "0.5.0",
37
37
  "bin": {
38
38
  "shuffl": "./dist/bin.js"
39
39
  }