@harness-lab/cli 0.1.8 → 0.2.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/README.md CHANGED
@@ -10,6 +10,10 @@ Current shipped scope:
10
10
  - `harness auth logout`
11
11
  - `harness auth status`
12
12
  - `harness workshop status`
13
+ - `harness workshop create-instance`
14
+ - `harness workshop update-instance`
15
+ - `harness workshop prepare`
16
+ - `harness workshop remove-instance`
13
17
  - `harness workshop archive`
14
18
  - `harness workshop phase set <phase-id>`
15
19
 
@@ -102,11 +106,21 @@ Workshop commands:
102
106
  harness auth status
103
107
  harness skill install
104
108
  harness workshop status
109
+ harness workshop create-instance developer-hackathon-praha-24-4-saturn --event-title "Developer Hackathon Praha"
110
+ harness workshop update-instance developer-hackathon-praha-24-4-saturn --room-name Saturn
111
+ harness workshop prepare developer-hackathon-praha-24-4-saturn
112
+ harness workshop remove-instance developer-hackathon-praha-24-4-saturn
105
113
  harness workshop phase set rotation
106
114
  harness workshop archive --notes "Manual archive"
107
115
  harness auth logout
108
116
  ```
109
117
 
118
+ Facilitator lifecycle commands are intentionally CLI-first:
119
+
120
+ - skill invokes `harness`
121
+ - `harness` invokes the protected dashboard APIs
122
+ - the dashboard APIs remain the source of truth for authorization, validation, idempotency, and audit logging
123
+
110
124
  Environment variables:
111
125
 
112
126
  - `HARNESS_DASHBOARD_URL`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Participant-facing Harness Lab CLI for facilitator auth and workshop operations",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/src/client.js CHANGED
@@ -97,5 +97,26 @@ export function createHarnessClient({ fetchFn, session }) {
97
97
  archiveWorkshop(notes) {
98
98
  return request("/api/workshop/archive", { method: "POST", body: notes ? { notes } : {} });
99
99
  },
100
+ createWorkshopInstance(input) {
101
+ return request("/api/workshop/instances", { method: "POST", body: input });
102
+ },
103
+ updateWorkshopInstance(instanceId, input) {
104
+ return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}`, {
105
+ method: "PATCH",
106
+ body: { action: "update_metadata", ...input },
107
+ });
108
+ },
109
+ prepareWorkshopInstance(instanceId) {
110
+ return request("/api/workshop", {
111
+ method: "POST",
112
+ body: { action: "prepare", instanceId },
113
+ });
114
+ },
115
+ removeWorkshopInstance(instanceId) {
116
+ return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}`, {
117
+ method: "PATCH",
118
+ body: { action: "remove" },
119
+ });
120
+ },
100
121
  };
101
122
  }
package/src/run-cli.js CHANGED
@@ -64,6 +64,99 @@ async function readJson(response) {
64
64
  }
65
65
  }
66
66
 
67
+ function readStringFlag(flags, ...keys) {
68
+ for (const key of keys) {
69
+ if (typeof flags[key] === "string") {
70
+ const trimmed = String(flags[key]).trim();
71
+ if (trimmed.length > 0) {
72
+ return trimmed;
73
+ }
74
+ }
75
+ }
76
+
77
+ return undefined;
78
+ }
79
+
80
+ function readOptionalPositional(positionals, index) {
81
+ const value = positionals[index];
82
+ if (typeof value !== "string") {
83
+ return undefined;
84
+ }
85
+
86
+ const trimmed = value.trim();
87
+ return trimmed.length > 0 ? trimmed : undefined;
88
+ }
89
+
90
+ async function readRequiredCommandValue(io, flags, keys, promptLabel, fallbackValue) {
91
+ const fromFlags = readStringFlag(flags, ...keys);
92
+ if (fromFlags) {
93
+ return fromFlags;
94
+ }
95
+
96
+ if (typeof fallbackValue === "string" && fallbackValue.trim().length > 0) {
97
+ return fallbackValue.trim();
98
+ }
99
+
100
+ const prompted = await prompt(io, promptLabel);
101
+ return prompted.trim();
102
+ }
103
+
104
+ function buildWorkshopMetadataInput(flags) {
105
+ const input = {
106
+ eventTitle: readStringFlag(flags, "event-title", "title"),
107
+ city: readStringFlag(flags, "city"),
108
+ dateRange: readStringFlag(flags, "date-range", "date"),
109
+ venueName: readStringFlag(flags, "venue-name", "venue"),
110
+ roomName: readStringFlag(flags, "room-name", "room"),
111
+ addressLine: readStringFlag(flags, "address-line", "address"),
112
+ locationDetails: readStringFlag(flags, "location-details", "location"),
113
+ facilitatorLabel: readStringFlag(flags, "facilitator-label", "facilitator"),
114
+ };
115
+
116
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => typeof value === "string"));
117
+ }
118
+
119
+ function hasWorkshopMetadataInput(input) {
120
+ return Object.keys(input).length > 0;
121
+ }
122
+
123
+ async function promptWorkshopMetadataInput(io) {
124
+ const prompts = [
125
+ ["eventTitle", "Event title (leave blank to skip): "],
126
+ ["city", "City (leave blank to skip): "],
127
+ ["dateRange", "Date range (leave blank to skip): "],
128
+ ["venueName", "Venue name (leave blank to skip): "],
129
+ ["roomName", "Room name (leave blank to skip): "],
130
+ ["addressLine", "Address line (leave blank to skip): "],
131
+ ["locationDetails", "Location details (leave blank to skip): "],
132
+ ["facilitatorLabel", "Facilitator label (leave blank to skip): "],
133
+ ];
134
+ const values = {};
135
+
136
+ for (const [key, label] of prompts) {
137
+ const value = await prompt(io, label);
138
+ if (value) {
139
+ values[key] = value;
140
+ }
141
+ }
142
+
143
+ return values;
144
+ }
145
+
146
+ function summarizeWorkshopInstance(instance) {
147
+ const workshopMeta = instance?.workshopMeta ?? {};
148
+
149
+ return {
150
+ instanceId: instance?.id ?? null,
151
+ status: instance?.status ?? null,
152
+ eventTitle: workshopMeta.eventTitle ?? null,
153
+ city: workshopMeta.city ?? null,
154
+ dateRange: workshopMeta.dateRange ?? null,
155
+ venueName: workshopMeta.venueName ?? null,
156
+ roomName: workshopMeta.roomName ?? null,
157
+ };
158
+ }
159
+
67
160
  function printUsage(io, ui) {
68
161
  ui.heading("Harness CLI");
69
162
  ui.paragraph(`Version ${version}`);
@@ -83,6 +176,10 @@ function printUsage(io, ui) {
83
176
  "harness skill install [--force]",
84
177
  "harness workshop status",
85
178
  "harness workshop archive [--notes TEXT]",
179
+ "harness workshop create-instance [<instance-id>] [--template-id ID] [--event-title TEXT] [--city CITY]",
180
+ "harness workshop update-instance <instance-id> [--event-title TEXT] [--city CITY]",
181
+ "harness workshop prepare <instance-id>",
182
+ "harness workshop remove-instance <instance-id>",
86
183
  "harness workshop phase set <phase-id>",
87
184
  ]);
88
185
  }
@@ -498,6 +595,171 @@ async function handleWorkshopArchive(io, ui, env, flags, deps) {
498
595
  }
499
596
  }
500
597
 
598
+ async function handleWorkshopCreateInstance(io, ui, env, positionals, flags, deps) {
599
+ const session = await requireSession(io, ui, env);
600
+ if (!session) {
601
+ return 1;
602
+ }
603
+
604
+ const instanceId = await readRequiredCommandValue(
605
+ io,
606
+ flags,
607
+ ["id"],
608
+ "Instance id: ",
609
+ readOptionalPositional(positionals, 2),
610
+ );
611
+ if (!instanceId) {
612
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
613
+ return 1;
614
+ }
615
+
616
+ const payload = {
617
+ id: instanceId,
618
+ ...(readStringFlag(flags, "template-id", "template") ? { templateId: readStringFlag(flags, "template-id", "template") } : {}),
619
+ ...buildWorkshopMetadataInput(flags),
620
+ };
621
+
622
+ try {
623
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
624
+ const result = await client.createWorkshopInstance(payload);
625
+ ui.json("Workshop Create Instance", {
626
+ ok: true,
627
+ created: result.created ?? true,
628
+ ...summarizeWorkshopInstance(result.instance),
629
+ instance: result.instance,
630
+ });
631
+ return 0;
632
+ } catch (error) {
633
+ if (error instanceof HarnessApiError) {
634
+ ui.status("error", `Create instance failed: ${error.message}`, { stream: "stderr" });
635
+ return 1;
636
+ }
637
+ throw error;
638
+ }
639
+ }
640
+
641
+ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, deps) {
642
+ const session = await requireSession(io, ui, env);
643
+ if (!session) {
644
+ return 1;
645
+ }
646
+
647
+ const instanceId = await readRequiredCommandValue(
648
+ io,
649
+ flags,
650
+ ["id"],
651
+ "Instance id: ",
652
+ readOptionalPositional(positionals, 2),
653
+ );
654
+ if (!instanceId) {
655
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
656
+ return 1;
657
+ }
658
+
659
+ let payload = buildWorkshopMetadataInput(flags);
660
+ if (!hasWorkshopMetadataInput(payload)) {
661
+ payload = await promptWorkshopMetadataInput(io);
662
+ }
663
+
664
+ if (!hasWorkshopMetadataInput(payload)) {
665
+ ui.status(
666
+ "error",
667
+ "At least one metadata field is required. Use flags such as --event-title, --date-range, --venue-name, or --room-name.",
668
+ { stream: "stderr" },
669
+ );
670
+ return 1;
671
+ }
672
+
673
+ try {
674
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
675
+ const result = await client.updateWorkshopInstance(instanceId, payload);
676
+ ui.json("Workshop Update Instance", {
677
+ ok: true,
678
+ ...summarizeWorkshopInstance(result.instance),
679
+ instance: result.instance,
680
+ });
681
+ return 0;
682
+ } catch (error) {
683
+ if (error instanceof HarnessApiError) {
684
+ ui.status("error", `Update instance failed: ${error.message}`, { stream: "stderr" });
685
+ return 1;
686
+ }
687
+ throw error;
688
+ }
689
+ }
690
+
691
+ async function handleWorkshopPrepare(io, ui, env, positionals, flags, deps) {
692
+ const session = await requireSession(io, ui, env);
693
+ if (!session) {
694
+ return 1;
695
+ }
696
+
697
+ const instanceId = await readRequiredCommandValue(
698
+ io,
699
+ flags,
700
+ ["id", "instance-id"],
701
+ "Instance id: ",
702
+ readOptionalPositional(positionals, 2),
703
+ );
704
+ if (!instanceId) {
705
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
706
+ return 1;
707
+ }
708
+
709
+ try {
710
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
711
+ const result = await client.prepareWorkshopInstance(instanceId);
712
+ ui.json("Workshop Prepare", {
713
+ ok: true,
714
+ ...summarizeWorkshopInstance(result.instance),
715
+ instance: result.instance,
716
+ });
717
+ return 0;
718
+ } catch (error) {
719
+ if (error instanceof HarnessApiError) {
720
+ ui.status("error", `Prepare failed: ${error.message}`, { stream: "stderr" });
721
+ return 1;
722
+ }
723
+ throw error;
724
+ }
725
+ }
726
+
727
+ async function handleWorkshopRemoveInstance(io, ui, env, positionals, flags, deps) {
728
+ const session = await requireSession(io, ui, env);
729
+ if (!session) {
730
+ return 1;
731
+ }
732
+
733
+ const instanceId = await readRequiredCommandValue(
734
+ io,
735
+ flags,
736
+ ["id", "instance-id"],
737
+ "Instance id: ",
738
+ readOptionalPositional(positionals, 2),
739
+ );
740
+ if (!instanceId) {
741
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
742
+ return 1;
743
+ }
744
+
745
+ try {
746
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
747
+ await client.removeWorkshopInstance(instanceId);
748
+ ui.json("Workshop Remove Instance", {
749
+ ok: true,
750
+ instanceId,
751
+ removed: true,
752
+ });
753
+ return 0;
754
+ } catch (error) {
755
+ if (error instanceof HarnessApiError) {
756
+ ui.status("error", `Remove instance failed: ${error.message}`, { stream: "stderr" });
757
+ return 1;
758
+ }
759
+ throw error;
760
+ }
761
+ }
762
+
501
763
  async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
502
764
  const phaseId = positionals[3];
503
765
  if (!phaseId) {
@@ -579,6 +841,22 @@ export async function runCli(argv, io, deps = {}) {
579
841
  return handleWorkshopArchive(io, ui, io.env, flags, mergedDeps);
580
842
  }
581
843
 
844
+ if (scope === "workshop" && action === "create-instance") {
845
+ return handleWorkshopCreateInstance(io, ui, io.env, positionals, flags, mergedDeps);
846
+ }
847
+
848
+ if (scope === "workshop" && action === "update-instance") {
849
+ return handleWorkshopUpdateInstance(io, ui, io.env, positionals, flags, mergedDeps);
850
+ }
851
+
852
+ if (scope === "workshop" && action === "prepare") {
853
+ return handleWorkshopPrepare(io, ui, io.env, positionals, flags, mergedDeps);
854
+ }
855
+
856
+ if (scope === "workshop" && action === "remove-instance") {
857
+ return handleWorkshopRemoveInstance(io, ui, io.env, positionals, flags, mergedDeps);
858
+ }
859
+
582
860
  if (scope === "workshop" && action === "phase" && subaction === "set") {
583
861
  return handleWorkshopPhaseSet(io, ui, io.env, positionals, mergedDeps);
584
862
  }