dhti-cli 0.8.0 → 1.0.1

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.
@@ -1,268 +1,203 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
- import { exec } from 'node:child_process';
3
+ import { exec, spawn } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
- // Helper function to escape shell arguments
9
- function escapeShellArg(arg) {
10
- return `'${arg.replaceAll("'", "'\\''")}'`;
11
- }
7
+ import { promisify } from 'node:util';
8
+ const execAsync = promisify(exec);
12
9
  export default class Conch extends Command {
13
10
  static args = {
14
- op: Args.string({ description: 'Operation to perform (install, uninstall or dev)' }),
11
+ op: Args.string({ description: 'Operation to perform (init, install, or start)' }),
15
12
  };
16
- static description = 'Install or uninstall conchs to create a Docker image';
17
- static examples = ['<%= config.bin %> <%= command.id %>'];
13
+ static description = 'Initialize, install, or start OpenMRS frontend development';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %> install -n my-app -w ~/projects',
16
+ '<%= config.bin %> <%= command.id %> init -n my-app -w ~/projects',
17
+ '<%= config.bin %> <%= command.id %> start -n my-app -w ~/projects',
18
+ ];
18
19
  static flags = {
19
- branch: Flags.string({ char: 'b', default: 'develop', description: 'Branch to install from' }),
20
- container: Flags.string({
21
- char: 'c',
22
- default: 'dhti-frontend-1',
23
- description: 'Name of the container to copy the conch to while in dev mode',
20
+ branch: Flags.string({
21
+ char: 'b',
22
+ default: 'develop',
23
+ description: 'Branch to install from (for install operation)',
24
24
  }),
25
- dev: Flags.string({ char: 'd', default: 'none', description: 'Dev folder to install' }),
26
25
  'dry-run': Flags.boolean({
27
26
  default: false,
28
27
  description: 'Show what changes would be made without actually making them',
29
28
  }),
30
- git: Flags.string({ char: 'g', default: 'none', description: 'Github repository to install' }),
31
- image: Flags.string({
32
- char: 'i',
33
- default: 'openmrs/openmrs-reference-application-3-frontend:3.0.0-beta.17',
34
- description: 'Base image to use for the conch',
29
+ git: Flags.string({
30
+ char: 'g',
31
+ default: 'dermatologist/openmrs-esm-dhti-template',
32
+ description: 'GitHub repository to install (for install operation)',
35
33
  }),
36
- local: Flags.string({ char: 'l', default: 'none', description: 'Local directory to install from' }),
37
- name: Flags.string({ char: 'n', description: 'Name of the elixir' }),
38
- repoVersion: Flags.string({ char: 'v', default: '1.0.0', description: 'Version of the conch' }),
39
- subdirectory: Flags.string({
34
+ name: Flags.string({ char: 'n', description: 'Name of the conch' }),
35
+ sources: Flags.string({
40
36
  char: 's',
41
- default: 'none',
42
- description: 'Subdirectory in the repository to install from (for monorepos)',
37
+ description: 'Additional sources to include when starting (e.g., packages/esm-chatbot-agent)',
43
38
  }),
44
39
  workdir: Flags.string({
45
40
  char: 'w',
46
41
  default: `${os.homedir()}/dhti`,
47
- description: 'Working directory to install the conch',
42
+ description: 'Working directory for the conch',
48
43
  }),
49
44
  };
50
45
  async run() {
51
46
  const { args, flags } = await this.parse(Conch);
52
- // Resolve resources directory for both dev (src) and packaged (dist)
53
- const __filename = fileURLToPath(import.meta.url);
54
- const __dirname = path.dirname(__filename);
55
- const RESOURCES_DIR = path.resolve(__dirname, '../resources');
56
- if (!flags.name) {
57
- console.log('Please provide a name for the conch');
58
- this.exit(1);
59
- }
60
- // if arg is dev then copy to docker as below
61
- // docker cp ../../openmrs-esm-genai/dist/. dhti-frontend-1:/usr/share/nginx/html/openmrs-esm-genai-1.0.0
62
- // docker restart dhti-frontend-1
63
- if (args.op === 'dev') {
64
- const buildCommand = `cd ${flags.dev} && yarn build && docker cp dist/. ${flags.container}:/usr/share/nginx/html/${flags.name}-${flags.repoVersion}`;
65
- const restartCommand = `docker restart ${flags.container}`;
47
+ if (args.op === 'init') {
48
+ // Validate required flags
49
+ if (!flags.workdir) {
50
+ console.error(chalk.red('Error: workdir flag is required for init operation'));
51
+ this.exit(1);
52
+ }
53
+ if (!flags.name) {
54
+ console.error(chalk.red('Error: name flag is required for init operation'));
55
+ this.exit(1);
56
+ }
57
+ const targetDir = path.join(flags.workdir, 'openmrs-esm-dhti');
66
58
  if (flags['dry-run']) {
67
- console.log(chalk.yellow('[DRY RUN] Would execute commands:'));
68
- console.log(chalk.cyan(` ${buildCommand}`));
69
- console.log(chalk.cyan(` ${restartCommand}`));
59
+ console.log(chalk.yellow('[DRY RUN] Would execute init operation:'));
60
+ console.log(chalk.cyan(` npx degit dermatologist/openmrs-esm-dhti ${targetDir}`));
61
+ console.log(chalk.cyan(` Copy ${targetDir}/packages/esm-starter-app to ${targetDir}/packages/${flags.name}`));
70
62
  return;
71
63
  }
72
- console.log(buildCommand);
73
64
  try {
74
- exec(buildCommand, (error, stdout, stderr) => {
75
- if (error) {
76
- console.error(`exec error: ${error}`);
77
- return;
78
- }
79
- console.log(`stdout: ${stdout}`);
80
- console.error(`stderr: ${stderr}`);
81
- });
82
- exec(restartCommand, (error, stdout, stderr) => {
83
- if (error) {
84
- console.error(`exec error: ${error}`);
85
- return;
86
- }
87
- console.log(`stdout: ${stdout}`);
88
- console.error(`stderr: ${stderr}`);
89
- });
65
+ // Run npx degit to clone the dhti template
66
+ console.log(chalk.blue(`Initializing DHTI template in ${targetDir}...`));
67
+ const degitCommand = `npx degit dermatologist/openmrs-esm-dhti ${targetDir}`;
68
+ await execAsync(degitCommand);
69
+ console.log(chalk.green('✓ DHTI template cloned successfully'));
70
+ // Copy packages/esm-starter-app subdirectory to packages/<name>
71
+ const starterAppSource = path.join(targetDir, 'packages', 'esm-starter-app');
72
+ const targetPackageDir = path.join(targetDir, 'packages', flags.name);
73
+ if (fs.existsSync(starterAppSource)) {
74
+ console.log(chalk.blue(`Copying esm-starter-app to packages/${flags.name}...`));
75
+ fs.cpSync(starterAppSource, targetPackageDir, { recursive: true });
76
+ console.log(chalk.green(`✓ esm-starter-app copied to packages/${flags.name}`));
77
+ }
78
+ else {
79
+ console.log(chalk.yellow(`Warning: esm-starter-app not found at ${starterAppSource}`));
80
+ }
81
+ console.log(chalk.green(`\n✓ Initialization complete! Your workspace is ready at ${targetDir}`));
82
+ console.log(chalk.blue(`\nTo start development, run:`));
83
+ const startCmd = `dhti-cli conch start -w ${flags.workdir} -n ${flags.name}`;
84
+ console.log(chalk.cyan(` ${startCmd}`));
90
85
  }
91
86
  catch (error) {
92
- console.log('Error copying conch to container', error);
87
+ console.error(chalk.red('Error during initialization:'), error);
88
+ this.exit(1);
93
89
  }
94
90
  return;
95
91
  }
96
- // Create a directory to install the elixir
97
- if (!fs.existsSync(`${flags.workdir}/conch`)) {
98
- if (flags['dry-run']) {
99
- console.log(chalk.yellow(`[DRY RUN] Would create directory: ${flags.workdir}/conch`));
92
+ if (args.op === 'start') {
93
+ // Validate required flags
94
+ if (!flags.workdir) {
95
+ console.error(chalk.red('Error: workdir flag is required for start operation'));
96
+ this.exit(1);
100
97
  }
101
- else {
102
- fs.mkdirSync(`${flags.workdir}/conch`);
98
+ if (!flags.name) {
99
+ console.error(chalk.red('Error: name flag is required for start operation'));
100
+ this.exit(1);
103
101
  }
104
- }
105
- if (flags['dry-run']) {
106
- console.log(chalk.yellow(`[DRY RUN] Would copy resources from ${RESOURCES_DIR}/spa to ${flags.workdir}/conch`));
107
- }
108
- else {
109
- fs.cpSync(path.join(RESOURCES_DIR, 'spa'), `${flags.workdir}/conch`, { recursive: true });
110
- }
111
- // Rewrite files
112
- const rewrite = () => {
113
- flags.name = flags.name ?? 'openmrs-esm-genai';
102
+ const targetDir = path.join(flags.workdir, flags.name);
114
103
  if (flags['dry-run']) {
115
- console.log(chalk.yellow('[DRY RUN] Would update configuration files:'));
116
- console.log(chalk.cyan(` - ${flags.workdir}/conch/def/importmap.json`));
117
- if (args.op === 'install') {
118
- console.log(chalk.green(` Add import: ${flags.name.replace('openmrs-', '@openmrs/')} -> ./${flags.name}-${flags.repoVersion}/${flags.name}.js`));
119
- }
120
- if (args.op === 'uninstall') {
121
- console.log(chalk.green(` Remove import: ${flags.name.replace('openmrs-', '@openmrs/')}`));
122
- }
123
- console.log(chalk.cyan(` - ${flags.workdir}/conch/def/spa-assemble-config.json`));
124
- if (args.op === 'install') {
125
- console.log(chalk.green(` Add module: ${flags.name.replace('openmrs-', '@openmrs/')} = ${flags.repoVersion}`));
126
- }
127
- if (args.op === 'uninstall') {
128
- console.log(chalk.green(` Remove module: ${flags.name.replace('openmrs-', '@openmrs/')}`));
129
- }
130
- console.log(chalk.cyan(` - ${flags.workdir}/conch/Dockerfile`));
131
- console.log(chalk.green(` Update with conch=${flags.name}, version=${flags.repoVersion}, image=${flags.image}`));
132
- console.log(chalk.cyan(` - ${flags.workdir}/conch/def/routes.registry.json`));
133
- if (args.op === 'install') {
134
- console.log(chalk.green(` Add routes for ${flags.name.replace('openmrs-', '@openmrs/')}`));
135
- }
136
- if (args.op === 'uninstall') {
137
- console.log(chalk.green(` Remove routes for ${flags.name.replace('openmrs-', '@openmrs/')}`));
104
+ console.log(chalk.yellow('[DRY RUN] Would execute start operation:'));
105
+ console.log(chalk.cyan(` cd ${targetDir}`));
106
+ let dryRunCommand = 'corepack enable && yarn install && yarn start';
107
+ if (flags.sources) {
108
+ dryRunCommand += ` --sources '${flags.sources}'`;
138
109
  }
110
+ console.log(chalk.cyan(` ${dryRunCommand}`));
139
111
  return;
140
112
  }
141
- // Read and process importmap.json
142
- const importmap = JSON.parse(fs.readFileSync(`${flags.workdir}/conch/def/importmap.json`, 'utf8'));
143
- if (args.op === 'install')
144
- importmap.imports[flags.name.replace('openmrs-', '@openmrs/')] = `./${flags.name}-${flags.repoVersion}/${flags.name}.js`;
145
- if (args.op === 'uninstall')
146
- delete importmap.imports[flags.name.replace('openmrs-', '@openmrs/')];
147
- fs.writeFileSync(`${flags.workdir}/conch/def/importmap.json`, JSON.stringify(importmap, null, 2));
148
- // Read and process spa-assemble-config.json
149
- const spaAssembleConfig = JSON.parse(fs.readFileSync(`${flags.workdir}/conch/def/spa-assemble-config.json`, 'utf8'));
150
- if (args.op === 'install')
151
- spaAssembleConfig.frontendModules[flags.name.replace('openmrs-', '@openmrs/')] = `${flags.repoVersion}`;
152
- if (args.op === 'uninstall')
153
- delete spaAssembleConfig.frontendModules[flags.name.replace('openmrs-', '@openmrs/')];
154
- fs.writeFileSync(`${flags.workdir}/conch/def/spa-assemble-config.json`, JSON.stringify(spaAssembleConfig, null, 2));
155
- // Read and process Dockerfile
156
- let dockerfile = fs.readFileSync(`${flags.workdir}/conch/Dockerfile`, 'utf8');
157
- dockerfile = dockerfile
158
- .replaceAll('conch', flags.name)
159
- .replaceAll('version', flags.repoVersion)
160
- .replaceAll('server-image', flags.image);
161
- fs.writeFileSync(`${flags.workdir}/conch/Dockerfile`, dockerfile);
162
- // Read routes.json
163
- const routes = JSON.parse(fs.readFileSync(`${flags.workdir}/conch/${flags.name}/src/routes.json`, 'utf8'));
164
- // Add to routes.registry.json
165
- const registry = JSON.parse(fs.readFileSync(`${flags.workdir}/conch/def/routes.registry.json`, 'utf8'));
166
- if (args.op === 'install')
167
- registry[flags.name.replace('openmrs-', '@openmrs/')] = routes;
168
- if (args.op === 'uninstall')
169
- delete registry[flags.name.replace('openmrs-', '@openmrs/')];
170
- fs.writeFileSync(`${flags.workdir}/conch/def/routes.registry.json`, JSON.stringify(registry, null, 2));
171
- };
172
- if (flags.git !== 'none') {
173
- let cloneCommand;
174
- let checkoutCommand;
175
- if (flags.subdirectory === 'none') {
176
- cloneCommand = `git clone ${escapeShellArg(flags.git)} ${escapeShellArg(`${flags.workdir}/conch/${flags.name}`)}`;
177
- checkoutCommand = `cd ${escapeShellArg(`${flags.workdir}/conch/${flags.name}`)} && git checkout ${escapeShellArg(flags.branch)}`;
178
- }
179
- else {
180
- // Use sparse checkout for subdirectory - broken into steps for readability and security
181
- const targetDir = `${flags.workdir}/conch/${flags.name}`;
182
- const escapedDir = escapeShellArg(targetDir);
183
- const escapedGit = escapeShellArg(flags.git);
184
- const escapedBranch = escapeShellArg(flags.branch);
185
- const escapedSubdir = escapeShellArg(flags.subdirectory);
186
- // Build sparse checkout command with proper escaping
187
- const initCommand = `mkdir -p ${escapedDir} && cd ${escapedDir} && git init`;
188
- const remoteCommand = `git remote add origin ${escapedGit}`;
189
- const sparseCommand = `git config core.sparseCheckout true`;
190
- // Don't escape the glob pattern itself, only the subdirectory name
191
- const patternCommand = `echo ${escapedSubdir}/'*' >> .git/info/sparse-checkout`;
192
- const fetchCommand = `git fetch --depth=1 origin ${escapedBranch}`;
193
- const checkoutCmd = `git checkout ${escapedBranch}`;
194
- // Use bash's dotglob to include hidden files, and handle the case when no files exist
195
- const moveCommand = `bash -c "shopt -s dotglob; if [ -d ${escapedSubdir} ]; then mv ${escapedSubdir}/* . 2>/dev/null || true; fi"`;
196
- const cleanupCommand = `rm -rf ${escapedSubdir}`;
197
- cloneCommand = `${initCommand} && ${remoteCommand} && ${sparseCommand} && ${patternCommand} && ${fetchCommand} && ${checkoutCmd} && ${moveCommand} && ${cleanupCommand}`;
198
- checkoutCommand = `cd ${escapedDir} && echo "Sparse checkout complete"`;
113
+ // Check if directory exists (not in dry-run mode)
114
+ if (!fs.existsSync(targetDir)) {
115
+ console.error(chalk.red(`Error: Directory does not exist: ${targetDir}`));
116
+ console.log(chalk.yellow(`Run 'dhti-cli conch init -n ${flags.name} -w ${flags.workdir}' first`));
117
+ this.exit(1);
199
118
  }
200
- if (flags['dry-run']) {
201
- console.log(chalk.yellow('[DRY RUN] Would execute git commands:'));
202
- if (flags.subdirectory === 'none') {
203
- console.log(chalk.cyan(` ${cloneCommand}`));
204
- console.log(chalk.cyan(` ${checkoutCommand}`));
205
- }
206
- else {
207
- console.log(chalk.cyan(` Sparse checkout: ${flags.subdirectory} from ${flags.git}`));
208
- }
209
- rewrite();
210
- return;
211
- }
212
- // git clone the repository
213
- exec(cloneCommand, (error, stdout, stderr) => {
214
- if (error) {
215
- console.error(`exec error: ${error}`);
216
- return;
119
+ try {
120
+ console.log(chalk.blue(`Starting OpenMRS development server in ${targetDir}...`));
121
+ console.log(chalk.yellow('Press Ctrl-C to stop\n'));
122
+ // Build the start command with sources flag if provided
123
+ let startCommand = 'corepack enable && yarn install && yarn start';
124
+ if (flags.sources) {
125
+ startCommand += ` --sources '${flags.sources}'`;
217
126
  }
218
- // Checkout the branch (or confirm sparse checkout)
219
- exec(checkoutCommand, (error, stdout, stderr) => {
220
- if (error) {
221
- console.error(`exec error: ${error}`);
222
- return;
127
+ // Spawn corepack enable && yarn install && yarn start with stdio inheritance to show output and allow Ctrl-C
128
+ const child = spawn(startCommand, {
129
+ cwd: targetDir,
130
+ shell: true,
131
+ stdio: 'inherit',
132
+ });
133
+ // Handle process exit
134
+ child.on('exit', (code) => {
135
+ if (code === 0) {
136
+ console.log(chalk.green('\n✓ Development server stopped'));
137
+ }
138
+ else if (code !== null) {
139
+ console.log(chalk.yellow(`\nDevelopment server exited with code ${code}`));
223
140
  }
224
- rewrite();
225
- console.log(`stdout: ${stdout}`);
226
- console.error(`stderr: ${stderr}`);
227
141
  });
228
- console.log(`stdout: ${stdout}`);
229
- console.error(`stderr: ${stderr}`);
230
- });
231
- }
232
- // If flags.dev is not none, copy the dev folder to the conch directory
233
- if (flags.dev !== 'none' && args.op !== 'dev') {
234
- if (flags['dry-run']) {
235
- console.log(chalk.yellow(`[DRY RUN] Would copy ${flags.dev} to ${flags.workdir}/conch/${flags.name}`));
236
- rewrite();
142
+ // Handle errors
143
+ child.on('error', (error) => {
144
+ console.error(chalk.red('Error starting development server:'), error);
145
+ this.exit(1);
146
+ });
147
+ // Wait for the child process to complete
148
+ await new Promise((resolve) => {
149
+ child.on('close', () => resolve());
150
+ });
237
151
  }
238
- else {
239
- fs.cpSync(flags.dev, `${flags.workdir}/conch/${flags.name}`, { recursive: true });
240
- rewrite();
152
+ catch (error) {
153
+ console.error(chalk.red('Error during start:'), error);
154
+ this.exit(1);
241
155
  }
156
+ return;
242
157
  }
243
- // If flags.local is not none, copy the local directory to the conch directory
244
- if (flags.local !== 'none' && args.op !== 'dev') {
245
- const absolutePath = path.isAbsolute(flags.local) ? flags.local : path.resolve(process.cwd(), flags.local);
246
- // Validate that the path exists and is a directory (skip validation in dry-run mode)
247
- if (!flags['dry-run']) {
248
- if (!fs.existsSync(absolutePath)) {
249
- console.error(chalk.red(`Error: Local directory does not exist: ${absolutePath}`));
250
- this.exit(1);
251
- }
252
- const stats = fs.statSync(absolutePath);
253
- if (!stats.isDirectory()) {
254
- console.error(chalk.red(`Error: Path is not a directory: ${absolutePath}`));
255
- this.exit(1);
256
- }
158
+ if (args.op === 'install') {
159
+ // Validate required flags
160
+ if (!flags.workdir) {
161
+ console.error(chalk.red('Error: workdir flag is required for install operation'));
162
+ this.exit(1);
163
+ }
164
+ if (!flags.name) {
165
+ console.error(chalk.red('Error: name flag is required for install operation'));
166
+ this.exit(1);
167
+ }
168
+ // Warn if sources flag is used with install (not applicable)
169
+ if (flags.sources) {
170
+ console.warn(chalk.yellow('Warning: --sources flag is not applicable for install operation. It will be ignored.'));
171
+ console.warn(chalk.yellow('Use --sources with the start operation instead.'));
257
172
  }
173
+ const targetDir = path.join(flags.workdir, flags.name);
174
+ const degitSource = `${flags.git}#${flags.branch}`;
258
175
  if (flags['dry-run']) {
259
- console.log(chalk.yellow(`[DRY RUN] Would copy ${absolutePath} to ${flags.workdir}/conch/${flags.name}`));
260
- rewrite();
176
+ console.log(chalk.yellow('[DRY RUN] Would execute install operation:'));
177
+ console.log(chalk.cyan(` npx degit ${degitSource} ${targetDir}`));
178
+ return;
179
+ }
180
+ try {
181
+ console.log(chalk.blue(`Installing from ${degitSource} to ${targetDir}...`));
182
+ const degitCommand = `npx degit ${degitSource} ${targetDir}`;
183
+ await execAsync(degitCommand);
184
+ console.log(chalk.green('✓ Repository cloned successfully'));
185
+ console.log(chalk.green(`\n✓ Installation complete! Your app is ready at ${targetDir}`));
186
+ console.log(chalk.blue(`\nTo start development, run:`));
187
+ let startCmd = `dhti-cli conch start -n ${flags.name} -w ${flags.workdir}`;
188
+ if (flags.sources) {
189
+ startCmd += ` -s '${flags.sources}'`;
190
+ }
191
+ console.log(chalk.cyan(` ${startCmd}`));
261
192
  }
262
- else {
263
- fs.cpSync(absolutePath, `${flags.workdir}/conch/${flags.name}`, { recursive: true });
264
- rewrite();
193
+ catch (error) {
194
+ console.error(chalk.red('Error during installation:'), error);
195
+ this.exit(1);
265
196
  }
197
+ return;
266
198
  }
199
+ // If no valid operation is provided
200
+ console.error(chalk.red('Error: Invalid operation. Use "install", "init", or "start"'));
201
+ this.exit(1);
267
202
  }
268
203
  }
@@ -10,6 +10,8 @@ export default class Elixir extends Command {
10
10
  container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
11
  dev: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
12
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ elixir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ fhir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
15
  git: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
16
  local: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
17
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;