Skip to content

Commit 5aa3daf

Browse files
authored
Feature/command magic args (#367)
* feat: Add support for commands implements property Allows commands to act as updaters, launchers, and service controllers. * feat: Add support for commands implements property editing * feat: Add verbose install and uninstall logs
1 parent 08aa7c1 commit 5aa3daf

File tree

17 files changed

+2043
-277
lines changed

17 files changed

+2043
-277
lines changed

cli/src/main/java/ca/weblite/jdeploy/gui/tabs/CliCommandsPanel.java

Lines changed: 141 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public class CliCommandsPanel extends JPanel {
3030
private JButton removeButton;
3131
private JTextField nameField;
3232
private JTextField descriptionField;
33+
private JCheckBox updaterCheckbox;
34+
private JCheckBox launcherCheckbox;
35+
private JCheckBox serviceControllerCheckbox;
3336
private JTextArea argsField;
3437
private JLabel validationLabel;
3538
private ActionListener changeListener;
@@ -179,6 +182,56 @@ private JPanel createRightPanel() {
179182

180183
formPanel.add(Box.createVerticalStrut(10));
181184

185+
// Implements field (checkboxes)
186+
JPanel implLabelPanel = new JPanel();
187+
implLabelPanel.setOpaque(false);
188+
implLabelPanel.setLayout(new BoxLayout(implLabelPanel, BoxLayout.X_AXIS));
189+
JLabel implLabel = new JLabel("Implements");
190+
implLabel.setFont(implLabel.getFont().deriveFont(Font.BOLD));
191+
implLabelPanel.add(implLabel);
192+
implLabelPanel.add(Box.createHorizontalStrut(5));
193+
implLabelPanel.add(createInfoIcon("<html>Special behaviors for this command.<br>See individual checkbox tooltips for details.</html>"));
194+
implLabelPanel.add(Box.createHorizontalGlue());
195+
implLabelPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
196+
implLabelPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, implLabel.getPreferredSize().height));
197+
formPanel.add(implLabelPanel);
198+
199+
// Checkboxes panel
200+
JPanel checkboxPanel = new JPanel();
201+
checkboxPanel.setOpaque(false);
202+
checkboxPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
203+
checkboxPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
204+
205+
updaterCheckbox = new JCheckBox("Updater");
206+
updaterCheckbox.setOpaque(false);
207+
updaterCheckbox.setToolTipText("<html>Intercepts 'update' argument to trigger app updates.<br>" +
208+
"Example: <code>myapp-cli update</code> → calls launcher with --jdeploy:update</html>");
209+
updaterCheckbox.addActionListener(e -> onFieldChanged());
210+
checkboxPanel.add(updaterCheckbox);
211+
checkboxPanel.add(Box.createHorizontalStrut(15));
212+
213+
launcherCheckbox = new JCheckBox("Launcher");
214+
launcherCheckbox.setOpaque(false);
215+
launcherCheckbox.setToolTipText("<html>Launches the desktop GUI application.<br>" +
216+
"Arguments are passed as file paths or URLs to open.<br>" +
217+
"macOS: Uses 'open -a MyApp.app', others call binary directly.</html>");
218+
launcherCheckbox.addActionListener(e -> onFieldChanged());
219+
checkboxPanel.add(launcherCheckbox);
220+
checkboxPanel.add(Box.createHorizontalStrut(15));
221+
222+
serviceControllerCheckbox = new JCheckBox("Service Controller");
223+
serviceControllerCheckbox.setOpaque(false);
224+
serviceControllerCheckbox.setToolTipText("<html>Intercepts 'service' as first argument for daemon control.<br>" +
225+
"Example: <code>myappctl service start</code> → calls launcher with --jdeploy:service start</html>");
226+
serviceControllerCheckbox.addActionListener(e -> onFieldChanged());
227+
checkboxPanel.add(serviceControllerCheckbox);
228+
229+
// Constrain the height of the checkbox panel to prevent vertical expansion
230+
checkboxPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, updaterCheckbox.getPreferredSize().height));
231+
formPanel.add(checkboxPanel);
232+
233+
formPanel.add(Box.createVerticalStrut(10));
234+
182235
// Arguments field
183236
JPanel argsLabelPanel = new JPanel();
184237
argsLabelPanel.setOpaque(false);
@@ -312,6 +365,9 @@ public void load(JSONObject jdeploy) {
312365
commandsModel.clear();
313366
nameField.setText("");
314367
descriptionField.setText("");
368+
updaterCheckbox.setSelected(false);
369+
launcherCheckbox.setSelected(false);
370+
serviceControllerCheckbox.setSelected(false);
315371
argsField.setForeground(Color.GRAY);
316372
argsField.setText(ARGS_PLACEHOLDER);
317373
validationLabel.setText(" ");
@@ -434,6 +490,9 @@ private void onCommandSelected(ListSelectionEvent evt) {
434490
if (index < 0) {
435491
nameField.setText("");
436492
descriptionField.setText("");
493+
updaterCheckbox.setSelected(false);
494+
launcherCheckbox.setSelected(false);
495+
serviceControllerCheckbox.setSelected(false);
437496
argsField.setForeground(Color.GRAY);
438497
argsField.setText(ARGS_PLACEHOLDER);
439498
validationLabel.setText(" ");
@@ -453,12 +512,31 @@ private void loadCommandForEditing(String commandName) {
453512
isUpdatingUI = true;
454513
try {
455514
nameField.setText(commandName);
456-
515+
457516
// Load description and args from the backing data model
458517
JSONObject spec = commandsModel.get(commandName);
459518
if (spec != null) {
460519
descriptionField.setText(spec.optString("description", ""));
461-
520+
521+
// Load implements array
522+
updaterCheckbox.setSelected(false);
523+
launcherCheckbox.setSelected(false);
524+
serviceControllerCheckbox.setSelected(false);
525+
526+
if (spec.has("implements")) {
527+
JSONArray implArray = spec.getJSONArray("implements");
528+
for (int i = 0; i < implArray.length(); i++) {
529+
String impl = implArray.getString(i);
530+
if (CommandSpecParser.IMPL_UPDATER.equals(impl)) {
531+
updaterCheckbox.setSelected(true);
532+
} else if (CommandSpecParser.IMPL_LAUNCHER.equals(impl)) {
533+
launcherCheckbox.setSelected(true);
534+
} else if (CommandSpecParser.IMPL_SERVICE_CONTROLLER.equals(impl)) {
535+
serviceControllerCheckbox.setSelected(true);
536+
}
537+
}
538+
}
539+
462540
// Load args - join array elements with newlines
463541
if (spec.has("args")) {
464542
JSONArray argsArray = spec.getJSONArray("args");
@@ -475,10 +553,13 @@ private void loadCommandForEditing(String commandName) {
475553
}
476554
} else {
477555
descriptionField.setText("");
556+
updaterCheckbox.setSelected(false);
557+
launcherCheckbox.setSelected(false);
558+
serviceControllerCheckbox.setSelected(false);
478559
argsField.setForeground(Color.GRAY);
479560
argsField.setText(ARGS_PLACEHOLDER);
480561
}
481-
562+
482563
validationLabel.setText(" ");
483564
} finally {
484565
isUpdatingUI = false;
@@ -537,15 +618,32 @@ private void onNameChanged() {
537618
if (spec == null) {
538619
spec = new JSONObject();
539620
}
540-
621+
541622
// Save current form values into the spec before moving it
542623
String desc = descriptionField.getText().trim();
543624
if (!desc.isEmpty()) {
544625
spec.put("description", desc);
545626
} else {
546627
spec.remove("description");
547628
}
548-
629+
630+
// Save implements array
631+
JSONArray implArray = new JSONArray();
632+
if (updaterCheckbox.isSelected()) {
633+
implArray.put(CommandSpecParser.IMPL_UPDATER);
634+
}
635+
if (launcherCheckbox.isSelected()) {
636+
implArray.put(CommandSpecParser.IMPL_LAUNCHER);
637+
}
638+
if (serviceControllerCheckbox.isSelected()) {
639+
implArray.put(CommandSpecParser.IMPL_SERVICE_CONTROLLER);
640+
}
641+
if (implArray.length() > 0) {
642+
spec.put("implements", implArray);
643+
} else {
644+
spec.remove("implements");
645+
}
646+
549647
String argsText = argsField.getText().trim();
550648
boolean isPlaceholder = argsText.equals(ARGS_PLACEHOLDER.trim()) && argsField.getForeground().equals(Color.GRAY);
551649

@@ -566,7 +664,7 @@ private void onNameChanged() {
566664
} else {
567665
spec.remove("args");
568666
}
569-
667+
570668
commandsModel.put(name, spec);
571669

572670
// Update the list (this will not trigger selection change)
@@ -605,6 +703,9 @@ private void removeSelectedCommand() {
605703
removeButton.setEnabled(false);
606704
nameField.setText("");
607705
descriptionField.setText("");
706+
updaterCheckbox.setSelected(false);
707+
launcherCheckbox.setSelected(false);
708+
serviceControllerCheckbox.setSelected(false);
608709
argsField.setForeground(Color.GRAY);
609710
argsField.setText(ARGS_PLACEHOLDER);
610711
validationLabel.setText(" ");
@@ -631,6 +732,21 @@ private JSONObject buildCommandSpec(String name) {
631732
spec.put("description", desc);
632733
}
633734

735+
// Build implements array
736+
JSONArray implArray = new JSONArray();
737+
if (updaterCheckbox.isSelected()) {
738+
implArray.put(CommandSpecParser.IMPL_UPDATER);
739+
}
740+
if (launcherCheckbox.isSelected()) {
741+
implArray.put(CommandSpecParser.IMPL_LAUNCHER);
742+
}
743+
if (serviceControllerCheckbox.isSelected()) {
744+
implArray.put(CommandSpecParser.IMPL_SERVICE_CONTROLLER);
745+
}
746+
if (implArray.length() > 0) {
747+
spec.put("implements", implArray);
748+
}
749+
634750
String argsText = argsField.getText().trim();
635751
// Don't save placeholder text as actual args
636752
boolean isPlaceholder = argsText.equals(ARGS_PLACEHOLDER.trim()) && argsField.getForeground().equals(Color.GRAY);
@@ -670,15 +786,32 @@ private void onFieldChanged() {
670786
spec = new JSONObject();
671787
commandsModel.put(currentName, spec);
672788
}
673-
789+
674790
// Update description
675791
String desc = descriptionField.getText().trim();
676792
if (!desc.isEmpty()) {
677793
spec.put("description", desc);
678794
} else {
679795
spec.remove("description");
680796
}
681-
797+
798+
// Update implements array
799+
JSONArray implArray = new JSONArray();
800+
if (updaterCheckbox.isSelected()) {
801+
implArray.put(CommandSpecParser.IMPL_UPDATER);
802+
}
803+
if (launcherCheckbox.isSelected()) {
804+
implArray.put(CommandSpecParser.IMPL_LAUNCHER);
805+
}
806+
if (serviceControllerCheckbox.isSelected()) {
807+
implArray.put(CommandSpecParser.IMPL_SERVICE_CONTROLLER);
808+
}
809+
if (implArray.length() > 0) {
810+
spec.put("implements", implArray);
811+
} else {
812+
spec.remove("implements");
813+
}
814+
682815
// Update args
683816
String argsText = argsField.getText().trim();
684817
boolean isPlaceholder = argsText.equals(ARGS_PLACEHOLDER.trim()) && argsField.getForeground().equals(Color.GRAY);

cli/src/test/java/ca/weblite/jdeploy/gui/tabs/CliCommandsPanelTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,78 @@ public void testEditDescriptionAndSwitchCommand() {
183183
assertEquals(1, cmd2Args.length());
184184
assertEquals("arg2", cmd2Args.getString(0));
185185
}
186+
187+
@Test
188+
public void testImplementsPropertySaveAndLoad() {
189+
// Create a command with implements array
190+
JSONObject jdeploy = new JSONObject();
191+
JSONObject commands = new JSONObject();
192+
193+
JSONObject cmd1 = new JSONObject();
194+
cmd1.put("description", "Updater command");
195+
JSONArray impl1 = new JSONArray();
196+
impl1.put("updater");
197+
cmd1.put("implements", impl1);
198+
199+
JSONObject cmd2 = new JSONObject();
200+
cmd2.put("description", "Launcher command");
201+
JSONArray impl2 = new JSONArray();
202+
impl2.put("launcher");
203+
cmd2.put("implements", impl2);
204+
205+
JSONObject cmd3 = new JSONObject();
206+
cmd3.put("description", "Service controller with updater");
207+
JSONArray impl3 = new JSONArray();
208+
impl3.put("service_controller");
209+
impl3.put("updater");
210+
cmd3.put("implements", impl3);
211+
212+
commands.put("updater-cmd", cmd1);
213+
commands.put("launcher-cmd", cmd2);
214+
commands.put("service-cmd", cmd3);
215+
jdeploy.put("commands", commands);
216+
217+
// Load and verify
218+
panel.load(jdeploy);
219+
220+
// Save and verify implements are preserved
221+
JSONObject saved = new JSONObject();
222+
panel.save(saved);
223+
224+
JSONObject savedCommands = saved.getJSONObject("commands");
225+
assertEquals(3, savedCommands.length());
226+
227+
// Verify updater-cmd has updater implementation
228+
assertTrue(savedCommands.has("updater-cmd"));
229+
JSONObject savedCmd1 = savedCommands.getJSONObject("updater-cmd");
230+
assertTrue(savedCmd1.has("implements"));
231+
JSONArray savedImpl1 = savedCmd1.getJSONArray("implements");
232+
assertEquals(1, savedImpl1.length());
233+
assertEquals("updater", savedImpl1.getString(0));
234+
235+
// Verify launcher-cmd has launcher implementation
236+
assertTrue(savedCommands.has("launcher-cmd"));
237+
JSONObject savedCmd2 = savedCommands.getJSONObject("launcher-cmd");
238+
assertTrue(savedCmd2.has("implements"));
239+
JSONArray savedImpl2 = savedCmd2.getJSONArray("implements");
240+
assertEquals(1, savedImpl2.length());
241+
assertEquals("launcher", savedImpl2.getString(0));
242+
243+
// Verify service-cmd has both implementations
244+
assertTrue(savedCommands.has("service-cmd"));
245+
JSONObject savedCmd3 = savedCommands.getJSONObject("service-cmd");
246+
assertTrue(savedCmd3.has("implements"));
247+
JSONArray savedImpl3 = savedCmd3.getJSONArray("implements");
248+
assertEquals(2, savedImpl3.length());
249+
// Note: Order might vary, so check both are present
250+
boolean hasServiceController = false;
251+
boolean hasUpdater = false;
252+
for (int i = 0; i < savedImpl3.length(); i++) {
253+
String impl = savedImpl3.getString(i);
254+
if ("service_controller".equals(impl)) hasServiceController = true;
255+
if ("updater".equals(impl)) hasUpdater = true;
256+
}
257+
assertTrue(hasServiceController, "Should have service_controller implementation");
258+
assertTrue(hasUpdater, "Should have updater implementation");
259+
}
186260
}

0 commit comments

Comments
 (0)