@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 +14 -0
- package/package.json +1 -1
- package/src/client.js +21 -0
- package/src/run-cli.js +278 -0
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
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
|
}
|