@medplum/cli 2.0.21 → 2.0.23

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.
@@ -4,15 +4,17 @@
4
4
  var core = require('@medplum/core');
5
5
  var commander = require('commander');
6
6
  var dotenv = require('dotenv');
7
+ var os = require('os');
8
+ var child_process = require('child_process');
9
+ var http = require('http');
10
+ var fs = require('fs');
11
+ var path = require('path');
7
12
  var clientCloudformation = require('@aws-sdk/client-cloudformation');
8
13
  var clientCloudfront = require('@aws-sdk/client-cloudfront');
9
14
  var clientEcs = require('@aws-sdk/client-ecs');
10
15
  var clientS3 = require('@aws-sdk/client-s3');
11
16
  var fastGlob = require('fast-glob');
12
- var fs = require('fs');
13
17
  var fetch$1 = require('node-fetch');
14
- var os = require('os');
15
- var path = require('path');
16
18
  var promises = require('stream/promises');
17
19
  var tar = require('tar');
18
20
  var clientAcm = require('@aws-sdk/client-acm');
@@ -20,22 +22,99 @@ var clientSsm = require('@aws-sdk/client-ssm');
20
22
  var clientSts = require('@aws-sdk/client-sts');
21
23
  var crypto = require('crypto');
22
24
  var readline = require('readline');
23
- var child_process = require('child_process');
24
- var http = require('http');
25
+
26
+ function createMedplumCommand(name) {
27
+ return new commander.Command(name)
28
+ .option('--client-id <clientId>', 'FHIR server client id')
29
+ .option('--client-secret <clientSecret>', 'FHIR server client secret')
30
+ .option('--base-url <baseUrl>', 'FHIR server base url')
31
+ .option('--token-url <tokenUrl>', 'FHIR server token url')
32
+ .option('--authorize-url <authorizeUrl>', 'FHIR server authorize url')
33
+ .option('--fhir-url-path <fhirUrlPath>', 'FHIR server url path');
34
+ }
35
+
36
+ class FileSystemStorage extends core.ClientStorage {
37
+ constructor() {
38
+ super();
39
+ this.dirName = path.resolve(os.homedir(), '.medplum');
40
+ this.fileName = path.resolve(this.dirName, 'credentials');
41
+ }
42
+ clear() {
43
+ this.writeFile({});
44
+ }
45
+ getString(key) {
46
+ return this.readFile()?.[key];
47
+ }
48
+ setString(key, value) {
49
+ const data = this.readFile() || {};
50
+ if (value) {
51
+ data[key] = value;
52
+ }
53
+ else {
54
+ delete data[key];
55
+ }
56
+ this.writeFile(data);
57
+ }
58
+ readFile() {
59
+ if (fs.existsSync(this.fileName)) {
60
+ return JSON.parse(fs.readFileSync(this.fileName, 'utf8'));
61
+ }
62
+ return undefined;
63
+ }
64
+ writeFile(data) {
65
+ if (!fs.existsSync(this.dirName)) {
66
+ fs.mkdirSync(this.dirName);
67
+ }
68
+ fs.writeFileSync(this.fileName, JSON.stringify(data, null, 2), 'utf8');
69
+ }
70
+ }
71
+
72
+ async function createMedplumClient(options) {
73
+ const baseUrl = options.baseUrl ?? process.env['MEDPLUM_BASE_URL'] ?? 'https://api.medplum.com/';
74
+ const fhirUrlPath = options.fhirUrlPath ?? process.env['MEDPLUM_FHIR_URL_PATH'] ?? '';
75
+ const accessToken = options.accessToken ?? process.env['MEDPLUM_CLIENT_ACCESS_TOKEN'] ?? '';
76
+ const tokenUrl = options.tokenUrl ?? process.env['MEDPLUM_TOKEN_URL'] ?? '';
77
+ const authorizeUrl = options.authorizeUrl ?? process.env['MEDPLUM_AUTHORIZE_URL'] ?? '';
78
+ const fetchApi = options.fetch ?? fetch;
79
+ const medplumClient = new core.MedplumClient({
80
+ fetch: fetchApi,
81
+ baseUrl,
82
+ tokenUrl,
83
+ fhirUrlPath,
84
+ authorizeUrl,
85
+ storage: new FileSystemStorage(),
86
+ onUnauthenticated: onUnauthenticated,
87
+ });
88
+ if (accessToken) {
89
+ medplumClient.setAccessToken(accessToken);
90
+ }
91
+ const clientId = options.clientId || process.env['MEDPLUM_CLIENT_ID'];
92
+ const clientSecret = options.clientSecret || process.env['MEDPLUM_CLIENT_SECRET'];
93
+ if (clientId && clientSecret) {
94
+ medplumClient.setBasicAuth(clientId, clientSecret);
95
+ await medplumClient.startClientLogin(clientId, clientSecret);
96
+ }
97
+ return medplumClient;
98
+ }
99
+ function onUnauthenticated() {
100
+ console.log('Unauthenticated: run `npx medplum login` to sign in');
101
+ }
25
102
 
26
103
  const clientId = 'medplum-cli';
27
104
  const redirectUri = 'http://localhost:9615';
28
- const login = new commander.Command('login');
29
- const whoami = new commander.Command('whoami');
30
- login.action(async () => {
31
- await startLogin(exports.medplum);
105
+ const login = createMedplumCommand('login');
106
+ const whoami = createMedplumCommand('whoami');
107
+ login.action(async (options) => {
108
+ const medplum = await createMedplumClient(options);
109
+ await startLogin(medplum);
32
110
  });
33
- whoami.action(() => {
34
- printMe(exports.medplum);
111
+ whoami.action(async (options) => {
112
+ const medplum = await createMedplumClient(options);
113
+ printMe(medplum);
35
114
  });
36
115
  async function startLogin(medplum) {
37
116
  await startWebServer(medplum);
38
- const loginUrl = new URL('/oauth2/authorize', medplum.getBaseUrl());
117
+ const loginUrl = new URL(medplum.getAuthorizeUrl());
39
118
  loginUrl.searchParams.set('client_id', clientId);
40
119
  loginUrl.searchParams.set('redirect_uri', redirectUri);
41
120
  loginUrl.searchParams.set('scope', 'openid');
@@ -204,7 +283,6 @@ function getEcsServiceName(resource) {
204
283
 
205
284
  /**
206
285
  * The AWS "describe" command prints details about a Medplum CloudFormation stack.
207
- *
208
286
  * @param tag The Medplum stack tag.
209
287
  */
210
288
  async function describeStacksCommand(tag) {
@@ -343,7 +421,6 @@ function escapeRegex(str) {
343
421
  * Expanding archive files without controlling resource consumption is security-sensitive
344
422
  *
345
423
  * See: https://sonarcloud.io/organizations/medplum/rules?open=typescript%3AS5042&rule_key=typescript%3AS5042
346
- *
347
424
  * @param destinationDir The destination directory where all files will be extracted.
348
425
  * @returns A tar file extractor.
349
426
  */
@@ -409,6 +486,8 @@ async function updateAppCommand(tag) {
409
486
  * Returns NPM package metadata for a given package name.
410
487
  * See: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#getpackageversion
411
488
  * @param packageName The npm package name.
489
+ * @param version The npm package version string.
490
+ * @returns The package.json metadata content.
412
491
  */
413
492
  async function getNpmPackageMetadata(packageName, version) {
414
493
  const url = `https://registry.npmjs.org/${packageName}/${version}`;
@@ -508,6 +587,11 @@ async function uploadAppToS3(tmpDir, bucketName) {
508
587
  /**
509
588
  * Uploads a directory of files to S3.
510
589
  * @param options The upload options such as bucket name, content type, and cache control.
590
+ * @param options.rootDir The root directory of the upload.
591
+ * @param options.bucketName The destination bucket name.
592
+ * @param options.fileNamePattern The glob file pattern to upload.
593
+ * @param options.contentType The content type MIME type.
594
+ * @param options.cached True to mark as public and cached forever.
511
595
  */
512
596
  async function uploadFolderToS3(options) {
513
597
  const items = fastGlob.sync(options.fileNamePattern, { cwd: options.rootDir });
@@ -519,6 +603,10 @@ async function uploadFolderToS3(options) {
519
603
  * Uploads a file to S3.
520
604
  * @param filePath The file path.
521
605
  * @param options The upload options such as bucket name, content type, and cache control.
606
+ * @param options.rootDir The root directory of the upload.
607
+ * @param options.bucketName The destination bucket name.
608
+ * @param options.contentType The content type MIME type.
609
+ * @param options.cached True to mark as public and cached forever.
522
610
  */
523
611
  async function uploadFileToS3(filePath, options) {
524
612
  const fileStream = fs.createReadStream(filePath);
@@ -561,7 +649,6 @@ async function createInvalidation(distributionId) {
561
649
  /**
562
650
  * The AWS "update-server" command updates the Medplum server in a Medplum CloudFormation stack.
563
651
  * @param tag The Medplum stack tag.
564
- * @returns
565
652
  */
566
653
  async function updateServerCommand(tag) {
567
654
  const details = await getStackByTag(tag);
@@ -800,15 +887,26 @@ async function initStackCommand() {
800
887
  print(' https://www.medplum.com/docs/self-hosting/install-on-aws');
801
888
  print('');
802
889
  }
803
- /** Prints to stdout. */
890
+ /**
891
+ * Prints to stdout.
892
+ * @param text The text to print.
893
+ */
804
894
  function print(text) {
805
895
  terminal.write(text + '\n');
806
896
  }
807
- /** Prints a header with extra line spacing. */
897
+ /**
898
+ * Prints a header with extra line spacing.
899
+ * @param text The text to print.
900
+ */
808
901
  function header(text) {
809
902
  print('\n' + text + '\n');
810
903
  }
811
- /** Prints a question and waits for user input. */
904
+ /**
905
+ * Prints a question and waits for user input.
906
+ * @param text The question text to print.
907
+ * @param defaultValue Optional default value.
908
+ * @returns The selected value, or default value on empty selection.
909
+ */
812
910
  function ask(text, defaultValue = '') {
813
911
  return new Promise((resolve) => {
814
912
  terminal.question(text + (defaultValue ? ' (' + defaultValue + ')' : '') + ' ', (answer) => {
@@ -816,7 +914,13 @@ function ask(text, defaultValue = '') {
816
914
  });
817
915
  });
818
916
  }
819
- /** Prints a question and waits for user to choose one of the provided options. */
917
+ /**
918
+ * Prints a question and waits for user to choose one of the provided options.
919
+ * @param text The prompt text to print.
920
+ * @param options The list of options that the user can select.
921
+ * @param defaultValue Optional default value.
922
+ * @returns The selected value, or default value on empty selection.
923
+ */
820
924
  async function choose(text, options, defaultValue = '') {
821
925
  const str = text + ' [' + options.map((o) => (o === defaultValue ? '(' + o + ')' : o)).join('|') + ']';
822
926
  // eslint-disable-next-line no-constant-condition
@@ -828,15 +932,28 @@ async function choose(text, options, defaultValue = '') {
828
932
  print('Please choose one of the following options: ' + options.join(', '));
829
933
  }
830
934
  }
831
- /** Prints a question and waits for the user to choose a valid integer option. */
935
+ /**
936
+ * Prints a question and waits for the user to choose a valid integer option.
937
+ * @param text The prompt text to print.
938
+ * @param options The list of options that the user can select.
939
+ * @param defaultValue Optional default value.
940
+ * @returns The selected value, or default value on empty selection.
941
+ */
832
942
  async function chooseInt(text, options, defaultValue = 0) {
833
943
  return parseInt(await choose(text, options.map((o) => o.toString()), defaultValue.toString()));
834
944
  }
835
- /** Prints a question and waits for the user to choose yes or no. */
945
+ /**
946
+ * Prints a question and waits for the user to choose yes or no.
947
+ * @param text The question to print.
948
+ * @returns true on accept or false on reject.
949
+ */
836
950
  async function yesOrNo(text) {
837
951
  return (await choose(text, ['y', 'n'])).toLowerCase() === 'y';
838
952
  }
839
- /** Prints a question and waits for the user to confirm yes. Throws error on no, and exits the program. */
953
+ /**
954
+ * Prints a question and waits for the user to confirm yes. Throws error on no, and exits the program.
955
+ * @param text The prompt text to print.
956
+ */
840
957
  async function checkOk(text) {
841
958
  if (!(await yesOrNo(text))) {
842
959
  print('Exiting...');
@@ -908,7 +1025,6 @@ async function listCertificates(region) {
908
1025
  * 1. If the certificate already exists, return the ARN.
909
1026
  * 2. If the certificate does not exist, and the user wants to create a new certificate, create it and return the ARN.
910
1027
  * 3. If the certificate does not exist, and the user does not want to create a new certificate, return a placeholder.
911
- *
912
1028
  * @param config In-progress config settings.
913
1029
  * @param allCerts List of all existing certificates.
914
1030
  * @param region The AWS region where the certificate is needed.
@@ -963,7 +1079,6 @@ async function requestCert(region, domain) {
963
1079
  * 3. It must be a 2048-bit key pair.
964
1080
  *
965
1081
  * See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs
966
- *
967
1082
  * @returns A new signing key.
968
1083
  */
969
1084
  function generateSigningKey() {
@@ -1029,31 +1144,37 @@ aws
1029
1144
  aws.command('update-server').description('Update the server image').argument('<tag>').action(updateServerCommand);
1030
1145
  aws.command('update-app').description('Update the app site').argument('<tag>').action(updateAppCommand);
1031
1146
 
1032
- const bot = new commander.Command('bot');
1147
+ const botSaveCommand = createMedplumCommand('save');
1148
+ const botDeployCommand = createMedplumCommand('deploy');
1149
+ const botCreateCommand = createMedplumCommand('create');
1150
+ const bot = new commander.Command('bot')
1151
+ .addCommand(botSaveCommand)
1152
+ .addCommand(botDeployCommand)
1153
+ .addCommand(botCreateCommand);
1033
1154
  // Commands to deprecate
1034
- const saveBotDeprecate = new commander.Command('save-bot');
1035
- const deployBotDeprecate = new commander.Command('deploy-bot');
1036
- const createBotDeprecate = new commander.Command('create-bot');
1037
- bot
1038
- .command('save')
1155
+ const saveBotDeprecate = createMedplumCommand('save-bot');
1156
+ const deployBotDeprecate = createMedplumCommand('deploy-bot');
1157
+ const createBotDeprecate = createMedplumCommand('create-bot');
1158
+ botSaveCommand
1039
1159
  .description('Saving the bot')
1040
1160
  .argument('<botName>')
1041
- .action(async (botName) => {
1042
- await botWrapper(exports.medplum, botName);
1161
+ .action(async (botName, options) => {
1162
+ const medplum = await createMedplumClient(options);
1163
+ await botWrapper(medplum, botName);
1043
1164
  });
1044
- bot
1045
- .command('deploy')
1165
+ botDeployCommand
1046
1166
  .description('Deploy the app to AWS')
1047
1167
  .argument('<botName>')
1048
- .action(async (botName) => {
1049
- await botWrapper(exports.medplum, botName, true);
1168
+ .action(async (botName, options) => {
1169
+ const medplum = await createMedplumClient(options);
1170
+ await botWrapper(medplum, botName, true);
1050
1171
  });
1051
- bot
1052
- .command('create')
1172
+ botCreateCommand
1053
1173
  .arguments('<botName> <projectId> <sourceFile> <distFile>')
1054
1174
  .description('Creating a bot')
1055
- .action(async (botName, projectId, sourceFile, distFile) => {
1056
- await createBot(exports.medplum, [botName, projectId, sourceFile, distFile]);
1175
+ .action(async (botName, projectId, sourceFile, distFile, options) => {
1176
+ const medplum = await createMedplumClient(options);
1177
+ await createBot(medplum, [botName, projectId, sourceFile, distFile]);
1057
1178
  });
1058
1179
  async function botWrapper(medplum, botName, deploy = false) {
1059
1180
  const botConfigs = readBotConfigs(botName);
@@ -1070,55 +1191,63 @@ async function botWrapper(medplum, botName, deploy = false) {
1070
1191
  saveBotDeprecate
1071
1192
  .description('Saves the bot')
1072
1193
  .argument('<botName>')
1073
- .action(async (botName) => {
1074
- await botWrapper(exports.medplum, botName);
1194
+ .action(async (botName, options) => {
1195
+ const medplum = await createMedplumClient(options);
1196
+ await botWrapper(medplum, botName);
1075
1197
  });
1076
1198
  deployBotDeprecate
1077
1199
  .description('Deploy the bot to AWS')
1078
1200
  .argument('<botName>')
1079
- .action(async (botName) => {
1080
- await botWrapper(exports.medplum, botName, true);
1201
+ .action(async (botName, options) => {
1202
+ const medplum = await createMedplumClient(options);
1203
+ await botWrapper(medplum, botName, true);
1081
1204
  });
1082
1205
  createBotDeprecate
1083
1206
  .arguments('<botName> <projectId> <sourceFile> <distFile>')
1084
1207
  .description('Creates and saves the bot')
1085
- .action(async (botName, projectId, sourceFile, distFile) => {
1086
- await createBot(exports.medplum, [botName, projectId, sourceFile, distFile]);
1208
+ .action(async (botName, projectId, sourceFile, distFile, options) => {
1209
+ const medplum = await createMedplumClient(options);
1210
+ await createBot(medplum, [botName, projectId, sourceFile, distFile]);
1087
1211
  });
1088
1212
 
1089
- const bulk = new commander.Command('bulk');
1090
- bulk
1091
- .command('export')
1092
- .option('-e, --exportLevel <exportLevel>', 'Optional export level. Defaults to system level export. "Group/:id" - Group of Patients, "Patient" - All Patients.')
1213
+ const bulkExportCommand = createMedplumCommand('export');
1214
+ const bulkImportCommand = createMedplumCommand('import');
1215
+ const bulk = new commander.Command('bulk').addCommand(bulkExportCommand).addCommand(bulkImportCommand);
1216
+ bulkExportCommand
1217
+ .option('-e, --export-level <exportLevel>', 'Optional export level. Defaults to system level export. "Group/:id" - Group of Patients, "Patient" - All Patients.')
1093
1218
  .option('-t, --types <types>', 'optional resource types to export')
1094
1219
  .option('-s, --since <since>', 'optional Resources will be included in the response if their state has changed after the supplied time (e.g. if Resource.meta.lastUpdated is later than the supplied _since time).')
1095
- .action(async ({ exportLevel, types, since }) => {
1096
- const response = await exports.medplum.bulkExport(exportLevel, types, since);
1220
+ .action(async (options) => {
1221
+ const { exportLevel, types, since } = options;
1222
+ const medplum = await createMedplumClient(options);
1223
+ const response = await medplum.bulkExport(exportLevel, types, since);
1097
1224
  response.output?.forEach(async ({ type, url }) => {
1098
- const data = await exports.medplum.download(url);
1099
- const fileName = `${type}.ndjson`;
1225
+ const fileUrl = new URL(url);
1226
+ const data = await medplum.download(url);
1227
+ const fileName = `${type}_${fileUrl.pathname}`.replace(/[^a-zA-Z0-9]+/g, '_') + '.ndjson';
1100
1228
  fs.writeFile(`${fileName}`, await data.text(), () => {
1101
1229
  console.log(`${fileName} is created`);
1102
1230
  });
1103
1231
  });
1104
1232
  });
1105
- bulk
1106
- .command('import')
1233
+ bulkImportCommand
1107
1234
  .argument('<filename>', 'File Name')
1108
- .option('--numResourcesPerRequest <numResourcesPerRequest>', 'optional number of resources to import per batch request. Defaults to 25.', '25')
1235
+ .option('--num-resources-per-request <numResourcesPerRequest>', 'optional number of resources to import per batch request. Defaults to 25.', '25')
1236
+ .option('--add-extensions-for-missing-values', 'optional flag to add extensions for missing values in a resource', false)
1109
1237
  .action(async (fileName, options) => {
1110
1238
  const path$1 = path.resolve(process.cwd(), fileName);
1111
- const { numResourcesPerRequest } = options;
1112
- await importFile(path$1, parseInt(numResourcesPerRequest));
1239
+ const { numResourcesPerRequest, addExtensionsForMissingValues } = options;
1240
+ const medplum = await createMedplumClient(options);
1241
+ await importFile(path$1, parseInt(numResourcesPerRequest), medplum, addExtensionsForMissingValues);
1113
1242
  });
1114
- async function importFile(path, numResourcesPerRequest) {
1243
+ async function importFile(path, numResourcesPerRequest, medplum, addExtensionsForMissingValues) {
1115
1244
  let entries = [];
1116
1245
  const fileStream = fs.createReadStream(path);
1117
1246
  const rl = readline.createInterface({
1118
1247
  input: fileStream,
1119
1248
  });
1120
1249
  for await (const line of rl) {
1121
- const resource = JSON.parse(line);
1250
+ const resource = parseResource(line, addExtensionsForMissingValues);
1122
1251
  entries.push({
1123
1252
  resource: resource,
1124
1253
  request: {
@@ -1127,16 +1256,16 @@ async function importFile(path, numResourcesPerRequest) {
1127
1256
  },
1128
1257
  });
1129
1258
  if (entries.length % numResourcesPerRequest === 0) {
1130
- await sendBatchEntries(entries);
1259
+ await sendBatchEntries(entries, medplum);
1131
1260
  entries = [];
1132
1261
  }
1133
1262
  }
1134
1263
  if (entries.length > 0) {
1135
- await sendBatchEntries(entries);
1264
+ await sendBatchEntries(entries, medplum);
1136
1265
  }
1137
1266
  }
1138
- async function sendBatchEntries(entries) {
1139
- const result = await exports.medplum.executeBatch({
1267
+ async function sendBatchEntries(entries, medplum) {
1268
+ const result = await medplum.executeBatch({
1140
1269
  resourceType: 'Bundle',
1141
1270
  type: 'transaction',
1142
1271
  entry: entries,
@@ -1145,13 +1274,53 @@ async function sendBatchEntries(entries) {
1145
1274
  prettyPrint(resultEntry.response);
1146
1275
  });
1147
1276
  }
1277
+ function parseResource(jsonString, addExtensionsForMissingValues) {
1278
+ const resource = JSON.parse(jsonString);
1279
+ if (addExtensionsForMissingValues) {
1280
+ return addExtensionsForMissingValuesResource(resource);
1281
+ }
1282
+ return resource;
1283
+ }
1284
+ function addExtensionsForMissingValuesResource(resource) {
1285
+ if (resource.resourceType === 'ExplanationOfBenefit') {
1286
+ return addExtensionsForMissingValuesExplanationOfBenefits(resource);
1287
+ }
1288
+ return resource;
1289
+ }
1290
+ function addExtensionsForMissingValuesExplanationOfBenefits(resource) {
1291
+ if (!resource.provider) {
1292
+ resource.provider = getUnmappedExtension();
1293
+ }
1294
+ resource.item?.forEach((item) => {
1295
+ if (!item?.productOrService) {
1296
+ item.productOrService = getUnmappedExtension();
1297
+ }
1298
+ });
1299
+ return resource;
1300
+ }
1301
+ function getUnmappedExtension() {
1302
+ return {
1303
+ extension: [
1304
+ {
1305
+ url: 'https://g.co/unmapped-by-bcda',
1306
+ valueString: 'This is a required FHIR R4 Field, but not mapped by BCDA, which is why we expect it to be empty.',
1307
+ },
1308
+ ],
1309
+ };
1310
+ }
1148
1311
 
1149
- const project = new commander.Command('project');
1150
- project
1151
- .command('list')
1152
- .description('List of current projects')
1153
- .action(async () => {
1154
- projectList(exports.medplum);
1312
+ const projectListCommand = createMedplumCommand('list');
1313
+ const projectCurrentCommand = createMedplumCommand('current');
1314
+ const projectSwitchCommand = createMedplumCommand('switch');
1315
+ const projectInviteCommand = createMedplumCommand('invite');
1316
+ const project = new commander.Command('project')
1317
+ .addCommand(projectListCommand)
1318
+ .addCommand(projectCurrentCommand)
1319
+ .addCommand(projectSwitchCommand)
1320
+ .addCommand(projectInviteCommand);
1321
+ projectListCommand.description('List of current projects').action(async (options) => {
1322
+ const medplum = await createMedplumClient(options);
1323
+ projectList(medplum);
1155
1324
  });
1156
1325
  function projectList(medplum) {
1157
1326
  const logins = medplum.getLogins();
@@ -1160,25 +1329,22 @@ function projectList(medplum) {
1160
1329
  .join('\n\n');
1161
1330
  console.log(projects);
1162
1331
  }
1163
- project
1164
- .command('current')
1165
- .description('Project you are currently on')
1166
- .action(() => {
1167
- const login = exports.medplum.getActiveLogin();
1332
+ projectCurrentCommand.description('Project you are currently on').action(async (options) => {
1333
+ const medplum = await createMedplumClient(options);
1334
+ const login = medplum.getActiveLogin();
1168
1335
  if (!login) {
1169
1336
  throw new Error('Unauthenticated: run `npx medplum login` to login');
1170
1337
  }
1171
1338
  console.log(`${login.project.display} (${login.project.reference})`);
1172
1339
  });
1173
- project
1174
- .command('switch')
1340
+ projectSwitchCommand
1175
1341
  .description('Switching to another project from the current one')
1176
1342
  .argument('<projectId>')
1177
- .action(async (projectId) => {
1178
- await switchProject(exports.medplum, projectId);
1343
+ .action(async (projectId, options) => {
1344
+ const medplum = await createMedplumClient(options);
1345
+ await switchProject(medplum, projectId);
1179
1346
  });
1180
- project
1181
- .command('invite')
1347
+ projectInviteCommand
1182
1348
  .description('Invite a member to your current project (run npx medplum project current to confirm)')
1183
1349
  .arguments('<firstName> <lastName> <email>')
1184
1350
  .option('--send-email', 'If you want to send the email when inviting the user')
@@ -1187,7 +1353,8 @@ project
1187
1353
  .choices(['Practitioner', 'Patient', 'RelatedPerson'])
1188
1354
  .default('Practitioner'))
1189
1355
  .action(async (firstName, lastName, email, options) => {
1190
- const login = exports.medplum.getActiveLogin();
1356
+ const medplum = await createMedplumClient(options);
1357
+ const login = medplum.getActiveLogin();
1191
1358
  if (!login) {
1192
1359
  throw new Error('Unauthenticated: run `npx medplum login` to login');
1193
1360
  }
@@ -1203,7 +1370,7 @@ project
1203
1370
  sendEmail: !!options.sendEmail,
1204
1371
  admin: !!options.admin,
1205
1372
  };
1206
- await inviteUser(projectId, inviteBody);
1373
+ await inviteUser(projectId, inviteBody, medplum);
1207
1374
  });
1208
1375
  async function switchProject(medplum, projectId) {
1209
1376
  const logins = medplum.getLogins();
@@ -1216,9 +1383,9 @@ async function switchProject(medplum, projectId) {
1216
1383
  console.log(`Switched to project ${projectId}\n`);
1217
1384
  }
1218
1385
  }
1219
- async function inviteUser(projectId, inviteBody) {
1386
+ async function inviteUser(projectId, inviteBody, medplum) {
1220
1387
  try {
1221
- await exports.medplum.invite(projectId, inviteBody);
1388
+ await medplum.invite(projectId, inviteBody);
1222
1389
  if (inviteBody.sendEmail) {
1223
1390
  console.log('Email sent');
1224
1391
  }
@@ -1229,19 +1396,21 @@ async function inviteUser(projectId, inviteBody) {
1229
1396
  }
1230
1397
  }
1231
1398
 
1232
- const deleteObject = new commander.Command('delete');
1233
- const get = new commander.Command('get');
1234
- const patch = new commander.Command('patch');
1235
- const post = new commander.Command('post');
1236
- const put = new commander.Command('put');
1237
- deleteObject.argument('<url>', 'Resource/$id').action(async (url) => {
1238
- prettyPrint(await exports.medplum.delete(cleanUrl(url)));
1399
+ const deleteObject = createMedplumCommand('delete');
1400
+ const get = createMedplumCommand('get');
1401
+ const patch = createMedplumCommand('patch');
1402
+ const post = createMedplumCommand('post');
1403
+ const put = createMedplumCommand('put');
1404
+ deleteObject.argument('<url>', 'Resource/$id').action(async (url, options) => {
1405
+ const medplum = await createMedplumClient(options);
1406
+ prettyPrint(await medplum.delete(cleanUrl(url)));
1239
1407
  });
1240
1408
  get
1241
1409
  .argument('<url>', 'Resource/$id')
1242
1410
  .option('--as-transaction', 'Print out the bundle as a transaction type')
1243
1411
  .action(async (url, options) => {
1244
- const response = await exports.medplum.get(cleanUrl(url));
1412
+ const medplum = await createMedplumClient(options);
1413
+ const response = await medplum.get(cleanUrl(url));
1245
1414
  if (options.asTransaction) {
1246
1415
  prettyPrint(core.convertToTransactionBundle(response));
1247
1416
  }
@@ -1249,14 +1418,17 @@ get
1249
1418
  prettyPrint(response);
1250
1419
  }
1251
1420
  });
1252
- patch.arguments('<url> <body>').action(async (url, body) => {
1253
- prettyPrint(await exports.medplum.patch(cleanUrl(url), parseBody(body)));
1421
+ patch.arguments('<url> <body>').action(async (url, body, options) => {
1422
+ const medplum = await createMedplumClient(options);
1423
+ prettyPrint(await medplum.patch(cleanUrl(url), parseBody(body)));
1254
1424
  });
1255
- post.arguments('<url> <body>').action(async (url, body) => {
1256
- prettyPrint(await exports.medplum.post(cleanUrl(url), parseBody(body)));
1425
+ post.arguments('<url> <body>').action(async (url, body, options) => {
1426
+ const medplum = await createMedplumClient(options);
1427
+ prettyPrint(await medplum.post(cleanUrl(url), parseBody(body)));
1257
1428
  });
1258
- put.arguments('<url> <body>').action(async (url, body) => {
1259
- prettyPrint(await exports.medplum.put(cleanUrl(url), parseBody(body)));
1429
+ put.arguments('<url> <body>').action(async (url, body, options) => {
1430
+ const medplum = await createMedplumClient(options);
1431
+ prettyPrint(await medplum.put(cleanUrl(url), parseBody(body)));
1260
1432
  });
1261
1433
  function parseBody(input) {
1262
1434
  if (!input) {
@@ -1279,51 +1451,7 @@ function cleanUrl(input) {
1279
1451
  return 'fhir/R4/' + input;
1280
1452
  }
1281
1453
 
1282
- class FileSystemStorage extends core.ClientStorage {
1283
- constructor() {
1284
- super();
1285
- this.dirName = path.resolve(os.homedir(), '.medplum');
1286
- this.fileName = path.resolve(this.dirName, 'credentials');
1287
- }
1288
- clear() {
1289
- this.writeFile({});
1290
- }
1291
- getString(key) {
1292
- return this.readFile()?.[key];
1293
- }
1294
- setString(key, value) {
1295
- const data = this.readFile() || {};
1296
- if (value) {
1297
- data[key] = value;
1298
- }
1299
- else {
1300
- delete data[key];
1301
- }
1302
- this.writeFile(data);
1303
- }
1304
- readFile() {
1305
- if (fs.existsSync(this.fileName)) {
1306
- return JSON.parse(fs.readFileSync(this.fileName, 'utf8'));
1307
- }
1308
- return undefined;
1309
- }
1310
- writeFile(data) {
1311
- if (!fs.existsSync(this.dirName)) {
1312
- fs.mkdirSync(this.dirName);
1313
- }
1314
- fs.writeFileSync(this.fileName, JSON.stringify(data, null, 2), 'utf8');
1315
- }
1316
- }
1317
-
1318
- exports.medplum = void 0;
1319
- async function main(medplumClient, argv) {
1320
- exports.medplum = medplumClient;
1321
- // Legacy support for MEDPLUM_CLIENT_ID and MEDPLUM_CLIENT_SECRET environment variables
1322
- const clientId = process.env['MEDPLUM_CLIENT_ID'];
1323
- const clientSecret = process.env['MEDPLUM_CLIENT_SECRET'];
1324
- if (clientId && clientSecret) {
1325
- await exports.medplum.startClientLogin(clientId, clientSecret);
1326
- }
1454
+ async function main(argv) {
1327
1455
  try {
1328
1456
  const index = new commander.Command('medplum').description('Command to access Medplum CLI');
1329
1457
  index.version(core.MEDPLUM_VERSION);
@@ -1338,7 +1466,7 @@ async function main(medplumClient, argv) {
1338
1466
  index.addCommand(deleteObject);
1339
1467
  // Project
1340
1468
  index.addCommand(project);
1341
- // Export
1469
+ // Bulk Commands
1342
1470
  index.addCommand(bulk);
1343
1471
  // Bot Commands
1344
1472
  index.addCommand(bot);
@@ -1354,28 +1482,12 @@ async function main(medplumClient, argv) {
1354
1482
  console.error('Error: ' + core.normalizeErrorString(err));
1355
1483
  }
1356
1484
  }
1357
- function run() {
1485
+ async function run() {
1358
1486
  dotenv.config();
1359
- const baseUrl = process.env['MEDPLUM_BASE_URL'] || 'https://api.medplum.com/';
1360
- const fhirUrlPath = process.env['MEDPLUM_FHIR_URL_PATH'] || '';
1361
- const accessToken = process.env['MEDPLUM_CLIENT_ACCESS_TOKEN'] || '';
1362
- const medplumClient = new core.MedplumClient({
1363
- fetch,
1364
- baseUrl,
1365
- fhirUrlPath,
1366
- storage: new FileSystemStorage(),
1367
- onUnauthenticated: onUnauthenticated,
1368
- });
1369
- if (accessToken) {
1370
- medplumClient.setAccessToken(accessToken);
1371
- }
1372
- main(medplumClient, process.argv).catch((err) => console.error('Unhandled error:', err));
1487
+ await main(process.argv);
1373
1488
  }
1374
1489
  if (require.main === module) {
1375
- run();
1376
- }
1377
- function onUnauthenticated() {
1378
- console.log('Unauthenticated: run `npx medplum login` to sign in');
1490
+ run().catch((err) => console.error('Unhandled error:', err));
1379
1491
  }
1380
1492
 
1381
1493
  exports.main = main;