@catalyst-cloud/cli 0.15.2 → 0.15.4
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 +13 -0
- package/README.md +4 -2
- package/dist/args.js +5 -1
- package/dist/cli.js +44 -20
- package/dist/oauth.js +1 -1
- package/dist/onboard-accounts.js +67 -171
- package/dist/onboard-checklist.js +310 -0
- package/dist/onboard-existing.js +1 -1
- package/dist/onboard-first-ticket.js +199 -0
- package/dist/onboard-headless.js +2 -2
- package/dist/onboard-housekeeping.js +93 -0
- package/dist/onboard-next.js +108 -9
- package/dist/onboard-ready.js +61 -0
- package/dist/onboard-runtime.js +60 -27
- package/dist/onboard-standalone-copy.js +73 -66
- package/dist/onboard-ui.js +278 -105
- package/dist/onboard-values.js +47 -0
- package/dist/onboard.js +63 -18
- package/dist/ready-copy.js +87 -0
- package/dist/ready.js +1 -57
- package/dist/setup-brand.js +202 -0
- package/dist/setup-onboard-copy.js +314 -42
- package/dist/setup-render.js +99 -17
- package/dist/setup.js +6 -3
- 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 +3 -3
- package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
- package/skills/run-this-project/SKILL.md +2 -2
- 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 +2 -2
- package/skills/whats-happening/references/coding-accounts.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { loadContract } from "./contract.js";
|
|
3
|
+
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
4
|
+
import { valuesFacts } from "./ready-copy.js";
|
|
5
|
+
const waiting = (reason, evidence) => ({
|
|
6
|
+
state: "waiting",
|
|
7
|
+
reason,
|
|
8
|
+
...(evidence ? { evidence } : {}),
|
|
9
|
+
});
|
|
10
|
+
/** The selected team's `required_values` readiness check decides this step. It passes when the
|
|
11
|
+
* selected repositories declare no required value, or every one has a value. Nothing here reads,
|
|
12
|
+
* imports or prints a value: a missing one is named, with the page where it is set. A stale, absent
|
|
13
|
+
* or unknown check waits, so the step never claims values it could not see. */
|
|
14
|
+
export function onboardValuesAdapter() {
|
|
15
|
+
return {
|
|
16
|
+
check: async (ctx, journal, signal) => {
|
|
17
|
+
const cfg = loadConfig(ctx.home);
|
|
18
|
+
const teamId = selectedOnboardTeam(journal);
|
|
19
|
+
if (!cfg || !teamId)
|
|
20
|
+
return waiting("required_values_unverified");
|
|
21
|
+
let loaded;
|
|
22
|
+
try {
|
|
23
|
+
loaded = await loadContract(ctx, cfg, { refresh: true, signal });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return waiting(signal?.aborted ? "interrupted" : "required_values_unverified");
|
|
27
|
+
}
|
|
28
|
+
if (loaded.source === "cache")
|
|
29
|
+
return waiting("required_values_unverified");
|
|
30
|
+
const readiness = loaded.doc.teams.find((team) => team.id === teamId)?.readiness;
|
|
31
|
+
const fresh = readiness?.checkedAt != null &&
|
|
32
|
+
readiness.expiresAt != null &&
|
|
33
|
+
readiness.expiresAt > ctx.now().getTime();
|
|
34
|
+
const check = fresh
|
|
35
|
+
? readiness.checks.find((row) => row.id === "required_values")
|
|
36
|
+
: undefined;
|
|
37
|
+
if (check?.state === "pass")
|
|
38
|
+
return { state: "done", evidence: { team: teamId } };
|
|
39
|
+
if (check?.state !== "fail")
|
|
40
|
+
return waiting("required_values_unverified");
|
|
41
|
+
return waiting("required_values_missing", {
|
|
42
|
+
team: teamId,
|
|
43
|
+
requiredValues: JSON.stringify(valuesFacts(check)),
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/onboard.js
CHANGED
|
@@ -10,7 +10,7 @@ import { onboardJsonView, TIMED_OUT_REASONS } from "./setup-onboard-copy.js";
|
|
|
10
10
|
import { standalonePlan, standalonePlanNotes, } from "./onboard-standalone-copy.js";
|
|
11
11
|
import { CliError, UsageError } from "./errors.js";
|
|
12
12
|
import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
|
|
13
|
-
import { onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
|
|
13
|
+
import { dispatchStageName, onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
|
|
14
14
|
import { parseBootstrapPlan, bootstrapPlanHash, bootstrapPlanLines, } from "./onboard-bootstrap.js";
|
|
15
15
|
export const ONBOARD_STEPS = [
|
|
16
16
|
"machine",
|
|
@@ -37,16 +37,33 @@ export const ONBOARD_STEPS = [
|
|
|
37
37
|
"first-ticket",
|
|
38
38
|
"ready",
|
|
39
39
|
];
|
|
40
|
+
function dailyUpdateChoice(value) {
|
|
41
|
+
const row = object(value);
|
|
42
|
+
if (row?.state === "on" || row?.state === "off")
|
|
43
|
+
return { state: row.state };
|
|
44
|
+
if (row?.state === "skipped" && row.reason === "no_scheduler")
|
|
45
|
+
return { state: "skipped", reason: "no_scheduler" };
|
|
46
|
+
return undefined;
|
|
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
|
+
}
|
|
40
61
|
export function onboardIdentityLines(identity) {
|
|
41
62
|
if (!identity)
|
|
42
63
|
return [
|
|
43
64
|
"Use an existing Catalyst account or accept your invitation before approving sign-in.",
|
|
44
65
|
];
|
|
45
|
-
const label =
|
|
46
|
-
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ")
|
|
47
|
-
.replace(/\s+/g, " ")
|
|
48
|
-
.trim()
|
|
49
|
-
.slice(0, 180);
|
|
66
|
+
const label = identityLabel;
|
|
50
67
|
const display = identity.display;
|
|
51
68
|
const person = label(display?.personLabel || identity.membershipId);
|
|
52
69
|
const email = display?.email ? label(display.email) : "";
|
|
@@ -104,7 +121,8 @@ export const ONBOARD_DEPENDENCIES = {
|
|
|
104
121
|
projects: ["linear.team", "github.repos"],
|
|
105
122
|
accounts: ["signin"],
|
|
106
123
|
settings: ["projects"],
|
|
107
|
-
values
|
|
124
|
+
// The cloud's required-values check reads the registered repositories, not a local checkout.
|
|
125
|
+
values: ["projects"],
|
|
108
126
|
capacity: ["projects"],
|
|
109
127
|
// The join token names the selected team's pool.
|
|
110
128
|
runner: ["linear.team"],
|
|
@@ -127,7 +145,7 @@ const ADMIN_STEPS = new Set([
|
|
|
127
145
|
"github.repos",
|
|
128
146
|
"projects",
|
|
129
147
|
"settings",
|
|
130
|
-
|
|
148
|
+
// `values` is not here: its check only reads, so a member's run finds missing values too.
|
|
131
149
|
"capacity",
|
|
132
150
|
"runner",
|
|
133
151
|
"first-ticket",
|
|
@@ -147,9 +165,9 @@ export const ONBOARD_TITLES = {
|
|
|
147
165
|
"github.personal": "Connect your GitHub account",
|
|
148
166
|
"github.repos": "Choose repositories",
|
|
149
167
|
projects: "Register your projects",
|
|
150
|
-
accounts: "Check
|
|
168
|
+
accounts: "Check AI accounts",
|
|
151
169
|
settings: "Review repository settings",
|
|
152
|
-
values: "
|
|
170
|
+
values: "Check the repository's values",
|
|
153
171
|
capacity: "Check runner capacity",
|
|
154
172
|
runner: "Run Catalyst's work on this machine",
|
|
155
173
|
daemon: "Check optional local sync",
|
|
@@ -251,6 +269,10 @@ function normalizeStep(value, fallbackAt) {
|
|
|
251
269
|
"repository",
|
|
252
270
|
"installation",
|
|
253
271
|
"ticket",
|
|
272
|
+
// The team's dispatch stage name, as the contract gave it.
|
|
273
|
+
"stage",
|
|
274
|
+
// Admin-owned step ids a member's run found unfinished.
|
|
275
|
+
"admin",
|
|
254
276
|
"cursor",
|
|
255
277
|
"lag",
|
|
256
278
|
"heartbeatAgeMs",
|
|
@@ -288,6 +310,8 @@ function normalizeStep(value, fallbackAt) {
|
|
|
288
310
|
"org",
|
|
289
311
|
"url",
|
|
290
312
|
"actor",
|
|
313
|
+
// CTC-4716: which AI accounts the workspace may add, so the step's text offers only those.
|
|
314
|
+
"aiAccountKinds",
|
|
291
315
|
]) {
|
|
292
316
|
const item = evidence?.[key];
|
|
293
317
|
if (item === null ||
|
|
@@ -323,6 +347,7 @@ export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()
|
|
|
323
347
|
: typeof value.exitCode === "number"
|
|
324
348
|
? value.exitCode
|
|
325
349
|
: null;
|
|
350
|
+
const dailyUpdate = dailyUpdateChoice(value.dailyUpdate);
|
|
326
351
|
return {
|
|
327
352
|
schema: 1,
|
|
328
353
|
runId: typeof value.runId === "string" ? value.runId : randomUUID(),
|
|
@@ -354,6 +379,7 @@ export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()
|
|
|
354
379
|
...(typeof value.localSync === "boolean"
|
|
355
380
|
? { localSync: value.localSync }
|
|
356
381
|
: {}),
|
|
382
|
+
...(dailyUpdate ? { dailyUpdate } : {}),
|
|
357
383
|
operations: Object.fromEntries(Object.entries(object(value.operations) ?? {}).filter(([key, v]) => ONBOARD_STEPS.includes(key) &&
|
|
358
384
|
typeof v === "string" &&
|
|
359
385
|
/^[a-zA-Z0-9_.:-]{1,160}$/.test(v))),
|
|
@@ -573,7 +599,8 @@ function printPlan(ctx, journal, identity, options) {
|
|
|
573
599
|
for (const line of onboardIdentityLines(identity))
|
|
574
600
|
ctx.stdout(line);
|
|
575
601
|
let group;
|
|
576
|
-
|
|
602
|
+
// Beside --json and headless output: the numbers JSON actions carry (CTC-4680).
|
|
603
|
+
for (const row of standalonePlan(journal, options?.scope, options?.runner, "json")) {
|
|
577
604
|
if (row.group !== group) {
|
|
578
605
|
ctx.stdout("");
|
|
579
606
|
ctx.stdout(row.group);
|
|
@@ -629,14 +656,28 @@ export function stepSatisfied(step) {
|
|
|
629
656
|
return (step?.state === "done" ||
|
|
630
657
|
(step?.state === "skipped" &&
|
|
631
658
|
(step.id === "legacy" ||
|
|
659
|
+
// Optional: a computer without the daily update still runs work.
|
|
660
|
+
step.id === "housekeeping" ||
|
|
632
661
|
step.reason === "member_scope" ||
|
|
633
662
|
(step.id === "daemon" && step.reason === "local_sync_not_selected") ||
|
|
634
663
|
(step.id === "runner" && step.reason === "runner_not_selected") ||
|
|
664
|
+
(step.id === "first-ticket" && step.reason === "first_ticket_skipped") ||
|
|
635
665
|
(step.id === "linear.automations" &&
|
|
636
666
|
step.reason === "automation_management_unavailable"))));
|
|
637
667
|
}
|
|
668
|
+
/** Optional work this computer or checkout cannot do. Work runs without it, so its wait never holds
|
|
669
|
+
* a complete setup. */
|
|
670
|
+
const OPTIONAL_WAITS = new Set([
|
|
671
|
+
"settings_checkout_unverified",
|
|
672
|
+
"settings_approval_unverified",
|
|
673
|
+
]);
|
|
674
|
+
/** Satisfied, or an optional wait. What a complete setup needs of every step. */
|
|
675
|
+
export function stepSettled(step) {
|
|
676
|
+
return (stepSatisfied(step) ||
|
|
677
|
+
(step?.state === "waiting" && OPTIONAL_WAITS.has(step.reason ?? "")));
|
|
678
|
+
}
|
|
638
679
|
/** Steps whose wait does not hold a full run's exit code: work runs without them. A failure still
|
|
639
|
-
* fails the run, and the receipt's `complete` still needs them
|
|
680
|
+
* fails the run, and the receipt's `complete` still needs them settled. Never used for gating. */
|
|
640
681
|
export const ONBOARD_DEFERRED_STEPS = new Set([
|
|
641
682
|
"settings",
|
|
642
683
|
"values",
|
|
@@ -677,8 +718,11 @@ export function onboardNextActions(journal, baseUrl, only) {
|
|
|
677
718
|
.filter((step) => (!only || step.id === only) && !stepSatisfied(step))
|
|
678
719
|
.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id))
|
|
679
720
|
.map((step) => `${ONBOARD_TITLES[step.id]}: ${onboardStepAction(journal, step, baseUrl)}`);
|
|
680
|
-
|
|
681
|
-
|
|
721
|
+
// A waiting first ticket already says to move a ticket, in its own line.
|
|
722
|
+
const firstTicket = journalStep(journal, "first-ticket");
|
|
723
|
+
if (["projects", "accounts", "linear.adopt", "capacity"].every((id) => journalStep(journal, id)?.state === "done") &&
|
|
724
|
+
firstTicket?.state !== "waiting")
|
|
725
|
+
lines.push(`Move a ticket in ${onboardTeamKey(journal) ?? "<TEAM KEY>"} to ${dispatchStageName(firstTicket?.evidence?.stage)}; \`catalyst explain <ticket>\` says why it is or is not starting.`);
|
|
682
726
|
return lines;
|
|
683
727
|
}
|
|
684
728
|
function requireMatchingIdentity(journal, identity) {
|
|
@@ -727,9 +771,10 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
727
771
|
const only = typeof args.flags.only === "string"
|
|
728
772
|
? args.flags.only
|
|
729
773
|
: undefined;
|
|
774
|
+
// A member's values check reads the selected team, so without --team it waits on the same skip.
|
|
730
775
|
const memberScopeSkips = (id) => ADMIN_STEPS.has(id) ||
|
|
731
|
-
(id === "linear.team" &&
|
|
732
|
-
only !==
|
|
776
|
+
((id === "linear.team" || id === "values") &&
|
|
777
|
+
only !== id &&
|
|
733
778
|
typeof args.flags.team !== "string");
|
|
734
779
|
const resume = typeof args.flags["resume-from"] === "string"
|
|
735
780
|
? args.flags["resume-from"]
|
|
@@ -1069,7 +1114,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1069
1114
|
journal.complete =
|
|
1070
1115
|
!only &&
|
|
1071
1116
|
code === 0 &&
|
|
1072
|
-
ONBOARD_STEPS.every((id) =>
|
|
1117
|
+
ONBOARD_STEPS.every((id) => stepSettled(journalStep(journal, id)));
|
|
1073
1118
|
writeOnboardJournal(statePath, journal);
|
|
1074
1119
|
if (args.json)
|
|
1075
1120
|
ctx.stdout(JSON.stringify(journal));
|
|
@@ -1442,7 +1487,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1442
1487
|
journal.complete =
|
|
1443
1488
|
!only &&
|
|
1444
1489
|
code === 0 &&
|
|
1445
|
-
ONBOARD_STEPS.every((id) =>
|
|
1490
|
+
ONBOARD_STEPS.every((id) => stepSettled(journalStep(journal, id)));
|
|
1446
1491
|
const unfinished = journal.steps.filter((step) => !stepSatisfied(step));
|
|
1447
1492
|
if (!only &&
|
|
1448
1493
|
!refused &&
|
package/dist/ready-copy.js
CHANGED
|
@@ -65,6 +65,93 @@ function slotList(slots) {
|
|
|
65
65
|
function origin(baseUrl) {
|
|
66
66
|
return baseUrl.replace(/\/+$/, "");
|
|
67
67
|
}
|
|
68
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
69
|
+
const REPOSITORY = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
|
|
70
|
+
const identifiers = (value) => Array.isArray(value)
|
|
71
|
+
? value.filter((name) => typeof name === "string" && NAME.test(name))
|
|
72
|
+
: [];
|
|
73
|
+
const unresolvedFacts = (value) => Array.isArray(value)
|
|
74
|
+
? value.flatMap((row) => typeof row?.name === "string" && NAME.test(row.name) && identifiers(row.references).length
|
|
75
|
+
? [{ name: row.name, references: identifiers(row.references) }]
|
|
76
|
+
: [])
|
|
77
|
+
: [];
|
|
78
|
+
/** The declared names a failing check carries, kept in the receipt for the fix line: which variables
|
|
79
|
+
* need a value, which refer to a secret with no value, and the same for the team's other
|
|
80
|
+
* repositories. Identifiers and repository names only. */
|
|
81
|
+
export function valuesFacts(value) {
|
|
82
|
+
const row = (value ?? {});
|
|
83
|
+
return {
|
|
84
|
+
names: identifiers(row.names),
|
|
85
|
+
unresolved: unresolvedFacts(row.unresolved),
|
|
86
|
+
repos: Array.isArray(row.repos)
|
|
87
|
+
? row.repos.flatMap((note) => typeof note?.repo === "string" && REPOSITORY.test(note.repo)
|
|
88
|
+
? [{ repo: note.repo, names: identifiers(note.names), unresolved: unresolvedFacts(note.unresolved) }]
|
|
89
|
+
: [])
|
|
90
|
+
: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Where the named variables get their values. Names only: nothing here reads or prints a value. */
|
|
94
|
+
function setValuesFix(baseUrl, names) {
|
|
95
|
+
return baseUrl
|
|
96
|
+
? `set ${names.join(", ")} on the repository's Environment page: open ${origin(baseUrl)}/settings/projects, then the project, then the repository`
|
|
97
|
+
: `set ${names.join(", ")} on the repository's Environment page in Catalyst's project settings`;
|
|
98
|
+
}
|
|
99
|
+
function nameList(v) {
|
|
100
|
+
return Array.isArray(v)
|
|
101
|
+
? v.filter((n) => typeof n === "string" && n.length > 0)
|
|
102
|
+
: [];
|
|
103
|
+
}
|
|
104
|
+
/** Entries whose name and references are both present; anything malformed is dropped, not guessed. */
|
|
105
|
+
function unresolvedList(v) {
|
|
106
|
+
if (!Array.isArray(v))
|
|
107
|
+
return [];
|
|
108
|
+
return v.flatMap((u) => {
|
|
109
|
+
if (typeof u !== "object" || u === null)
|
|
110
|
+
return [];
|
|
111
|
+
const { name, references } = u;
|
|
112
|
+
const refs = nameList(references);
|
|
113
|
+
return typeof name === "string" && name.length > 0 && refs.length > 0
|
|
114
|
+
? [{ name, references: refs }]
|
|
115
|
+
: [];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function unresolvedLine(u) {
|
|
119
|
+
const which = u.references.length === 1 ? "which has" : "which have";
|
|
120
|
+
return `${u.name} refers to ${u.references.join(", ")}, ${which} no value, so Catalyst can't start work there`;
|
|
121
|
+
}
|
|
122
|
+
/** A team check's fix line. A check that carries `names` (CTC-3561: `required_values`, contract
|
|
123
|
+
* 1.24.0) names them and where to set them. CTC-3606: it also names each `unresolved` variable and
|
|
124
|
+
* the reference with no value, and each other repository's missing names from `repos[]`. `unresolved`
|
|
125
|
+
* is a subset of `names` (the variable exists; its reference does not), so those are not told to be
|
|
126
|
+
* "set". Every field is optional so an older cloud still works. Everything printed is a declared
|
|
127
|
+
* identifier or a repository name, never a value, and nothing here reads a value. Any other check
|
|
128
|
+
* keeps the generic line. Shared with setup's values step, which has no web address when the
|
|
129
|
+
* saved login has none. */
|
|
130
|
+
export function valuesFix(base, c) {
|
|
131
|
+
const unresolved = unresolvedList(c.unresolved);
|
|
132
|
+
const unresolvedNames = new Set(unresolved.map((u) => u.name));
|
|
133
|
+
const names = nameList(c.names).filter((n) => !unresolvedNames.has(n));
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (names.length > 0)
|
|
136
|
+
parts.push(setValuesFix(base, names));
|
|
137
|
+
for (const u of unresolved)
|
|
138
|
+
parts.push(unresolvedLine(u));
|
|
139
|
+
for (const note of Array.isArray(c.repos) ? c.repos : []) {
|
|
140
|
+
if (typeof note !== "object" ||
|
|
141
|
+
note === null ||
|
|
142
|
+
typeof note.repo !== "string" ||
|
|
143
|
+
note.repo.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
const repoUnresolved = unresolvedList(note.unresolved);
|
|
146
|
+
const skip = new Set(repoUnresolved.map((u) => u.name));
|
|
147
|
+
const missing = nameList(note.names).filter((n) => !skip.has(n));
|
|
148
|
+
if (missing.length > 0)
|
|
149
|
+
parts.push(`${note.repo} is missing ${missing.join(", ")}`);
|
|
150
|
+
for (const u of repoUnresolved)
|
|
151
|
+
parts.push(`in ${note.repo}, ${unresolvedLine(u)}`);
|
|
152
|
+
}
|
|
153
|
+
return parts.length === 0 ? null : parts.join(". ");
|
|
154
|
+
}
|
|
68
155
|
/** The team's own page in Catalyst settings, optionally one of its sub-pages (`map`, `adopt`). */
|
|
69
156
|
export function teamPage(baseUrl, key, sub) {
|
|
70
157
|
return `${origin(baseUrl)}/settings/linear-teams/${encodeURIComponent(key)}${sub ? `/${sub}` : ""}`;
|
package/dist/ready.js
CHANGED
|
@@ -15,7 +15,7 @@ import { FIRST_STAMPED_VERSION } from "./skill-shape.js";
|
|
|
15
15
|
import { installedBundleVersion } from "./skills.js";
|
|
16
16
|
import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
|
|
17
17
|
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
18
|
-
import { dispatchGateCopy, teamCheckCopy, teamPage, whoFixes } from "./ready-copy.js";
|
|
18
|
+
import { dispatchGateCopy, teamCheckCopy, teamPage, valuesFix, whoFixes } from "./ready-copy.js";
|
|
19
19
|
import { onboardStatePath, readOnboardJournal } from "./onboard.js";
|
|
20
20
|
export { semverOlder };
|
|
21
21
|
/** `ready`'s own wording for the replica. It never recommends adopting the replica — until the
|
|
@@ -112,62 +112,6 @@ export function catalystCommandCheck(env, platform = process.platform) {
|
|
|
112
112
|
"Keep using catalyst-skills on this machine, or put the npm global bin directory ahead of that one on PATH.",
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
-
function nameList(v) {
|
|
116
|
-
return Array.isArray(v)
|
|
117
|
-
? v.filter((n) => typeof n === "string" && n.length > 0)
|
|
118
|
-
: [];
|
|
119
|
-
}
|
|
120
|
-
/** Entries whose name and references are both present; anything malformed is dropped, not guessed. */
|
|
121
|
-
function unresolvedList(v) {
|
|
122
|
-
if (!Array.isArray(v))
|
|
123
|
-
return [];
|
|
124
|
-
return v.flatMap((u) => {
|
|
125
|
-
if (typeof u !== "object" || u === null)
|
|
126
|
-
return [];
|
|
127
|
-
const { name, references } = u;
|
|
128
|
-
const refs = nameList(references);
|
|
129
|
-
return typeof name === "string" && name.length > 0 && refs.length > 0
|
|
130
|
-
? [{ name, references: refs }]
|
|
131
|
-
: [];
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
function unresolvedLine(u) {
|
|
135
|
-
const which = u.references.length === 1 ? "which has" : "which have";
|
|
136
|
-
return `${u.name} refers to ${u.references.join(", ")}, ${which} no value, so Catalyst can't start work there`;
|
|
137
|
-
}
|
|
138
|
-
/** A team check's fix line. A check that carries `names` (CTC-3561: `required_values`, contract
|
|
139
|
-
* 1.24.0) names them and where to set them. CTC-3606: it also names each `unresolved` variable and
|
|
140
|
-
* the reference with no value, and each other repository's missing names from `repos[]`. `unresolved`
|
|
141
|
-
* is a subset of `names` (the variable exists; its reference does not), so those are not told to be
|
|
142
|
-
* "set". Every field is optional so an older cloud still works. Everything printed is a declared
|
|
143
|
-
* identifier or a repository name, never a value, and nothing here reads a value. Any other check
|
|
144
|
-
* keeps the generic line. */
|
|
145
|
-
function valuesFix(base, c) {
|
|
146
|
-
const unresolved = unresolvedList(c.unresolved);
|
|
147
|
-
const unresolvedNames = new Set(unresolved.map((u) => u.name));
|
|
148
|
-
const names = nameList(c.names).filter((n) => !unresolvedNames.has(n));
|
|
149
|
-
const parts = [];
|
|
150
|
-
if (names.length > 0) {
|
|
151
|
-
parts.push(`set ${names.join(", ")} on the repository's Environment page: open ${base}/settings/projects, then the project, then the repository`);
|
|
152
|
-
}
|
|
153
|
-
for (const u of unresolved)
|
|
154
|
-
parts.push(unresolvedLine(u));
|
|
155
|
-
for (const note of Array.isArray(c.repos) ? c.repos : []) {
|
|
156
|
-
if (typeof note !== "object" ||
|
|
157
|
-
note === null ||
|
|
158
|
-
typeof note.repo !== "string" ||
|
|
159
|
-
note.repo.length === 0)
|
|
160
|
-
continue;
|
|
161
|
-
const repoUnresolved = unresolvedList(note.unresolved);
|
|
162
|
-
const skip = new Set(repoUnresolved.map((u) => u.name));
|
|
163
|
-
const missing = nameList(note.names).filter((n) => !skip.has(n));
|
|
164
|
-
if (missing.length > 0)
|
|
165
|
-
parts.push(`${note.repo} is missing ${missing.join(", ")}`);
|
|
166
|
-
for (const u of repoUnresolved)
|
|
167
|
-
parts.push(`in ${note.repo}, ${unresolvedLine(u)}`);
|
|
168
|
-
}
|
|
169
|
-
return parts.length === 0 ? null : parts.join(". ");
|
|
170
|
-
}
|
|
171
115
|
export async function readyReport(ctx, deps) {
|
|
172
116
|
const checks = [];
|
|
173
117
|
const facts = deps.runtime ?? detectRuntime();
|
|
@@ -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
|
+
}
|