@harness-lab/cli 0.2.8 → 0.3.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 +34 -3
- package/assets/workshop-bundle/SKILL.md +28 -0
- package/assets/workshop-bundle/bundle-manifest.json +44 -52
- package/assets/workshop-bundle/content/challenge-cards/deck.md +19 -17
- package/assets/workshop-bundle/content/challenge-cards/locales/en/deck.md +7 -5
- package/assets/workshop-bundle/content/codex-craft.md +190 -0
- package/assets/workshop-bundle/content/facilitation/codex-setup-verification.md +5 -5
- package/assets/workshop-bundle/content/facilitation/master-guide.md +137 -67
- package/assets/workshop-bundle/content/project-briefs/code-review-helper.md +9 -9
- package/assets/workshop-bundle/content/project-briefs/devtoolbox-cli.md +11 -9
- package/assets/workshop-bundle/content/project-briefs/doc-generator.md +10 -8
- package/assets/workshop-bundle/content/project-briefs/locales/en/devtoolbox-cli.md +4 -2
- package/assets/workshop-bundle/content/project-briefs/locales/en/doc-generator.md +5 -3
- package/assets/workshop-bundle/content/project-briefs/locales/en/metrics-dashboard.md +4 -2
- package/assets/workshop-bundle/content/project-briefs/locales/en/standup-bot.md +4 -2
- package/assets/workshop-bundle/content/project-briefs/metrics-dashboard.md +14 -12
- package/assets/workshop-bundle/content/project-briefs/standup-bot.md +11 -9
- package/assets/workshop-bundle/content/talks/codex-demo-script.md +12 -10
- package/assets/workshop-bundle/content/talks/context-is-king.md +26 -23
- package/assets/workshop-bundle/docs/harness-cli-foundation.md +23 -11
- package/assets/workshop-bundle/docs/learner-resource-kit.md +37 -37
- package/assets/workshop-bundle/materials/coaching-codex.md +76 -0
- package/assets/workshop-bundle/materials/locales/en/participant-resource-kit.md +14 -2
- package/assets/workshop-bundle/materials/participant-resource-kit.md +23 -11
- package/assets/workshop-bundle/workshop-blueprint/README.md +2 -5
- package/assets/workshop-bundle/workshop-blueprint/day-structure.md +14 -0
- package/assets/workshop-bundle/workshop-skill/analyze-checklist.md +3 -3
- package/assets/workshop-bundle/workshop-skill/closing-skill.md +5 -5
- package/assets/workshop-bundle/workshop-skill/commands.md +13 -13
- package/assets/workshop-bundle/workshop-skill/facilitator.md +95 -0
- package/assets/workshop-bundle/workshop-skill/follow-up-package.md +13 -8
- package/assets/workshop-bundle/workshop-skill/install.md +8 -8
- package/assets/workshop-bundle/workshop-skill/locales/en/follow-up-package.md +8 -3
- package/assets/workshop-bundle/workshop-skill/locales/en/recap.md +8 -1
- package/assets/workshop-bundle/workshop-skill/locales/en/reference.md +19 -3
- package/assets/workshop-bundle/workshop-skill/locales/en/setup.md +1 -1
- package/assets/workshop-bundle/workshop-skill/recap.md +12 -5
- package/assets/workshop-bundle/workshop-skill/reference.md +45 -29
- package/assets/workshop-bundle/workshop-skill/setup.md +11 -11
- package/assets/workshop-bundle/workshop-skill/template-agents.md +4 -4
- package/package.json +1 -1
- package/src/client.js +18 -0
- package/src/io.js +11 -2
- package/src/run-cli.js +266 -8
- package/src/session-store.js +1 -0
- package/src/skill-install.js +108 -7
- package/src/workshop-bundle.js +48 -3
- package/assets/workshop-bundle/content/czech-editorial-review-checklist.md +0 -88
- package/assets/workshop-bundle/content/style-examples.md +0 -127
- package/assets/workshop-bundle/content/style-guide.md +0 -108
- package/assets/workshop-bundle/workshop-blueprint/edit-boundaries.md +0 -64
package/src/run-cli.js
CHANGED
|
@@ -16,6 +16,7 @@ function sleep(ms) {
|
|
|
16
16
|
function parseArgs(argv) {
|
|
17
17
|
const positionals = [];
|
|
18
18
|
const flags = {};
|
|
19
|
+
const booleanFlags = new Set(["json", "help", "version", "force", "no-open", "clear"]);
|
|
19
20
|
|
|
20
21
|
for (let index = 0; index < argv.length; index += 1) {
|
|
21
22
|
const value = argv[index];
|
|
@@ -29,6 +30,10 @@ function parseArgs(argv) {
|
|
|
29
30
|
}
|
|
30
31
|
if (value.startsWith("--")) {
|
|
31
32
|
const key = value.slice(2);
|
|
33
|
+
if (booleanFlags.has(key)) {
|
|
34
|
+
flags[key] = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
32
37
|
const next = argv[index + 1];
|
|
33
38
|
if (!next || next.startsWith("--")) {
|
|
34
39
|
flags[key] = true;
|
|
@@ -161,12 +166,47 @@ function summarizeWorkshopInstance(instance) {
|
|
|
161
166
|
};
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
function summarizeParticipantAccess(participantAccess) {
|
|
170
|
+
return {
|
|
171
|
+
instanceId: participantAccess?.instanceId ?? null,
|
|
172
|
+
active: participantAccess?.active ?? false,
|
|
173
|
+
version: participantAccess?.version ?? null,
|
|
174
|
+
codeId: participantAccess?.codeId ?? null,
|
|
175
|
+
expiresAt: participantAccess?.expiresAt ?? null,
|
|
176
|
+
canRevealCurrent: participantAccess?.canRevealCurrent ?? false,
|
|
177
|
+
source: participantAccess?.source ?? "missing",
|
|
178
|
+
currentCode: participantAccess?.currentCode ?? null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveCurrentInstanceTarget(session, env) {
|
|
183
|
+
if (typeof session?.selectedInstanceId === "string" && session.selectedInstanceId.trim().length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
instanceId: session.selectedInstanceId.trim(),
|
|
186
|
+
source: "session",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof env?.HARNESS_WORKSHOP_INSTANCE_ID === "string" && env.HARNESS_WORKSHOP_INSTANCE_ID.trim().length > 0) {
|
|
191
|
+
return {
|
|
192
|
+
instanceId: env.HARNESS_WORKSHOP_INSTANCE_ID.trim(),
|
|
193
|
+
source: "env",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
instanceId: null,
|
|
199
|
+
source: "none",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
164
203
|
function printUsage(io, ui) {
|
|
165
204
|
ui.heading("Harness CLI");
|
|
166
205
|
ui.paragraph(`Version ${version}`);
|
|
167
206
|
ui.blank();
|
|
168
207
|
ui.section("Usage");
|
|
169
208
|
ui.commandList([
|
|
209
|
+
"harness [--json] <command>",
|
|
170
210
|
"harness --help",
|
|
171
211
|
"harness --version",
|
|
172
212
|
"harness version",
|
|
@@ -178,9 +218,12 @@ function printUsage(io, ui) {
|
|
|
178
218
|
"harness auth logout",
|
|
179
219
|
"harness auth status",
|
|
180
220
|
"harness skill install [--target PATH] [--force]",
|
|
221
|
+
"harness workshop current-instance",
|
|
222
|
+
"harness workshop select-instance <instance-id> [--clear]",
|
|
181
223
|
"harness workshop status",
|
|
182
224
|
"harness workshop list-instances",
|
|
183
225
|
"harness workshop show-instance <instance-id>",
|
|
226
|
+
"harness workshop participant-access [<instance-id>] [--rotate] [--code VALUE]",
|
|
184
227
|
"harness workshop archive [--notes TEXT]",
|
|
185
228
|
"harness workshop create-instance [<instance-id>] [--template-id ID] [--content-lang cs|en] [--event-title TEXT] [--city CITY]",
|
|
186
229
|
"harness workshop update-instance <instance-id> [--content-lang cs|en] [--event-title TEXT] [--city CITY]",
|
|
@@ -563,6 +606,22 @@ async function handleAuthLogout(io, ui, env, deps) {
|
|
|
563
606
|
return 0;
|
|
564
607
|
}
|
|
565
608
|
|
|
609
|
+
function renderSelectedInstanceBanner(ui, target) {
|
|
610
|
+
if (ui.jsonMode) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const instanceId = target.instanceId ?? "none";
|
|
614
|
+
const label = target.source === "session"
|
|
615
|
+
? "Selected instance (locally selected)"
|
|
616
|
+
: target.instanceId
|
|
617
|
+
? `Selected instance (from ${target.source})`
|
|
618
|
+
: "Selected instance";
|
|
619
|
+
ui.keyValue(label, instanceId);
|
|
620
|
+
if (!target.instanceId) {
|
|
621
|
+
ui.keyValue("", "no instance is currently selected — run `harness workshop select-instance <id>` to pin one");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
566
625
|
async function handleWorkshopStatus(io, ui, env, deps) {
|
|
567
626
|
const session = await requireSession(io, ui, env);
|
|
568
627
|
if (!session) {
|
|
@@ -571,9 +630,42 @@ async function handleWorkshopStatus(io, ui, env, deps) {
|
|
|
571
630
|
|
|
572
631
|
try {
|
|
573
632
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
633
|
+
const target = resolveCurrentInstanceTarget(session, env);
|
|
634
|
+
|
|
635
|
+
renderSelectedInstanceBanner(ui, target);
|
|
636
|
+
|
|
637
|
+
if (target.source === "session" && target.instanceId) {
|
|
638
|
+
const [instanceResult, agenda] = await Promise.all([
|
|
639
|
+
client.getWorkshopInstance(target.instanceId),
|
|
640
|
+
client.getWorkshopAgenda(target.instanceId),
|
|
641
|
+
]);
|
|
642
|
+
ui.json("Workshop Status", {
|
|
643
|
+
ok: true,
|
|
644
|
+
selectedInstance: {
|
|
645
|
+
instanceId: target.instanceId,
|
|
646
|
+
source: target.source,
|
|
647
|
+
selected: true,
|
|
648
|
+
},
|
|
649
|
+
targetInstanceId: target.instanceId,
|
|
650
|
+
targetSource: target.source,
|
|
651
|
+
...summarizeWorkshopInstance(instanceResult.instance),
|
|
652
|
+
workshopMeta: instanceResult.instance?.workshopMeta ?? null,
|
|
653
|
+
currentPhase: agenda.phase,
|
|
654
|
+
agendaItems: Array.isArray(agenda.items) ? agenda.items.length : null,
|
|
655
|
+
});
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
574
659
|
const [workshop, agenda] = await Promise.all([client.getWorkshopStatus(), client.getAgenda()]);
|
|
575
660
|
ui.json("Workshop Status", {
|
|
576
661
|
ok: true,
|
|
662
|
+
selectedInstance: {
|
|
663
|
+
instanceId: target.instanceId ?? null,
|
|
664
|
+
source: target.source,
|
|
665
|
+
selected: Boolean(target.instanceId),
|
|
666
|
+
},
|
|
667
|
+
targetInstanceId: target.instanceId,
|
|
668
|
+
targetSource: target.source,
|
|
577
669
|
workshopId: workshop.workshopId,
|
|
578
670
|
workshopMeta: workshop.workshopMeta,
|
|
579
671
|
currentPhase: agenda.phase,
|
|
@@ -589,6 +681,107 @@ async function handleWorkshopStatus(io, ui, env, deps) {
|
|
|
589
681
|
}
|
|
590
682
|
}
|
|
591
683
|
|
|
684
|
+
async function handleWorkshopCurrentInstance(io, ui, env, deps) {
|
|
685
|
+
const session = await requireSession(io, ui, env);
|
|
686
|
+
if (!session) {
|
|
687
|
+
return 1;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const target = resolveCurrentInstanceTarget(session, env);
|
|
691
|
+
if (!target.instanceId) {
|
|
692
|
+
ui.json("Workshop Current Instance", {
|
|
693
|
+
ok: true,
|
|
694
|
+
instanceId: null,
|
|
695
|
+
source: target.source,
|
|
696
|
+
selectedInstanceId: session.selectedInstanceId ?? null,
|
|
697
|
+
});
|
|
698
|
+
return 0;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
703
|
+
const result = await client.getWorkshopInstance(target.instanceId);
|
|
704
|
+
ui.json("Workshop Current Instance", {
|
|
705
|
+
ok: true,
|
|
706
|
+
source: target.source,
|
|
707
|
+
selectedInstanceId: session.selectedInstanceId ?? null,
|
|
708
|
+
...summarizeWorkshopInstance(result.instance),
|
|
709
|
+
instance: result.instance,
|
|
710
|
+
});
|
|
711
|
+
return 0;
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (error instanceof HarnessApiError) {
|
|
714
|
+
ui.status("error", `Current instance lookup failed: ${error.message}`, { stream: "stderr" });
|
|
715
|
+
return 1;
|
|
716
|
+
}
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function handleWorkshopSelectInstance(io, ui, env, positionals, flags, deps) {
|
|
722
|
+
const session = await requireSession(io, ui, env);
|
|
723
|
+
if (!session) {
|
|
724
|
+
return 1;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (flags.clear === true) {
|
|
728
|
+
const nextSession = { ...session };
|
|
729
|
+
delete nextSession.selectedInstanceId;
|
|
730
|
+
if (!(await persistSession(io, ui, env, nextSession))) {
|
|
731
|
+
return 1;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const target = resolveCurrentInstanceTarget(nextSession, env);
|
|
735
|
+
ui.json("Workshop Select Instance", {
|
|
736
|
+
ok: true,
|
|
737
|
+
selectedInstanceId: null,
|
|
738
|
+
currentInstanceId: target.instanceId,
|
|
739
|
+
source: target.source,
|
|
740
|
+
cleared: true,
|
|
741
|
+
});
|
|
742
|
+
return 0;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const instanceId = await readRequiredCommandValue(
|
|
746
|
+
io,
|
|
747
|
+
flags,
|
|
748
|
+
["id", "instance-id"],
|
|
749
|
+
"Instance id: ",
|
|
750
|
+
readOptionalPositional(positionals, 2),
|
|
751
|
+
);
|
|
752
|
+
if (!instanceId) {
|
|
753
|
+
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
754
|
+
return 1;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
759
|
+
const result = await client.getWorkshopInstance(instanceId);
|
|
760
|
+
const nextSession = {
|
|
761
|
+
...session,
|
|
762
|
+
selectedInstanceId: result.instance?.id ?? instanceId,
|
|
763
|
+
};
|
|
764
|
+
if (!(await persistSession(io, ui, env, nextSession))) {
|
|
765
|
+
return 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
ui.json("Workshop Select Instance", {
|
|
769
|
+
ok: true,
|
|
770
|
+
source: "session",
|
|
771
|
+
selectedInstanceId: nextSession.selectedInstanceId,
|
|
772
|
+
...summarizeWorkshopInstance(result.instance),
|
|
773
|
+
instance: result.instance,
|
|
774
|
+
});
|
|
775
|
+
return 0;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (error instanceof HarnessApiError) {
|
|
778
|
+
ui.status("error", `Select instance failed: ${error.message}`, { stream: "stderr" });
|
|
779
|
+
return 1;
|
|
780
|
+
}
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
592
785
|
async function handleWorkshopListInstances(io, ui, env, deps) {
|
|
593
786
|
const session = await requireSession(io, ui, env);
|
|
594
787
|
if (!session) {
|
|
@@ -625,7 +818,7 @@ async function handleWorkshopShowInstance(io, ui, env, positionals, flags, deps)
|
|
|
625
818
|
flags,
|
|
626
819
|
["id", "instance-id"],
|
|
627
820
|
"Instance id: ",
|
|
628
|
-
readOptionalPositional(positionals, 2),
|
|
821
|
+
readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
|
|
629
822
|
);
|
|
630
823
|
if (!instanceId) {
|
|
631
824
|
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
@@ -650,6 +843,55 @@ async function handleWorkshopShowInstance(io, ui, env, positionals, flags, deps)
|
|
|
650
843
|
}
|
|
651
844
|
}
|
|
652
845
|
|
|
846
|
+
async function handleWorkshopParticipantAccess(io, ui, env, positionals, flags, deps) {
|
|
847
|
+
const session = await requireSession(io, ui, env);
|
|
848
|
+
if (!session) {
|
|
849
|
+
return 1;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const instanceId = await readRequiredCommandValue(
|
|
853
|
+
io,
|
|
854
|
+
flags,
|
|
855
|
+
["id", "instance-id"],
|
|
856
|
+
"Instance id: ",
|
|
857
|
+
readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
|
|
858
|
+
);
|
|
859
|
+
if (!instanceId) {
|
|
860
|
+
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
861
|
+
return 1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
866
|
+
if (flags.rotate === true) {
|
|
867
|
+
const result = await client.issueWorkshopParticipantAccess(instanceId, {
|
|
868
|
+
...(typeof flags.code === "string" ? { code: flags.code } : {}),
|
|
869
|
+
});
|
|
870
|
+
ui.json("Workshop Participant Access", {
|
|
871
|
+
ok: true,
|
|
872
|
+
issuedCode: result.issuedCode ?? null,
|
|
873
|
+
...summarizeParticipantAccess(result.participantAccess),
|
|
874
|
+
participantAccess: result.participantAccess,
|
|
875
|
+
});
|
|
876
|
+
return 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const result = await client.getWorkshopParticipantAccess(instanceId);
|
|
880
|
+
ui.json("Workshop Participant Access", {
|
|
881
|
+
ok: true,
|
|
882
|
+
...summarizeParticipantAccess(result.participantAccess),
|
|
883
|
+
participantAccess: result.participantAccess,
|
|
884
|
+
});
|
|
885
|
+
return 0;
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (error instanceof HarnessApiError) {
|
|
888
|
+
ui.status("error", `Participant access failed: ${error.message}`, { stream: "stderr" });
|
|
889
|
+
return 1;
|
|
890
|
+
}
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
653
895
|
async function handleWorkshopArchive(io, ui, env, flags, deps) {
|
|
654
896
|
const session = await requireSession(io, ui, env);
|
|
655
897
|
if (!session) {
|
|
@@ -724,7 +966,7 @@ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, dep
|
|
|
724
966
|
flags,
|
|
725
967
|
["id"],
|
|
726
968
|
"Instance id: ",
|
|
727
|
-
readOptionalPositional(positionals, 2),
|
|
969
|
+
readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
|
|
728
970
|
);
|
|
729
971
|
if (!instanceId) {
|
|
730
972
|
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
@@ -739,7 +981,7 @@ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, dep
|
|
|
739
981
|
if (!hasWorkshopMetadataInput(payload)) {
|
|
740
982
|
ui.status(
|
|
741
983
|
"error",
|
|
742
|
-
"At least one metadata field is required. Use flags such as --event-title, --date-range, --venue-name, or --room-name.",
|
|
984
|
+
"At least one metadata field is required. Use flags such as --content-lang, --event-title, --date-range, --venue-name, or --room-name.",
|
|
743
985
|
{ stream: "stderr" },
|
|
744
986
|
);
|
|
745
987
|
return 1;
|
|
@@ -774,7 +1016,7 @@ async function handleWorkshopPrepare(io, ui, env, positionals, flags, deps) {
|
|
|
774
1016
|
flags,
|
|
775
1017
|
["id", "instance-id"],
|
|
776
1018
|
"Instance id: ",
|
|
777
|
-
readOptionalPositional(positionals, 2),
|
|
1019
|
+
readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
|
|
778
1020
|
);
|
|
779
1021
|
if (!instanceId) {
|
|
780
1022
|
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
@@ -810,7 +1052,7 @@ async function handleWorkshopResetInstance(io, ui, env, positionals, flags, deps
|
|
|
810
1052
|
flags,
|
|
811
1053
|
["id", "instance-id"],
|
|
812
1054
|
"Instance id: ",
|
|
813
|
-
readOptionalPositional(positionals, 2),
|
|
1055
|
+
readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
|
|
814
1056
|
);
|
|
815
1057
|
if (!instanceId) {
|
|
816
1058
|
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
@@ -845,7 +1087,7 @@ async function handleWorkshopRemoveInstance(io, ui, env, positionals, flags, dep
|
|
|
845
1087
|
flags,
|
|
846
1088
|
["id", "instance-id"],
|
|
847
1089
|
"Instance id: ",
|
|
848
|
-
readOptionalPositional(positionals, 2),
|
|
1090
|
+
readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
|
|
849
1091
|
);
|
|
850
1092
|
if (!instanceId) {
|
|
851
1093
|
ui.status("error", "Instance id is required.", { stream: "stderr" });
|
|
@@ -884,7 +1126,11 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
|
|
|
884
1126
|
|
|
885
1127
|
try {
|
|
886
1128
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
887
|
-
const
|
|
1129
|
+
const target = resolveCurrentInstanceTarget(session, env);
|
|
1130
|
+
const result =
|
|
1131
|
+
target.source === "session" && target.instanceId
|
|
1132
|
+
? await client.setCurrentPhaseForInstance(target.instanceId, phaseId)
|
|
1133
|
+
: await client.setCurrentPhase(phaseId);
|
|
888
1134
|
ui.json("Workshop Phase", result);
|
|
889
1135
|
return 0;
|
|
890
1136
|
} catch (error) {
|
|
@@ -899,8 +1145,8 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
|
|
|
899
1145
|
export async function runCli(argv, io, deps = {}) {
|
|
900
1146
|
const fetchFn = deps.fetchFn ?? globalThis.fetch;
|
|
901
1147
|
const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
|
|
902
|
-
const ui = createCliUi(io);
|
|
903
1148
|
const { positionals, flags } = parseArgs(argv);
|
|
1149
|
+
const ui = createCliUi(io, { jsonMode: flags.json === true || flags.output === "json" });
|
|
904
1150
|
const [scope, action, subaction] = positionals;
|
|
905
1151
|
|
|
906
1152
|
if (flags.help === true) {
|
|
@@ -943,6 +1189,14 @@ export async function runCli(argv, io, deps = {}) {
|
|
|
943
1189
|
return handleSkillInstall(io, ui, mergedDeps, flags);
|
|
944
1190
|
}
|
|
945
1191
|
|
|
1192
|
+
if (scope === "workshop" && action === "current-instance") {
|
|
1193
|
+
return handleWorkshopCurrentInstance(io, ui, io.env, mergedDeps);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (scope === "workshop" && action === "select-instance") {
|
|
1197
|
+
return handleWorkshopSelectInstance(io, ui, io.env, positionals, flags, mergedDeps);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
946
1200
|
if (scope === "workshop" && action === "status") {
|
|
947
1201
|
return handleWorkshopStatus(io, ui, io.env, mergedDeps);
|
|
948
1202
|
}
|
|
@@ -955,6 +1209,10 @@ export async function runCli(argv, io, deps = {}) {
|
|
|
955
1209
|
return handleWorkshopShowInstance(io, ui, io.env, positionals, flags, mergedDeps);
|
|
956
1210
|
}
|
|
957
1211
|
|
|
1212
|
+
if (scope === "workshop" && action === "participant-access") {
|
|
1213
|
+
return handleWorkshopParticipantAccess(io, ui, io.env, positionals, flags, mergedDeps);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
958
1216
|
if (scope === "workshop" && action === "archive") {
|
|
959
1217
|
return handleWorkshopArchive(io, ui, io.env, flags, mergedDeps);
|
|
960
1218
|
}
|
package/src/session-store.js
CHANGED
|
@@ -314,6 +314,7 @@ export function sanitizeSession(session, env) {
|
|
|
314
314
|
authType: session.authType,
|
|
315
315
|
username: session.username ?? null,
|
|
316
316
|
email: session.email ?? null,
|
|
317
|
+
selectedInstanceId: session.selectedInstanceId ?? null,
|
|
317
318
|
loggedInAt: session.loggedInAt,
|
|
318
319
|
expiresAt: session.expiresAt ?? null,
|
|
319
320
|
mode: session.mode ?? "local-dev",
|
package/src/skill-install.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
3
4
|
import {
|
|
4
5
|
createWorkshopBundleManifestFromDirectory,
|
|
5
6
|
createWorkshopBundleManifestFromSource,
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
WORKSHOP_SKILL_NAME,
|
|
13
14
|
} from "./workshop-bundle.js";
|
|
14
15
|
|
|
16
|
+
const MIN_NODE_MAJOR = 22;
|
|
17
|
+
|
|
15
18
|
export class SkillInstallError extends Error {
|
|
16
19
|
constructor(message, options = {}) {
|
|
17
20
|
super(message);
|
|
@@ -20,6 +23,81 @@ export class SkillInstallError extends Error {
|
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
function assertSupportedNodeVersion() {
|
|
27
|
+
const raw = process.versions?.node;
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const major = Number.parseInt(raw.split(".")[0], 10);
|
|
32
|
+
if (Number.isFinite(major) && major < MIN_NODE_MAJOR) {
|
|
33
|
+
throw new SkillInstallError(
|
|
34
|
+
`Harness CLI requires Node.js ${MIN_NODE_MAJOR} or newer. This process is running Node.js ${raw}. Upgrade with your version manager (for example \`nvm install --lts\`) and re-run \`harness skill install\`.`,
|
|
35
|
+
{ code: "unsupported_node_version" },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function translateFileSystemError(error, context) {
|
|
41
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const targetPath = error.path ? ` (${error.path})` : "";
|
|
46
|
+
|
|
47
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
48
|
+
return new SkillInstallError(
|
|
49
|
+
`Harness CLI could not ${context}${targetPath} because the current user does not have write permission. Try running from a directory you own, or adjust the directory permissions. On macOS and Linux, that usually means avoiding system paths like /usr. On Windows, avoid running from a protected location such as C:\\Program Files.`,
|
|
50
|
+
{ code: "install_permission_denied" },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (error.code === "ENOSPC") {
|
|
55
|
+
return new SkillInstallError(
|
|
56
|
+
`Harness CLI could not ${context}${targetPath} because the disk is full. Free some space and re-run \`harness skill install\`.`,
|
|
57
|
+
{ code: "install_no_space" },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (error.code === "ENAMETOOLONG" || error.code === "ENOTDIR") {
|
|
62
|
+
return new SkillInstallError(
|
|
63
|
+
`Harness CLI could not ${context}${targetPath}. The target path is too long or part of it is not a directory. On Windows, this often happens when the repository lives in a deeply nested folder — move the repo closer to the drive root (for example C:\\repos\\your-project) and try again.`,
|
|
64
|
+
{ code: "install_path_invalid" },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error.code === "EROFS") {
|
|
69
|
+
return new SkillInstallError(
|
|
70
|
+
`Harness CLI could not ${context}${targetPath} because the file system is read-only. Re-run \`harness skill install\` from a writable directory.`,
|
|
71
|
+
{ code: "install_read_only" },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error.code === "EBUSY") {
|
|
76
|
+
return new SkillInstallError(
|
|
77
|
+
`Harness CLI could not ${context}${targetPath} because the target is busy (another process may hold it open). Close editors or agents pointing at \`.agents/skills/\` and re-run \`harness skill install\`.`,
|
|
78
|
+
{ code: "install_target_busy" },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fsWithActionableError(operation, context) {
|
|
86
|
+
try {
|
|
87
|
+
return await operation();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof SkillInstallError) {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const translated = translateFileSystemError(error, context);
|
|
93
|
+
if (translated) {
|
|
94
|
+
translated.cause = error;
|
|
95
|
+
throw translated;
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
23
101
|
async function resolveBundleSource() {
|
|
24
102
|
const packagedBundlePath = getPackagedWorkshopBundlePath();
|
|
25
103
|
if (await pathExists(path.join(packagedBundlePath, "SKILL.md"))) {
|
|
@@ -43,8 +121,14 @@ async function resolveBundleSource() {
|
|
|
43
121
|
}
|
|
44
122
|
|
|
45
123
|
async function ensureDirectory(targetPath) {
|
|
46
|
-
await
|
|
47
|
-
|
|
124
|
+
await fsWithActionableError(
|
|
125
|
+
() => fs.mkdir(targetPath, { recursive: true }),
|
|
126
|
+
`create the install target directory`,
|
|
127
|
+
);
|
|
128
|
+
const stat = await fsWithActionableError(
|
|
129
|
+
() => fs.stat(targetPath),
|
|
130
|
+
`inspect the install target directory`,
|
|
131
|
+
);
|
|
48
132
|
if (!stat.isDirectory()) {
|
|
49
133
|
throw new SkillInstallError(`Install target is not a directory: ${targetPath}`, {
|
|
50
134
|
code: "invalid_target",
|
|
@@ -71,14 +155,22 @@ async function getSourceBundleManifest(resolvedBundle) {
|
|
|
71
155
|
|
|
72
156
|
async function installFromResolvedBundle(resolvedBundle, installPath) {
|
|
73
157
|
if (resolvedBundle.mode === "packaged_bundle") {
|
|
74
|
-
await
|
|
158
|
+
await fsWithActionableError(
|
|
159
|
+
() => fs.cp(resolvedBundle.sourcePath, installPath, { recursive: true }),
|
|
160
|
+
`copy the workshop bundle into the install target`,
|
|
161
|
+
);
|
|
75
162
|
return;
|
|
76
163
|
}
|
|
77
164
|
|
|
78
|
-
await
|
|
165
|
+
await fsWithActionableError(
|
|
166
|
+
() => createWorkshopBundleFromSource(resolvedBundle.sourceRoot, installPath),
|
|
167
|
+
`build the workshop bundle from source into the install target`,
|
|
168
|
+
);
|
|
79
169
|
}
|
|
80
170
|
|
|
81
171
|
export async function installWorkshopSkill(startDir, options = {}) {
|
|
172
|
+
assertSupportedNodeVersion();
|
|
173
|
+
|
|
82
174
|
const resolvedBundle = await resolveBundleSource();
|
|
83
175
|
if (!resolvedBundle) {
|
|
84
176
|
throw new SkillInstallError(
|
|
@@ -106,7 +198,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
|
|
|
106
198
|
};
|
|
107
199
|
}
|
|
108
200
|
|
|
109
|
-
await
|
|
201
|
+
await fsWithActionableError(
|
|
202
|
+
() => fs.rm(installPath, { recursive: true, force: true }),
|
|
203
|
+
`remove the previous workshop bundle before refreshing it`,
|
|
204
|
+
);
|
|
110
205
|
|
|
111
206
|
return {
|
|
112
207
|
...(await installFreshBundle(resolvedBundle, installPath, targetRoot)),
|
|
@@ -115,7 +210,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
|
|
|
115
210
|
}
|
|
116
211
|
|
|
117
212
|
if (existingInstall && options.force === true) {
|
|
118
|
-
await
|
|
213
|
+
await fsWithActionableError(
|
|
214
|
+
() => fs.rm(installPath, { recursive: true, force: true }),
|
|
215
|
+
`remove the previous workshop bundle before reinstalling`,
|
|
216
|
+
);
|
|
119
217
|
}
|
|
120
218
|
|
|
121
219
|
const result = await installFreshBundle(resolvedBundle, installPath, targetRoot);
|
|
@@ -130,7 +228,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
|
|
|
130
228
|
}
|
|
131
229
|
|
|
132
230
|
async function installFreshBundle(resolvedBundle, installPath, targetRoot) {
|
|
133
|
-
await
|
|
231
|
+
await fsWithActionableError(
|
|
232
|
+
() => fs.mkdir(path.dirname(installPath), { recursive: true }),
|
|
233
|
+
`create the parent directory for the installed skill`,
|
|
234
|
+
);
|
|
134
235
|
await installFromResolvedBundle(resolvedBundle, installPath);
|
|
135
236
|
|
|
136
237
|
return {
|
package/src/workshop-bundle.js
CHANGED
|
@@ -26,8 +26,26 @@ const FILE_COPIES = [
|
|
|
26
26
|
["docs/locales/en/learner-reference-gallery.md", "docs/locales/en/learner-reference-gallery.md"],
|
|
27
27
|
["materials/participant-resource-kit.md", "materials/participant-resource-kit.md"],
|
|
28
28
|
["materials/locales/en/participant-resource-kit.md", "materials/locales/en/participant-resource-kit.md"],
|
|
29
|
+
["materials/coaching-codex.md", "materials/coaching-codex.md"],
|
|
29
30
|
];
|
|
30
31
|
|
|
32
|
+
// Files that live inside DIRECTORY_COPIES source trees but must not ship to
|
|
33
|
+
// participants. These are author/maintainer/copy-editor artifacts that have no
|
|
34
|
+
// runtime purpose inside the installed workshop skill. Keep the set minimal —
|
|
35
|
+
// every entry should be a file that (a) has no workshop-skill reference and
|
|
36
|
+
// (b) is clearly authoring/governance content rather than participant-facing.
|
|
37
|
+
const EXCLUDED_BUNDLE_PATHS = new Set([
|
|
38
|
+
"content/style-guide.md",
|
|
39
|
+
"content/style-examples.md",
|
|
40
|
+
"content/czech-reject-list.md",
|
|
41
|
+
"content/czech-editorial-review-checklist.md",
|
|
42
|
+
"workshop-blueprint/edit-boundaries.md",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function isExcludedBundlePath(bundleRelativePath) {
|
|
46
|
+
return EXCLUDED_BUNDLE_PATHS.has(normalizePathForManifest(bundleRelativePath));
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
export function getPackageRoot() {
|
|
32
50
|
return packageRoot;
|
|
33
51
|
}
|
|
@@ -66,24 +84,31 @@ async function copyDirectoryTree(sourceRoot, targetRoot) {
|
|
|
66
84
|
await copyDirectoryRecursive(
|
|
67
85
|
path.join(sourceRoot, sourceRelativePath),
|
|
68
86
|
path.join(targetRoot, targetRelativePath),
|
|
87
|
+
targetRelativePath,
|
|
69
88
|
);
|
|
70
89
|
}
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
async function copyDirectoryRecursive(sourcePath, targetPath) {
|
|
92
|
+
async function copyDirectoryRecursive(sourcePath, targetPath, bundleRelativePrefix) {
|
|
74
93
|
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
75
94
|
await fs.mkdir(targetPath, { recursive: true });
|
|
76
95
|
|
|
77
96
|
for (const entry of entries) {
|
|
78
97
|
const sourceEntryPath = path.join(sourcePath, entry.name);
|
|
79
98
|
const targetEntryPath = path.join(targetPath, entry.name);
|
|
99
|
+
const entryBundleRelativePath = bundleRelativePrefix
|
|
100
|
+
? path.join(bundleRelativePrefix, entry.name)
|
|
101
|
+
: entry.name;
|
|
80
102
|
|
|
81
103
|
if (entry.isDirectory()) {
|
|
82
|
-
await copyDirectoryRecursive(sourceEntryPath, targetEntryPath);
|
|
104
|
+
await copyDirectoryRecursive(sourceEntryPath, targetEntryPath, entryBundleRelativePath);
|
|
83
105
|
continue;
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
if (entry.isFile()) {
|
|
109
|
+
if (isExcludedBundlePath(entryBundleRelativePath)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
87
112
|
await fs.copyFile(sourceEntryPath, targetEntryPath);
|
|
88
113
|
}
|
|
89
114
|
}
|
|
@@ -187,6 +212,9 @@ export async function createWorkshopBundleManifestFromSource(sourceRoot) {
|
|
|
187
212
|
if (targetRelative === "workshop-skill/SKILL.md") {
|
|
188
213
|
continue;
|
|
189
214
|
}
|
|
215
|
+
if (isExcludedBundlePath(targetRelative)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
190
218
|
|
|
191
219
|
entries.push({
|
|
192
220
|
absolutePath: file.absolutePath,
|
|
@@ -225,6 +253,20 @@ async function writeWorkshopBundleManifest(bundleRoot, manifest) {
|
|
|
225
253
|
);
|
|
226
254
|
}
|
|
227
255
|
|
|
256
|
+
async function pruneBundleFiles(bundleRoot, manifest) {
|
|
257
|
+
const expectedFiles = new Set([
|
|
258
|
+
WORKSHOP_BUNDLE_MANIFEST,
|
|
259
|
+
...manifest.files.map((file) => file.path),
|
|
260
|
+
]);
|
|
261
|
+
const currentFiles = await listFilesRecursive(bundleRoot);
|
|
262
|
+
|
|
263
|
+
for (const file of currentFiles) {
|
|
264
|
+
if (!expectedFiles.has(file.relativePath)) {
|
|
265
|
+
await fs.rm(file.absolutePath, { force: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
228
270
|
export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, options = {}) {
|
|
229
271
|
if (options.clean === true) {
|
|
230
272
|
await fs.rm(targetRoot, { recursive: true, force: true });
|
|
@@ -234,6 +276,9 @@ export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, opt
|
|
|
234
276
|
await fs.rm(path.join(targetRoot, "workshop-skill", "SKILL.md"), { force: true });
|
|
235
277
|
await copyBundleFiles(sourceRoot, targetRoot);
|
|
236
278
|
const manifest = await createWorkshopBundleManifestFromSource(sourceRoot);
|
|
279
|
+
if (options.prune === true) {
|
|
280
|
+
await pruneBundleFiles(targetRoot, manifest);
|
|
281
|
+
}
|
|
237
282
|
await writeWorkshopBundleManifest(targetRoot, manifest);
|
|
238
283
|
}
|
|
239
284
|
|
|
@@ -250,7 +295,7 @@ export async function syncPackagedWorkshopBundle() {
|
|
|
250
295
|
export async function syncRepoBundledWorkshopSkill() {
|
|
251
296
|
const sourceRoot = getRepoWorkshopSourceRoot();
|
|
252
297
|
const bundleRoot = getRepoBundledWorkshopSkillPath();
|
|
253
|
-
await createWorkshopBundleFromSource(sourceRoot, bundleRoot, {
|
|
298
|
+
await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { prune: true });
|
|
254
299
|
return {
|
|
255
300
|
sourceRoot,
|
|
256
301
|
bundleRoot,
|