@catalyst-cloud/cli 0.15.3 → 0.15.5
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/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/dist/args.js +5 -1
- package/dist/cli.js +39 -3
- package/dist/onboard-checklist.js +3 -2
- package/dist/onboard-headless.js +3 -0
- package/dist/onboard-repositories.js +197 -6
- package/dist/onboard-standalone-copy.js +21 -5
- package/dist/onboard-ui.js +166 -29
- package/dist/onboard.js +24 -7
- package/dist/setup-brand.js +202 -0
- package/dist/setup-onboard-copy.js +92 -16
- package/dist/setup-render.js +62 -37
- package/dist/terminal-background.js +220 -0
- package/package.json +1 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +1 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +1 -1
package/dist/onboard-ui.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { COMPUTER_CHECKS, standalonePlan, standalonePlanNotes, pendingContinuation, } from "./onboard-standalone-copy.js";
|
|
2
2
|
import { trackedPrompt } from "./setup-prompt-frame.js";
|
|
3
3
|
import { createOnboardInterrupts } from "./onboard-interrupts.js";
|
|
4
|
-
import { setupStepView, CONNECTION_REPAIR_REASONS, setupFinalScreen, setupBrowserInstruction, setupCommandWords as say, setupPartHeading, setupPartProgress, setupTrackerRows, SETUP_PART_NUMBERS, SETUP_PART_OF, SETUP_PARTS, } from "./setup-onboard-copy.js";
|
|
4
|
+
import { setupStepView, CONNECTION_REPAIR_REASONS, setupFinalScreen, setupBrowserInstruction, setupCommandWords as say, setupWaitsOnAdmin, TIMED_OUT_REASONS, setupPartHeading, setupPartProgress, setupTrackerRows, SETUP_PART_NUMBERS, SETUP_PART_OF, SETUP_PARTS, } from "./setup-onboard-copy.js";
|
|
5
5
|
import { createOnboardProgress, } from "./onboard-progress.js";
|
|
6
6
|
import { CREATE_TEAM_CHOICE, suggestTeamKey, teamKeyProblem, teamNameProblem, } from "./onboard-team-create.js";
|
|
7
7
|
import { FIRST_TICKET_SAMPLE, FIRST_TICKET_SKIP, } from "./onboard-first-ticket.js";
|
|
8
|
-
import { ONBOARD_STEPS, ONBOARD_TITLES, stepSatisfied, onboardIdentityLines, onboardNextActions, onboardReadyForWork, onboardStepAction, } from "./onboard.js";
|
|
8
|
+
import { ONBOARD_STEPS, ONBOARD_TITLES, stepSatisfied, onboardHeaderIdentity, onboardIdentityLines, onboardNextActions, onboardReadyForWork, onboardStepAction, } from "./onboard.js";
|
|
9
9
|
import { onboardStepDetail, onboardReasonText } from "./onboard-next.js";
|
|
10
|
+
import { CONNECT_PAGE_PATH } from "./onboard-checklist.js";
|
|
11
|
+
/** What the runner question's answers mean, said before it is asked (CTC-4739). */
|
|
12
|
+
const RUNNER_EXPLAINED = [
|
|
13
|
+
"Yes makes this computer pick up tickets and run agents in Docker with your workspace's AI accounts. It uses this computer's CPU and memory while it is on, and turning it off or letting it sleep pauses that work.",
|
|
14
|
+
"No is right for a personal workstation: your workspace's runner hosts do the work.",
|
|
15
|
+
"Pass --no-runner or --runner to skip this question next time.",
|
|
16
|
+
];
|
|
10
17
|
export function shouldUseOnboardUi(args, stdoutTty) {
|
|
11
18
|
return (stdoutTty &&
|
|
12
19
|
!args.json &&
|
|
@@ -43,6 +50,11 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
43
50
|
let planned = {};
|
|
44
51
|
let rechecking = false;
|
|
45
52
|
let shownPlan;
|
|
53
|
+
// CTC-4680: steps whose wait was on the Connect accounts page. That page does not expire, so a
|
|
54
|
+
// wait that ran out is setup giving up, and the way back is the same page.
|
|
55
|
+
const connectWaits = new Map();
|
|
56
|
+
const onConnectPage = (step) => connectWaits.has(step.id) && TIMED_OUT_REASONS.has(step.reason ?? "");
|
|
57
|
+
const outcome = (step, view) => onConnectPage(step) ? "setup stopped waiting" : view.outcome;
|
|
46
58
|
let browserOpened = true;
|
|
47
59
|
let questionOpen = false;
|
|
48
60
|
let signinShown = false;
|
|
@@ -98,11 +110,15 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
98
110
|
renderer.heading(setupPartHeading(next, renderer.traits.unicode));
|
|
99
111
|
return;
|
|
100
112
|
}
|
|
101
|
-
const progress = setupPartProgress(journalSeen, planned.scope);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
const progress = setupPartProgress(journalSeen, planned.scope, "run");
|
|
114
|
+
const got = progress[previous];
|
|
115
|
+
const left = got.total - got.done;
|
|
116
|
+
// Mid-run nothing yet says whether an owner or admin has done their part, so this is neutral.
|
|
117
|
+
renderer.heading(!left
|
|
118
|
+
? `Part ${previous} done. ${SETUP_PARTS[previous].done}`
|
|
119
|
+
: left === got.admin + got.aside
|
|
120
|
+
? `The rest of part ${previous} is for an owner or admin of your Catalyst workspace.`
|
|
121
|
+
: `Part ${previous} has ${left === 1 ? "1 step" : `${left} steps`} left. Setup carries on with part ${next}.`);
|
|
106
122
|
renderer.tracker(setupTrackerRows(progress, { next }));
|
|
107
123
|
planPart(renderer, next, journalSeen);
|
|
108
124
|
renderer.blank();
|
|
@@ -221,10 +237,54 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
221
237
|
return result.answer;
|
|
222
238
|
};
|
|
223
239
|
let summaryShown = false;
|
|
224
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The end screen's actions, split by who acts: the person's own, and an owner's or admin's
|
|
242
|
+
* (CTC-4680). A step paused with Ctrl-C is not an action: the last line says how to carry on.
|
|
243
|
+
*/
|
|
244
|
+
const sortActions = (journal) => {
|
|
225
245
|
const screen = setupFinalScreen(journal, deps.baseUrl?.(), abort.signal.aborted);
|
|
246
|
+
const step = (id) => journal.steps.find((s) => s.id === id) ?? { id, state: "pending" };
|
|
247
|
+
const text = (action) => {
|
|
248
|
+
const row = step(action.id);
|
|
249
|
+
if (!onConnectPage(row))
|
|
250
|
+
return say(action.text);
|
|
251
|
+
const kinds = row.evidence?.aiAccountKinds;
|
|
252
|
+
// #277's rule: subscription wording only for a workspace the cloud enables for it.
|
|
253
|
+
const what = action.id === "accounts"
|
|
254
|
+
? typeof kinds === "string" && kinds.split(",").includes("subscription")
|
|
255
|
+
? "add an API key or connect a subscription"
|
|
256
|
+
: "add an API key"
|
|
257
|
+
: action.id === "github.install"
|
|
258
|
+
? "install Catalyst on GitHub"
|
|
259
|
+
: `finish "${title(action.id)}" there`;
|
|
260
|
+
return `Open ${connectWaits.get(action.id)} and ${what}.`;
|
|
261
|
+
};
|
|
262
|
+
const live = screen.actions.filter((action) => step(action.id).reason !== "interrupted");
|
|
263
|
+
const theirs = live.filter((action) => setupWaitsOnAdmin(step(action.id), journal));
|
|
264
|
+
const mine = live.filter((action) => !theirs.includes(action));
|
|
265
|
+
// Steps only an owner or admin can do, less any already listed as their task above.
|
|
266
|
+
const skipped = journal.steps.filter((s) => s.reason === "member_scope" &&
|
|
267
|
+
setupWaitsOnAdmin(s, journal) &&
|
|
268
|
+
SETUP_PART_NUMBERS[s.id] !== undefined &&
|
|
269
|
+
s.id !== "projects" &&
|
|
270
|
+
!theirs.some((action) => action.id === s.id));
|
|
271
|
+
const waiting = theirs.length > 0 || skipped.length > 0;
|
|
272
|
+
const own = mine.length;
|
|
273
|
+
const next = screen.paused ||
|
|
274
|
+
!/^Next: run catalyst onboard after you finish/.test(screen.next)
|
|
275
|
+
? say(screen.next)
|
|
276
|
+
: own
|
|
277
|
+
? `Next: run catalyst setup after you finish ${own === 1 ? "1" : own === 2 ? "1 and 2" : `1 to ${own}`}.`
|
|
278
|
+
: waiting
|
|
279
|
+
? "Next: run catalyst setup once they have."
|
|
280
|
+
: "Next: run catalyst setup to check again.";
|
|
281
|
+
return { screen, mine, theirs, skipped, waiting, next, text, live };
|
|
282
|
+
};
|
|
283
|
+
const summary = (journal) => {
|
|
284
|
+
const sorted = sortActions(journal);
|
|
285
|
+
const { screen, mine, theirs, skipped } = sorted;
|
|
226
286
|
if (!renderer)
|
|
227
|
-
return
|
|
287
|
+
return sorted;
|
|
228
288
|
renderer.suspendLive();
|
|
229
289
|
active = false;
|
|
230
290
|
renderer.heading(screen.heading);
|
|
@@ -233,12 +293,34 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
233
293
|
renderer.tracker(setupTrackerRows(setupPartProgress(journal, planned.scope), "end"));
|
|
234
294
|
if (screen.readinessCause)
|
|
235
295
|
renderer.line(say(screen.readinessCause));
|
|
236
|
-
if (
|
|
237
|
-
renderer.line(`${
|
|
238
|
-
|
|
296
|
+
if (mine.length) {
|
|
297
|
+
renderer.line(`${mine.length} ${mine.length === 1 ? "thing needs" : "things need"} you:`);
|
|
298
|
+
mine.forEach((action, i) => renderer.item(`${i + 1}. `, sorted.text(action)));
|
|
239
299
|
if (screen.more)
|
|
240
300
|
renderer.line(`and ${screen.more} more after these`);
|
|
241
|
-
|
|
301
|
+
}
|
|
302
|
+
if (sorted.waiting) {
|
|
303
|
+
renderer.line("Waiting on an owner or admin of your Catalyst workspace:");
|
|
304
|
+
for (const action of theirs)
|
|
305
|
+
renderer.item("· ", sorted.text(action));
|
|
306
|
+
if (skipped.length) {
|
|
307
|
+
renderer.item("· ", `Steps only they can do: ${skipped.map((s) => title(s.id)).join(", ")}.`);
|
|
308
|
+
const base = deps.baseUrl?.();
|
|
309
|
+
const integrations = base
|
|
310
|
+
? new URL("/settings/connections", base).href
|
|
311
|
+
: undefined;
|
|
312
|
+
if (integrations && !theirs.some((a) => a.text.includes(integrations)))
|
|
313
|
+
renderer.item("· ", `They start at ${integrations}, the Integrations page.`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// A step skipped with Ctrl-C is no action, but it is still to come: the line below names it.
|
|
317
|
+
if (screen.actions.length || skipped.length) {
|
|
318
|
+
const continuation = pendingContinuation(journal,
|
|
319
|
+
// A step paused with Ctrl-C is still to come, so it stays in this line.
|
|
320
|
+
new Set([
|
|
321
|
+
...sorted.live.map((action) => action.id),
|
|
322
|
+
...skipped.map((s) => s.id),
|
|
323
|
+
]));
|
|
242
324
|
if (continuation)
|
|
243
325
|
renderer.line(continuation);
|
|
244
326
|
}
|
|
@@ -246,7 +328,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
246
328
|
renderer.line("Catalyst is ready to work on your team's tickets.");
|
|
247
329
|
if (deps.logPath?.())
|
|
248
330
|
renderer.line(`Full log: ${deps.logPath()}`);
|
|
249
|
-
return
|
|
331
|
+
return sorted;
|
|
250
332
|
};
|
|
251
333
|
const ui = {
|
|
252
334
|
signal: abort.signal,
|
|
@@ -264,14 +346,16 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
264
346
|
machineChecks.clear();
|
|
265
347
|
machineBlocked = false;
|
|
266
348
|
stop();
|
|
349
|
+
// Signed in already, the header carries who and where, so the lines below would repeat it.
|
|
350
|
+
let identityShown = false;
|
|
267
351
|
if (!introduced) {
|
|
268
352
|
if (renderer)
|
|
269
|
-
renderer.brand("setup", deps.version);
|
|
353
|
+
identityShown = renderer.brand("setup", deps.version, identity ? onboardHeaderIdentity(identity) : undefined);
|
|
270
354
|
else
|
|
271
355
|
prompts.intro("Catalyst setup", { output: streams.output });
|
|
272
356
|
introduced = true;
|
|
273
357
|
}
|
|
274
|
-
if (identity !== undefined)
|
|
358
|
+
if (identity !== undefined && !identityShown)
|
|
275
359
|
for (const line of onboardIdentityLines(identity))
|
|
276
360
|
if (renderer)
|
|
277
361
|
renderer.line(line);
|
|
@@ -465,16 +549,35 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
465
549
|
...(prompts.multiselect
|
|
466
550
|
? {
|
|
467
551
|
async chooseRepositories(repositories) {
|
|
552
|
+
const projectsPage = () => {
|
|
553
|
+
try {
|
|
554
|
+
const base = deps.baseUrl?.();
|
|
555
|
+
return base ? ` (${new URL("/settings/projects", base).href})` : "";
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return "";
|
|
559
|
+
}
|
|
560
|
+
};
|
|
468
561
|
stop();
|
|
469
562
|
if (!repositories.length || abort.signal.aborted)
|
|
470
563
|
return null;
|
|
564
|
+
// CTC-4742: what the team already uses starts selected. Unselecting never removes one.
|
|
565
|
+
const selected = repositories
|
|
566
|
+
.filter((row) => row.registered === true)
|
|
567
|
+
.map((row) => `${row.owner}/${row.name}`);
|
|
471
568
|
const answer = await multiselect({
|
|
472
569
|
...options,
|
|
473
|
-
message:
|
|
570
|
+
message: selected.length
|
|
571
|
+
? `Which repositories should use this Linear team? Leaving a repository unselected does not remove it from this team; an admin removes one in Settings → Projects${projectsPage()}.`
|
|
572
|
+
: "Which repositories should use this Linear team?",
|
|
474
573
|
required: true,
|
|
574
|
+
...(selected.length ? { initialValues: selected } : {}),
|
|
475
575
|
options: repositories.map((row) => ({
|
|
476
576
|
value: `${row.owner}/${row.name}`,
|
|
477
577
|
label: `${row.owner}/${row.name}`,
|
|
578
|
+
...(row.registered === false
|
|
579
|
+
? { hint: "not used by this team yet; selecting it adds it" }
|
|
580
|
+
: {}),
|
|
478
581
|
})),
|
|
479
582
|
});
|
|
480
583
|
if (prompts.isCancel(answer)) {
|
|
@@ -558,9 +661,21 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
558
661
|
return typeof answer === "string" ? answer : null;
|
|
559
662
|
},
|
|
560
663
|
async chooseRunner() {
|
|
561
|
-
stop();
|
|
562
664
|
if (abort.signal.aborted)
|
|
563
665
|
return null;
|
|
666
|
+
// CTC-4739: say what each answer does to this computer before asking, and how to skip it.
|
|
667
|
+
stop();
|
|
668
|
+
if (renderer) {
|
|
669
|
+
renderer.begin(number("runner"), title("runner"), "", "ask");
|
|
670
|
+
for (const line of RUNNER_EXPLAINED)
|
|
671
|
+
renderer.detail(line);
|
|
672
|
+
active = true;
|
|
673
|
+
questionOpen = true;
|
|
674
|
+
}
|
|
675
|
+
else
|
|
676
|
+
prompts.log.message(RUNNER_EXPLAINED.join("\n"), {
|
|
677
|
+
output: streams.output,
|
|
678
|
+
});
|
|
564
679
|
const answer = await select({
|
|
565
680
|
...options,
|
|
566
681
|
message: "Run Catalyst's work on this machine?",
|
|
@@ -586,25 +701,35 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
586
701
|
async retryTimedOut(id) {
|
|
587
702
|
if (abort.signal.aborted)
|
|
588
703
|
return false;
|
|
704
|
+
// The Connect accounts page does not expire: setup stopped waiting, and can wait again.
|
|
705
|
+
const page = connectWaits.has(id);
|
|
706
|
+
const why = page
|
|
707
|
+
? "Setup stopped waiting for the Connect accounts page."
|
|
708
|
+
: "The link timed out.";
|
|
589
709
|
if (renderer) {
|
|
590
710
|
// The question draws under this row, so the reason stays on screen while it is asked.
|
|
591
711
|
stop();
|
|
592
|
-
renderer.begin(number(id), title(id), "the link timed out", "ask");
|
|
712
|
+
renderer.begin(number(id), title(id), page ? "setup stopped waiting" : "the link timed out", "ask");
|
|
713
|
+
if (page)
|
|
714
|
+
renderer.detail(why);
|
|
593
715
|
active = true;
|
|
594
716
|
questionOpen = true;
|
|
595
717
|
}
|
|
596
718
|
else {
|
|
597
719
|
stop();
|
|
598
|
-
prompts.log.message(`${title(id)}:
|
|
720
|
+
prompts.log.message(`${title(id)}: ${why}`, {
|
|
599
721
|
output: streams.output,
|
|
600
722
|
});
|
|
601
723
|
}
|
|
602
724
|
const answer = await select({
|
|
603
725
|
...options,
|
|
604
|
-
message: "Ready to try again?",
|
|
726
|
+
message: page ? "Keep waiting?" : "Ready to try again?",
|
|
605
727
|
initialValue: "again",
|
|
606
728
|
options: [
|
|
607
|
-
{
|
|
729
|
+
{
|
|
730
|
+
value: "again",
|
|
731
|
+
label: page ? "Yes, keep waiting" : "Yes, give me a new link",
|
|
732
|
+
},
|
|
608
733
|
{ value: "stop", label: "Stop here. Run catalyst setup later to carry on." },
|
|
609
734
|
],
|
|
610
735
|
});
|
|
@@ -716,7 +841,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
716
841
|
}
|
|
717
842
|
if (step.id === "ready" || (hidden(step.id) && step.state !== "failed"))
|
|
718
843
|
return;
|
|
719
|
-
if (
|
|
844
|
+
if (step.id === "github.repos" &&
|
|
720
845
|
step.state === "done")
|
|
721
846
|
return;
|
|
722
847
|
if (step.id === "projects" &&
|
|
@@ -725,7 +850,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
725
850
|
const view = setupStepView(step, journal);
|
|
726
851
|
if (hidden(step.id))
|
|
727
852
|
enterPart(step.id);
|
|
728
|
-
renderer.step(view.mark, view.number, view.title, view
|
|
853
|
+
renderer.step(view.mark, view.number, view.title, outcome(step, view));
|
|
729
854
|
const granted = deps.verbose ? onboardStepDetail(step) : undefined;
|
|
730
855
|
if (granted)
|
|
731
856
|
renderer.detail(granted);
|
|
@@ -772,7 +897,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
772
897
|
if (!renderer ||
|
|
773
898
|
!interactive ||
|
|
774
899
|
abort.signal.aborted ||
|
|
775
|
-
!
|
|
900
|
+
!sortActions(journal).mine.length)
|
|
776
901
|
return false;
|
|
777
902
|
active = false;
|
|
778
903
|
summary(journal);
|
|
@@ -819,15 +944,14 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
819
944
|
}
|
|
820
945
|
if (renderer) {
|
|
821
946
|
active = false;
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
: summary(journal);
|
|
947
|
+
const sorted = summaryShown ? sortActions(journal) : summary(journal);
|
|
948
|
+
const { screen } = sorted;
|
|
825
949
|
if (summaryShown && screen.paused) {
|
|
826
950
|
renderer.heading(screen.heading);
|
|
827
951
|
renderer.line("Your progress is saved.");
|
|
828
952
|
}
|
|
829
953
|
summaryShown = false;
|
|
830
|
-
renderer.outro(
|
|
954
|
+
renderer.outro(sorted.next);
|
|
831
955
|
return;
|
|
832
956
|
}
|
|
833
957
|
stop();
|
|
@@ -852,6 +976,19 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
852
976
|
prompts.outro(text, { output: streams.output });
|
|
853
977
|
},
|
|
854
978
|
async wait(text, run, page) {
|
|
979
|
+
if (currentStep && page) {
|
|
980
|
+
let path = "";
|
|
981
|
+
try {
|
|
982
|
+
path = new URL(page.url).pathname;
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
// Not a URL, so not the Connect accounts page.
|
|
986
|
+
}
|
|
987
|
+
if (path === CONNECT_PAGE_PATH)
|
|
988
|
+
connectWaits.set(currentStep, page.url);
|
|
989
|
+
else
|
|
990
|
+
connectWaits.delete(currentStep);
|
|
991
|
+
}
|
|
855
992
|
const browser = currentStep && renderer && page
|
|
856
993
|
? page
|
|
857
994
|
: currentStep &&
|
package/dist/onboard.js
CHANGED
|
@@ -45,16 +45,25 @@ function dailyUpdateChoice(value) {
|
|
|
45
45
|
return { state: "skipped", reason: "no_scheduler" };
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
+
const identityLabel = (text) => text
|
|
49
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ")
|
|
50
|
+
.replace(/\s+/g, " ")
|
|
51
|
+
.trim()
|
|
52
|
+
.slice(0, 180);
|
|
53
|
+
/** CTC-4680: who and where for the lines beside setup's mark, "user: …" and "workspace: …". */
|
|
54
|
+
export function onboardHeaderIdentity(identity) {
|
|
55
|
+
const display = identity.display;
|
|
56
|
+
return {
|
|
57
|
+
user: identityLabel(display?.email || display?.personLabel || identity.membershipId),
|
|
58
|
+
workspace: identityLabel(display?.workspaceName || identity.account),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
48
61
|
export function onboardIdentityLines(identity) {
|
|
49
62
|
if (!identity)
|
|
50
63
|
return [
|
|
51
64
|
"Use an existing Catalyst account or accept your invitation before approving sign-in.",
|
|
52
65
|
];
|
|
53
|
-
const label =
|
|
54
|
-
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ")
|
|
55
|
-
.replace(/\s+/g, " ")
|
|
56
|
-
.trim()
|
|
57
|
-
.slice(0, 180);
|
|
66
|
+
const label = identityLabel;
|
|
58
67
|
const display = identity.display;
|
|
59
68
|
const person = label(display?.personLabel || identity.membershipId);
|
|
60
69
|
const email = display?.email ? label(display.email) : "";
|
|
@@ -591,7 +600,7 @@ function printPlan(ctx, journal, identity, options) {
|
|
|
591
600
|
ctx.stdout(line);
|
|
592
601
|
let group;
|
|
593
602
|
// Beside --json and headless output: the numbers JSON actions carry (CTC-4680).
|
|
594
|
-
for (const row of standalonePlan(journal, options?.scope, options?.runner, "json")) {
|
|
603
|
+
for (const row of standalonePlan(journal, options?.scope, options?.runner, "json", options?.runnerDeclinedBy)) {
|
|
595
604
|
if (row.group !== group) {
|
|
596
605
|
ctx.stdout("");
|
|
597
606
|
ctx.stdout(row.group);
|
|
@@ -788,7 +797,15 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
788
797
|
for (const id of ONBOARD_STEPS)
|
|
789
798
|
reviewedScope.add(id);
|
|
790
799
|
const planOptions = (journal) => ({
|
|
791
|
-
runner: args.flags.runner === true
|
|
800
|
+
runner: args.flags.runner === true
|
|
801
|
+
? true
|
|
802
|
+
: args.flags["no-runner"] === true
|
|
803
|
+
? false
|
|
804
|
+
: undefined,
|
|
805
|
+
// Headless turns --runner no and CATALYST_ONBOARD_RUNNER=no into --no-runner; say which.
|
|
806
|
+
runnerDeclinedBy: args.flags.headless === true
|
|
807
|
+
? "the headless runner input is no"
|
|
808
|
+
: "--no-runner was passed",
|
|
792
809
|
localSync: args.flags["local-sync"] === true || journal.localSync === true,
|
|
793
810
|
scope: ONBOARD_STEPS.filter((id) => reviewedScope.has(id)),
|
|
794
811
|
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Ring order runs from the top tip, round the back, to the bottom tip, as in the brand script. */
|
|
2
|
+
export const RING = [
|
|
3
|
+
[3, 0],
|
|
4
|
+
[2, 0],
|
|
5
|
+
[1, 0],
|
|
6
|
+
[0, 1],
|
|
7
|
+
[0, 2],
|
|
8
|
+
[1, 3],
|
|
9
|
+
[2, 3],
|
|
10
|
+
[3, 3],
|
|
11
|
+
];
|
|
12
|
+
export const CORE = [
|
|
13
|
+
[1, 1],
|
|
14
|
+
[2, 1],
|
|
15
|
+
[1, 2],
|
|
16
|
+
[2, 2],
|
|
17
|
+
];
|
|
18
|
+
export const MARK_SIZE = { dot: 3, gap: 1 };
|
|
19
|
+
export const markSpan = (size = MARK_SIZE) => 4 * size.dot + 3 * size.gap;
|
|
20
|
+
export const markLines = (size = MARK_SIZE) => Math.ceil(markSpan(size) / 2);
|
|
21
|
+
const paintOf = (col, row) => RING.some(([c, r]) => c === col && r === row)
|
|
22
|
+
? "ring"
|
|
23
|
+
: CORE.some(([c, r]) => c === col && r === row)
|
|
24
|
+
? "core"
|
|
25
|
+
: null;
|
|
26
|
+
/** The mark as a grid of square units, `markSpan` rows of half lines by `markSpan` columns. */
|
|
27
|
+
export function markUnits(size = MARK_SIZE) {
|
|
28
|
+
const span = markSpan(size);
|
|
29
|
+
const pitch = size.dot + size.gap;
|
|
30
|
+
return Array.from({ length: span }, (_, y) => Array.from({ length: span }, (_, x) => x % pitch >= size.dot || y % pitch >= size.dot
|
|
31
|
+
? null
|
|
32
|
+
: paintOf(Math.floor(x / pitch), Math.floor(y / pitch))));
|
|
33
|
+
}
|
|
34
|
+
const BRAND = {
|
|
35
|
+
ink: "#11100e",
|
|
36
|
+
cream: "#f1ebe2",
|
|
37
|
+
copper: "#d28e63",
|
|
38
|
+
rust: "#a9512f",
|
|
39
|
+
};
|
|
40
|
+
/** The brand rules: a cream C and copper core on dark, an ink C and rust core on light. */
|
|
41
|
+
const PALETTES = {
|
|
42
|
+
dark: { ring: BRAND.cream, core: BRAND.copper },
|
|
43
|
+
light: { ring: BRAND.ink, core: BRAND.rust },
|
|
44
|
+
};
|
|
45
|
+
export function colourLevel(env, terminal) {
|
|
46
|
+
if (terminal.noColor)
|
|
47
|
+
return "none";
|
|
48
|
+
const force = env.FORCE_COLOR;
|
|
49
|
+
if (force !== undefined) {
|
|
50
|
+
if (force === "0" || force === "false")
|
|
51
|
+
return "none";
|
|
52
|
+
if (force === "" || force === "1" || force === "true")
|
|
53
|
+
return "16";
|
|
54
|
+
if (force === "2")
|
|
55
|
+
return "256";
|
|
56
|
+
if (force === "3")
|
|
57
|
+
return "truecolor";
|
|
58
|
+
}
|
|
59
|
+
if (env.NO_COLOR !== undefined || !terminal.tty)
|
|
60
|
+
return "none";
|
|
61
|
+
if (!env.TERM || env.TERM === "dumb")
|
|
62
|
+
return "none";
|
|
63
|
+
if (/^(truecolor|24bit)$/i.test(env.COLORTERM ?? ""))
|
|
64
|
+
return "truecolor";
|
|
65
|
+
return /256/.test(env.TERM) ? "256" : "16";
|
|
66
|
+
}
|
|
67
|
+
/** CI as most tools read it: set, and not "", "0" or "false". */
|
|
68
|
+
export function inCi(env) {
|
|
69
|
+
return ![undefined, "", "0", "false"].includes(env.CI);
|
|
70
|
+
}
|
|
71
|
+
export const MARK_MIN_COLUMNS = 60;
|
|
72
|
+
/**
|
|
73
|
+
* Why the mark is left off, or null to draw it. Off for every verb but setup, without a terminal,
|
|
74
|
+
* in CI, with --no-color or NO_COLOR or no colour at all, without UTF-8 (the blocks are UTF-8), and
|
|
75
|
+
* below 60 columns. The text lines are printed either way.
|
|
76
|
+
*/
|
|
77
|
+
export function markSkipped(options) {
|
|
78
|
+
const { env } = options;
|
|
79
|
+
if (!options.setup)
|
|
80
|
+
return "not catalyst setup";
|
|
81
|
+
if (!options.tty)
|
|
82
|
+
return "not a terminal";
|
|
83
|
+
if (inCi(env))
|
|
84
|
+
return "CI";
|
|
85
|
+
if (options.noColor)
|
|
86
|
+
return "--no-color";
|
|
87
|
+
if (env.NO_COLOR !== undefined)
|
|
88
|
+
return "NO_COLOR";
|
|
89
|
+
if (colourLevel(env, options) === "none")
|
|
90
|
+
return "no colour";
|
|
91
|
+
if (!/utf-?8/i.test(env.LC_ALL || env.LC_CTYPE || env.LANG || ""))
|
|
92
|
+
return "not UTF-8";
|
|
93
|
+
if (options.columns < MARK_MIN_COLUMNS)
|
|
94
|
+
return "narrow";
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const rgb = (hex) => [
|
|
98
|
+
Number.parseInt(hex.slice(1, 3), 16),
|
|
99
|
+
Number.parseInt(hex.slice(3, 5), 16),
|
|
100
|
+
Number.parseInt(hex.slice(5, 7), 16),
|
|
101
|
+
];
|
|
102
|
+
const distance = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;
|
|
103
|
+
/** The nearest xterm-256 colour: the 6×6×6 cube or the 24-step grey ramp, whichever is closer. */
|
|
104
|
+
export function nearest256(hex) {
|
|
105
|
+
const colour = rgb(hex);
|
|
106
|
+
const levels = [0, 95, 135, 175, 215, 255];
|
|
107
|
+
const step = (v) => levels.reduce((best, l, i) => (Math.abs(l - v) < Math.abs(levels[best] - v) ? i : best), 0);
|
|
108
|
+
const [ri, gi, bi] = colour.map(step);
|
|
109
|
+
const avg = (colour[0] + colour[1] + colour[2]) / 3;
|
|
110
|
+
const greyIndex = Math.min(23, Math.max(0, Math.round((avg - 8) / 10)));
|
|
111
|
+
const grey = 8 + 10 * greyIndex;
|
|
112
|
+
return distance([levels[ri], levels[gi], levels[bi]], colour) <=
|
|
113
|
+
distance([grey, grey, grey], colour)
|
|
114
|
+
? 16 + 36 * ri + 6 * gi + bi
|
|
115
|
+
: 232 + greyIndex;
|
|
116
|
+
}
|
|
117
|
+
/** xterm's default 16 colours, as SGR foreground codes. */
|
|
118
|
+
const XTERM16 = [
|
|
119
|
+
[30, [0, 0, 0]],
|
|
120
|
+
[31, [205, 0, 0]],
|
|
121
|
+
[32, [0, 205, 0]],
|
|
122
|
+
[33, [205, 205, 0]],
|
|
123
|
+
[34, [0, 0, 238]],
|
|
124
|
+
[35, [205, 0, 205]],
|
|
125
|
+
[36, [0, 205, 205]],
|
|
126
|
+
[37, [229, 229, 229]],
|
|
127
|
+
[90, [127, 127, 127]],
|
|
128
|
+
[91, [255, 0, 0]],
|
|
129
|
+
[92, [0, 255, 0]],
|
|
130
|
+
[93, [255, 255, 0]],
|
|
131
|
+
[94, [92, 92, 255]],
|
|
132
|
+
[95, [255, 0, 255]],
|
|
133
|
+
[96, [0, 255, 255]],
|
|
134
|
+
[97, [255, 255, 255]],
|
|
135
|
+
];
|
|
136
|
+
/**
|
|
137
|
+
* The nearest of xterm's 16 default colours. Plain distance puts copper nearest grey (90), and the
|
|
138
|
+
* core is the mark's one accent, so the core picks from the coloured entries only: copper is then
|
|
139
|
+
* yellow (33) and rust red (31). The ring takes plain distance: cream is white (37), ink black (30).
|
|
140
|
+
*/
|
|
141
|
+
export function nearest16(hex, accent = false) {
|
|
142
|
+
const colour = rgb(hex);
|
|
143
|
+
const greys = [30, 37, 90, 97];
|
|
144
|
+
return XTERM16.filter(([code]) => !accent || !greys.includes(code)).reduce((best, entry) => distance(entry[1], colour) < distance(best[1], colour) ? entry : best)[0];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The SGR parameters for one of the mark's colours on a `theme` background, as a foreground or a
|
|
148
|
+
* background. With 16 colours the core is also bold, which most terminals draw brighter.
|
|
149
|
+
*/
|
|
150
|
+
export function markColour(paint, theme, level, layer = "fg") {
|
|
151
|
+
const hex = PALETTES[theme][paint];
|
|
152
|
+
const lead = layer === "fg" ? 38 : 48;
|
|
153
|
+
if (level === "truecolor")
|
|
154
|
+
return `${lead};2;${rgb(hex).join(";")}`;
|
|
155
|
+
if (level === "256")
|
|
156
|
+
return `${lead};5;${nearest256(hex)}`;
|
|
157
|
+
const code = nearest16(hex, paint === "core") + (layer === "fg" ? 0 : 10);
|
|
158
|
+
return paint === "core" && layer === "fg" ? `1;${code}` : String(code);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Units to terminal lines, two unit rows per line: both halves set is a full block (or an upper
|
|
162
|
+
* half block on the lower colour's background when they differ), one half is a half block, neither
|
|
163
|
+
* is a space. Each run of one glyph colour is closed with a reset.
|
|
164
|
+
*/
|
|
165
|
+
export function unitLines(units, theme, level) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
for (let y = 0; y < units.length; y += 2) {
|
|
168
|
+
const upper = units[y];
|
|
169
|
+
const lower = units[y + 1] ?? [];
|
|
170
|
+
let out = "";
|
|
171
|
+
let open = "";
|
|
172
|
+
for (let x = 0; x < upper.length; x++) {
|
|
173
|
+
const [top, bottom] = [upper[x] ?? null, lower[x] ?? null];
|
|
174
|
+
const sgr = top && bottom && top !== bottom
|
|
175
|
+
? `${markColour(top, theme, level)};${markColour(bottom, theme, level, "bg")}`
|
|
176
|
+
: top || bottom
|
|
177
|
+
? markColour((top ?? bottom), theme, level)
|
|
178
|
+
: "";
|
|
179
|
+
if (sgr !== open) {
|
|
180
|
+
if (open)
|
|
181
|
+
out += "\u001b[0m";
|
|
182
|
+
if (sgr)
|
|
183
|
+
out += `\u001b[${sgr}m`;
|
|
184
|
+
open = sgr;
|
|
185
|
+
}
|
|
186
|
+
out +=
|
|
187
|
+
!top && !bottom
|
|
188
|
+
? " "
|
|
189
|
+
: top && bottom && top === bottom
|
|
190
|
+
? "█"
|
|
191
|
+
: top
|
|
192
|
+
? "▀"
|
|
193
|
+
: "▄";
|
|
194
|
+
}
|
|
195
|
+
lines.push(open ? `${out}\u001b[0m` : out);
|
|
196
|
+
}
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
199
|
+
/** The mark, one string per terminal line: `markLines` lines of `markSpan` columns. */
|
|
200
|
+
export function pixelNucleusLines(theme, level, size = MARK_SIZE) {
|
|
201
|
+
return unitLines(markUnits(size), theme, level);
|
|
202
|
+
}
|