@bli-cockpit/cli 0.2.239 → 0.2.241
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/dist/commands/cal-delete.js +13 -3
- package/dist/commands/cal-scope.js +71 -0
- package/dist/commands/cal-view-settings.js +61 -0
- package/dist/commands/cal.js +11 -0
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +5 -0
- package/dist/commands/drills-render.js +2 -2
- package/dist/commands/drills.js +4 -4
- package/dist/commands/issue-list-page.js +101 -0
- package/dist/commands/issue.js +23 -5
- package/dist/commands/local-arg-values.js +90 -19
- package/dist/commands/local-args-tower-cal.js +148 -8
- package/dist/commands/local-args-tower-docs-msg.js +30 -3
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-manager.js +128 -28
- package/dist/commands/local-args-tower-msg.js +43 -6
- package/dist/commands/local-args-tower-work.js +74 -6
- package/dist/commands/local-help-commands-tower-ops.js +35 -21
- package/dist/commands/local-help-commands-tower.js +15 -11
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/manager-fields-file.js +208 -0
- package/dist/commands/manager-hints.js +88 -0
- package/dist/commands/manager-limits.json +269 -0
- package/dist/commands/manager-read-reasons.js +78 -0
- package/dist/commands/manager-render-home.js +213 -37
- package/dist/commands/manager-render.js +119 -21
- package/dist/commands/manager-requests.js +7 -3
- package/dist/commands/manager.js +67 -43
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +20 -29
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* verbatim: no logic changed, and `normalizeUrl` is still re-exported from
|
|
10
10
|
* `./local-args.js` for the callers that had it there.
|
|
11
11
|
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
* command line at
|
|
12
|
+
* `rejectSupabaseServiceCredential` runs on every token, flag or positional of every command, and is
|
|
13
|
+
* why a Supabase service credential (a service-role JWT, or the newer `sb_secret_…` key) cannot enter a
|
|
14
|
+
* collector command from the command line. It looks at
|
|
15
|
+
* the SHAPE of a value, never at words: a note that says "the Supabase service-role key" is
|
|
16
|
+
* plain text. The wider telemetry-core secret rulebook (`rejectSecretLikeValue`) is NOT run
|
|
17
|
+
* here: it reads words like "password: x" or "M0-…-review" as secrets, which no other command
|
|
18
|
+
* may be refused for. Only `cockpit manager`'s free-text values go through it (BLI-5777).
|
|
15
19
|
*/
|
|
16
20
|
const WORK_ROOT_FLAGS = ["--repo", "--workspace"];
|
|
17
21
|
export function parseNamedArgs(args, options) {
|
|
@@ -23,12 +27,15 @@ export function parseNamedArgs(args, options) {
|
|
|
23
27
|
const positionals = [];
|
|
24
28
|
for (let index = 0; index < args.length; index += 1) {
|
|
25
29
|
const arg = args[index] ?? "";
|
|
26
|
-
|
|
30
|
+
rejectSupabaseServiceCredential(arg);
|
|
27
31
|
if (!arg.startsWith("--")) {
|
|
28
32
|
positionals.push(arg);
|
|
29
33
|
continue;
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
// Split on the FIRST "=" only: the value may hold more of them ("ARR = 1M", a URL query).
|
|
36
|
+
const equalsAt = arg.indexOf("=");
|
|
37
|
+
const flag = equalsAt === -1 ? arg : arg.slice(0, equalsAt);
|
|
38
|
+
const inlineValue = equalsAt === -1 ? undefined : arg.slice(equalsAt + 1);
|
|
32
39
|
if (!allowed.has(flag))
|
|
33
40
|
throw new Error(`Unknown flag: ${flag}`);
|
|
34
41
|
if (valueFlags.has(flag)) {
|
|
@@ -36,7 +43,7 @@ export function parseNamedArgs(args, options) {
|
|
|
36
43
|
if (!value || value.startsWith("--")) {
|
|
37
44
|
throw new Error(`${flag} requires a value.`);
|
|
38
45
|
}
|
|
39
|
-
|
|
46
|
+
rejectSupabaseServiceCredential(value);
|
|
40
47
|
flags.set(flag, value);
|
|
41
48
|
const existing = flagValues.get(flag) ?? [];
|
|
42
49
|
existing.push(value);
|
|
@@ -139,14 +146,84 @@ export function normalizeUrl(value) {
|
|
|
139
146
|
throw new Error("URL value cannot be empty.");
|
|
140
147
|
return trimmed;
|
|
141
148
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Supabase's newer secret key format (`SUPABASE_SECRET_KEY`, admin-contracts.ts): not a JWT, so it needs its own shape.
|
|
151
|
+
* A provider prefix plus a key-length body: prose about the key ("SUPABASE_SECRET_KEY docs") never matches.
|
|
152
|
+
*/
|
|
153
|
+
const SUPABASE_SECRET_KEY_SHAPE = /\bsb_secret_[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*/;
|
|
154
|
+
/**
|
|
155
|
+
* Every command, every token: refuses a Supabase service credential, by SHAPE: a JWT whose payload says it is the
|
|
156
|
+
* privileged role, or an `sb_secret_…` key. The words "service-role key" are plain text (BLI-5777: a meeting note that
|
|
157
|
+
* named one was refused, and an agent reworded the client's words to get through).
|
|
158
|
+
*/
|
|
159
|
+
export function rejectSupabaseServiceCredential(value) {
|
|
160
|
+
if (looksLikeServiceRoleJwt(value) || containsServiceRoleJwt(value) || SUPABASE_SECRET_KEY_SHAPE.test(value)) {
|
|
161
|
+
throw new Error("Service-role credentials are not accepted by local collector commands.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The key SHAPES `cockpit manager` free text is refused for: only high-precision ones, each a provider's own key format, so a
|
|
166
|
+
* sentence about a key, a commit sha or a session name is never refused. Copied from the telemetry-core rulebook
|
|
167
|
+
* (`packages/cockpit-telemetry-core/src/secret-guards-content-rules.ts`, `SECRET_VALUE_PATTERNS` and the password rule), which is
|
|
168
|
+
* built to over-match for redaction (it masks and uploads, never refuses) and is NOT used here: its mem0 rule `m0[-_]…` reads
|
|
169
|
+
* "M0-deliverable-review-session" as a key. Left out on purpose: that rule, `name=value` assignments other than a password, and
|
|
170
|
+
* any generic entropy, token or session-id shape.
|
|
171
|
+
*/
|
|
172
|
+
export const MANAGER_KEY_SHAPES = [
|
|
173
|
+
// Any JWT (a Supabase anon, service-role or user token): three base64url parts, the first starting `eyJ`.
|
|
174
|
+
/\beyJ[A-Za-z0-9_-]{10}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{10}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{10}[A-Za-z0-9_-]*\b/,
|
|
175
|
+
// Real keys are long and hold digits; prose ("Upgrade sk-learn-pipeline-improvements", "rk_quarterly_review_notes_final") does not: a digit is required,
|
|
176
|
+
// and sk- needs a 32+ character body, rk_ and the _live_/_test_ keys 24+. (Not pk_live_: a publishable key is public.)
|
|
177
|
+
/\bsk-(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{32,}/,
|
|
178
|
+
/\b(?:sk|rk)_(?:live|test)_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{24,}/,
|
|
179
|
+
/\brk_(?=[A-Za-z0-9_]*\d)[A-Za-z0-9_]{24,}/,
|
|
180
|
+
SUPABASE_SECRET_KEY_SHAPE,
|
|
181
|
+
/\bgh[pousr]_(?=[A-Za-z0-9_]*\d)[A-Za-z0-9_]{16,}/,
|
|
182
|
+
/\bgithub_pat_(?=[A-Za-z0-9_]*\d)[A-Za-z0-9_]{20,}/,
|
|
183
|
+
/\blin_api_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{16,}/,
|
|
184
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
|
|
185
|
+
/\bxox[abprs]-(?=[A-Za-z0-9-]*\d)[A-Za-z0-9-]{16,}/,
|
|
186
|
+
/\bBearer\s+(?=[A-Za-z0-9._~+/=-]*\d)[A-Za-z0-9._~+/=-]{16,}/,
|
|
187
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
188
|
+
];
|
|
189
|
+
/** A candidate password: the word, then `:` or `=` and a value (its quotes aside). */
|
|
190
|
+
const PASSWORD_ASSIGNMENT = /(?<![A-Za-z0-9])(?:password|passwd|passcode)(?:[_-][A-Za-z0-9_-]{0,40})?["']?\s*[:=]\s*["']?([^\s"'`,;<>(){}\[\]]{6,})/gi;
|
|
191
|
+
/**
|
|
192
|
+
* A password ASSIGNED a value is a secret when the value holds a digit or a symbol ("password: hunter2", `passwd="x1y2z3"`, "admin password: Tr0ub4dor&3").
|
|
193
|
+
* Prose is text: the word alone ("password reset flow"), a value of letters ("Password: rotated."), sentence punctuation after it (".,!?;:"), a hyphen or
|
|
194
|
+
* apostrophe between letters ("password: on-site", "Client password: e-mailed to Jo") and the product name 1Password ("Shared password: 1Password vault").
|
|
195
|
+
*/
|
|
196
|
+
export function looksLikePasswordAssignment(value) {
|
|
197
|
+
for (const match of value.matchAll(PASSWORD_ASSIGNMENT)) {
|
|
198
|
+
const plain = match[1].replace(/[.,!?;:]+$/, "").replace(/1password/gi, "").replace(/(?<=[A-Za-z])[-'’](?=[A-Za-z])/g, "");
|
|
199
|
+
if (/[^A-Za-z]/.test(plain))
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
/** The whole manager free-text question: does this text hold a key shape or an assigned password? */
|
|
205
|
+
export function looksLikeManagerSecret(value) {
|
|
206
|
+
return MANAGER_KEY_SHAPES.some((shape) => shape.test(value)) || looksLikePasswordAssignment(value);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* `cockpit manager` free text only (its text flags and every `--fields-file` value): the service-role JWT, plus the key shapes above.
|
|
210
|
+
* `where` names the flag or file key, so the sentence says which one; the value is never echoed.
|
|
211
|
+
*/
|
|
212
|
+
export function rejectSecretLikeValue(value, where) {
|
|
213
|
+
// The named sentence first (KEY_SHAPES holds the JWT and sb_secret_ shapes too); the service-role JWT check stays as the floor.
|
|
214
|
+
if (looksLikeManagerSecret(value)) {
|
|
215
|
+
throw new Error(`The value of ${where} looks like a secret (a key, token or password), and secrets are not accepted there. Take the secret out and run it again; text that only talks about a key is fine.`);
|
|
216
|
+
}
|
|
217
|
+
rejectSupabaseServiceCredential(value);
|
|
146
218
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
219
|
+
/**
|
|
220
|
+
* A service-role JWT anywhere inside the value ("the service-role key=<jwt> for staging."): every token that starts at a JWT header (`eyJ`, which every
|
|
221
|
+
* Supabase JWT does) is decoded. Anchored on the header, not on any three dotted words, so a JWT glued after a dotted word ("staging.eyJ….….…") is found whole.
|
|
222
|
+
*/
|
|
223
|
+
function containsServiceRoleJwt(value) {
|
|
224
|
+
return (value.match(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g) ?? []).some((token) => looksLikeServiceRoleJwt(token));
|
|
225
|
+
}
|
|
226
|
+
function looksLikeServiceRoleJwt(value) {
|
|
150
227
|
const parts = value.split(".");
|
|
151
228
|
if (parts.length !== 3)
|
|
152
229
|
return false;
|
|
@@ -162,12 +239,6 @@ function looksLikeServiceRoleSecret(value) {
|
|
|
162
239
|
return false;
|
|
163
240
|
}
|
|
164
241
|
}
|
|
165
|
-
function serviceCredentialNamePattern() {
|
|
166
|
-
return new RegExp([
|
|
167
|
-
["SUPABASE", "SERVICE", "ROLE", "KEY"].join("[_-]?"),
|
|
168
|
-
["service", "role"].join("[_-]?"),
|
|
169
|
-
].join("|"), "i");
|
|
170
|
-
}
|
|
171
242
|
function serviceCredentialPayloadPattern() {
|
|
172
243
|
const privilegedRole = ["service", "role"].join("_");
|
|
173
244
|
return new RegExp(`"role"\\s*:\\s*"${privilegedRole}"`);
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
22
22
|
const CAL_ACTIONS = new Set([
|
|
23
|
-
"event", "export", "duplicate", "batch", "undo", "conflicts", "calendar-change",
|
|
23
|
+
"event", "export", "duplicate", "batch", "undo", "conflicts", "calendar-change", "view-settings", "view-settings-change",
|
|
24
24
|
"today",
|
|
25
25
|
"week",
|
|
26
26
|
"next",
|
|
27
27
|
"find",
|
|
28
28
|
"calendars",
|
|
29
29
|
"add-ical",
|
|
30
|
-
"change", "cancel",
|
|
30
|
+
"change", "cancel", "rsvp",
|
|
31
31
|
"create",
|
|
32
32
|
"share",
|
|
33
33
|
"detach",
|
|
@@ -36,7 +36,7 @@ const CAL_ACTIONS = new Set([
|
|
|
36
36
|
"delete",
|
|
37
37
|
]);
|
|
38
38
|
/** Every verb that names ONE thing first — a calendar id, or (delete) an event id. */
|
|
39
|
-
const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["event", "export", "duplicate", "calendar-change", "sync", "detach", "share", "delete", "change", "cancel"]);
|
|
39
|
+
const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["event", "export", "duplicate", "calendar-change", "sync", "detach", "share", "delete", "change", "cancel", "rsvp"]);
|
|
40
40
|
/** `sync-status [<calendar id>]` — every calendar, or one's. Ticket 10730. */
|
|
41
41
|
const CAL_ACTIONS_WITH_OPTIONAL_SUBJECT = new Set(["sync-status", "undo"]);
|
|
42
42
|
const CAL_ALLOWED_FLAGS = [
|
|
@@ -71,6 +71,8 @@ const CAL_ALLOWED_FLAGS = [
|
|
|
71
71
|
"--idempotency-key",
|
|
72
72
|
"--expected-version",
|
|
73
73
|
"--reason",
|
|
74
|
+
"--scope", "--occurrence", "--response", "--comment", "--reminder", "--visibility", "--transparency", "--key",
|
|
75
|
+
"--view", "--days", "--week-start", "--hide-weekends", "--density", "--zone",
|
|
74
76
|
];
|
|
75
77
|
const CAL_VALUE_FLAGS = [
|
|
76
78
|
"--home",
|
|
@@ -94,6 +96,8 @@ const CAL_VALUE_FLAGS = [
|
|
|
94
96
|
"--idempotency-key",
|
|
95
97
|
"--expected-version",
|
|
96
98
|
"--reason",
|
|
99
|
+
"--scope", "--occurrence", "--response", "--comment", "--reminder", "--visibility", "--transparency", "--key",
|
|
100
|
+
"--view", "--days", "--week-start", "--hide-weekends", "--density", "--zone",
|
|
97
101
|
];
|
|
98
102
|
export function parseCalArgs(args) {
|
|
99
103
|
const values = parseNamedArgs(args, { allowedFlags: CAL_ALLOWED_FLAGS, valueFlags: CAL_VALUE_FLAGS });
|
|
@@ -106,13 +110,14 @@ export function parseCalArgs(args) {
|
|
|
106
110
|
// two of them wrong is told about --limit first, as before.
|
|
107
111
|
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
108
112
|
const offset = readCalOffset(values);
|
|
109
|
-
|
|
113
|
+
const command = buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit);
|
|
114
|
+
return { ...command, ...readCalEventScope(action, values), ...readViewSettingsPatch(action, values) };
|
|
110
115
|
}
|
|
111
116
|
function readCalAction(values) {
|
|
112
117
|
const first = values.positionals[0];
|
|
113
118
|
const action = (first === undefined ? "today" : first);
|
|
114
119
|
if (!CAL_ACTIONS.has(action)) {
|
|
115
|
-
throw new Error(`Unknown cal command: ${first}. Try today, week, conflicts, calendar-change, next, find, calendars, add-ical, create, duplicate, batch, undo, share, detach, sync, sync-status, or delete.`);
|
|
120
|
+
throw new Error(`Unknown cal command: ${first}. Try today, week, conflicts, calendar-change, view-settings, view-settings-change, next, find, calendars, add-ical, create, duplicate, batch, undo, share, detach, sync, sync-status, or delete.`);
|
|
116
121
|
}
|
|
117
122
|
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
118
123
|
}
|
|
@@ -122,7 +127,7 @@ function readCalSubjectOrQuery(action, rest) {
|
|
|
122
127
|
if (CAL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
|
|
123
128
|
subject = optionalNonEmpty(rest[0]);
|
|
124
129
|
if (!subject) {
|
|
125
|
-
throw new Error(action === "delete" || action === "duplicate"
|
|
130
|
+
throw new Error(action === "delete" || action === "duplicate" || action === "change" || action === "cancel" || action === "rsvp"
|
|
126
131
|
? `cal ${action} needs an event id — take one from a window verb's output (today/week/next/find).`
|
|
127
132
|
: `cal ${action} needs a calendar id — take one from \`cockpit cal calendars\`.`);
|
|
128
133
|
}
|
|
@@ -182,7 +187,7 @@ function readCalOffset(values) {
|
|
|
182
187
|
function buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit) {
|
|
183
188
|
return {
|
|
184
189
|
kind: "cal",
|
|
185
|
-
occurrenceStart: optionalNonEmpty(values.flags.get("--occurrence-start")),
|
|
190
|
+
occurrenceStart: optionalNonEmpty(values.flags.get("--occurrence") ?? values.flags.get("--occurrence-start")),
|
|
186
191
|
color: optionalNonEmpty(values.flags.get("--color")),
|
|
187
192
|
action,
|
|
188
193
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
@@ -211,7 +216,7 @@ function buildCalCommand(values, action, subject, query, title, startsAt, endsAt
|
|
|
211
216
|
allDay: values.booleans.has("--all-day"),
|
|
212
217
|
timed: values.booleans.has("--timed"),
|
|
213
218
|
keepInvitees: values.booleans.has("--keep-invitees"),
|
|
214
|
-
idempotencyKey: optionalNonEmpty(values.flags.get("--idempotency-key")),
|
|
219
|
+
idempotencyKey: optionalNonEmpty(values.flags.get("--idempotency-key") ?? values.flags.get("--key")),
|
|
215
220
|
expectedVersion: optionalNonEmpty(values.flags.get("--expected-version")),
|
|
216
221
|
reason: optionalNonEmpty(values.flags.get("--reason")),
|
|
217
222
|
meet: values.booleans.has("--meet"),
|
|
@@ -230,4 +235,139 @@ function splitAddresses(raw) {
|
|
|
230
235
|
.split(",")
|
|
231
236
|
.map((value) => value.trim())
|
|
232
237
|
.filter((value) => value !== "");
|
|
238
|
+
}
|
|
239
|
+
const SCOPES = ["instance", "following", "series"];
|
|
240
|
+
const RSVP_RESPONSES = ["accepted", "declined", "tentative"];
|
|
241
|
+
const VISIBILITIES = ["default", "public", "private", "confidential"];
|
|
242
|
+
const TRANSPARENCIES = ["opaque", "transparent"];
|
|
243
|
+
const WRITE_ACTIONS = new Set(["create", "change"]);
|
|
244
|
+
/**
|
|
245
|
+
* BLI-21311: the scope, RSVP and event-detail flags. Validated here so a typo
|
|
246
|
+
* never costs a request; the door still owns every rule that needs the event.
|
|
247
|
+
*/
|
|
248
|
+
function readCalEventScope(action, values) {
|
|
249
|
+
const out = {};
|
|
250
|
+
const scope = optionalNonEmpty(values.flags.get("--scope"));
|
|
251
|
+
if (scope !== undefined) {
|
|
252
|
+
if (!SCOPES.includes(scope))
|
|
253
|
+
throw new Error("--scope takes instance, following or series.");
|
|
254
|
+
if (action !== "change" && action !== "cancel" && action !== "delete" && action !== "rsvp")
|
|
255
|
+
throw new Error(`cal ${action} does not take --scope.`);
|
|
256
|
+
if (action === "rsvp" && scope === "following")
|
|
257
|
+
throw new Error("An RSVP takes --scope instance or series.");
|
|
258
|
+
out.scope = scope;
|
|
259
|
+
}
|
|
260
|
+
if (action === "rsvp") {
|
|
261
|
+
const response = optionalNonEmpty(values.flags.get("--response"));
|
|
262
|
+
if (!response || !RSVP_RESPONSES.includes(response)) {
|
|
263
|
+
throw new Error("cal rsvp needs --response accepted, declined or tentative.");
|
|
264
|
+
}
|
|
265
|
+
out.rsvpResponse = response;
|
|
266
|
+
out.rsvpComment = optionalNonEmpty(values.flags.get("--comment"));
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
if (values.flags.has("--response") || values.flags.has("--comment"))
|
|
270
|
+
throw new Error("--response and --comment belong to cal rsvp.");
|
|
271
|
+
if (!WRITE_ACTIONS.has(action)) {
|
|
272
|
+
for (const flag of ["--reminder", "--visibility", "--transparency"]) {
|
|
273
|
+
if (values.flags.has(flag))
|
|
274
|
+
throw new Error(`cal ${action} does not take ${flag}.`);
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
const color = optionalNonEmpty(values.flags.get("--color"));
|
|
279
|
+
if (color !== undefined) {
|
|
280
|
+
if (color === "none")
|
|
281
|
+
out.colorId = null;
|
|
282
|
+
else if (/^(?:[1-9]|10|11)$/.test(color))
|
|
283
|
+
out.colorId = Number(color);
|
|
284
|
+
else
|
|
285
|
+
throw new Error("--color takes a colour number 1 to 11, or none.");
|
|
286
|
+
}
|
|
287
|
+
const reminders = readReminders(values.flagValues.get("--reminder") ?? []);
|
|
288
|
+
if (reminders !== undefined)
|
|
289
|
+
out.reminders = reminders;
|
|
290
|
+
const visibility = optionalNonEmpty(values.flags.get("--visibility"));
|
|
291
|
+
if (visibility !== undefined) {
|
|
292
|
+
if (!VISIBILITIES.includes(visibility))
|
|
293
|
+
throw new Error(`--visibility takes ${VISIBILITIES.join(", ")}.`);
|
|
294
|
+
out.visibility = visibility;
|
|
295
|
+
}
|
|
296
|
+
const transparency = optionalNonEmpty(values.flags.get("--transparency"));
|
|
297
|
+
if (transparency !== undefined) {
|
|
298
|
+
if (!TRANSPARENCIES.includes(transparency))
|
|
299
|
+
throw new Error(`--transparency takes ${TRANSPARENCIES.join(" or ")}.`);
|
|
300
|
+
out.transparency = transparency;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
/** `--reminder 10 --reminder 60:email` or `--reminder default`. Max 5, 0..40320 minutes. */
|
|
305
|
+
function readReminders(raw) {
|
|
306
|
+
if (raw.length === 0)
|
|
307
|
+
return undefined;
|
|
308
|
+
if (raw.includes("default")) {
|
|
309
|
+
if (raw.length > 1)
|
|
310
|
+
throw new Error("--reminder default stands alone: it means the calendar's own reminders.");
|
|
311
|
+
return "default";
|
|
312
|
+
}
|
|
313
|
+
if (raw.length > 5)
|
|
314
|
+
throw new Error("An event holds at most 5 reminders.");
|
|
315
|
+
return raw.map((entry) => {
|
|
316
|
+
const [minutesText, method, extra] = entry.split(":");
|
|
317
|
+
if (extra !== undefined || !/^\d+$/.test(minutesText ?? ""))
|
|
318
|
+
throw new Error("--reminder takes minutes (0 to 40320), optionally minutes:popup or minutes:email, or default.");
|
|
319
|
+
const minutes = Number(minutesText);
|
|
320
|
+
if (minutes > 40320)
|
|
321
|
+
throw new Error("--reminder minutes run from 0 to 40320 (four weeks).");
|
|
322
|
+
if (method !== undefined && method !== "popup" && method !== "email")
|
|
323
|
+
throw new Error("A reminder method is popup or email.");
|
|
324
|
+
return { minutes, ...(method ? { method } : {}) };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const VIEW_FLAG_KEYS = ["--view", "--days", "--week-start", "--hide-weekends", "--density", "--zone"];
|
|
328
|
+
/**
|
|
329
|
+
* BLI-21423 `view-settings-change`: the flags become the same partial patch the
|
|
330
|
+
* door takes. Types are read here; the door owns the ranges and the zone list.
|
|
331
|
+
*/
|
|
332
|
+
function readViewSettingsPatch(action, values) {
|
|
333
|
+
if (action !== "view-settings-change") {
|
|
334
|
+
for (const flag of VIEW_FLAG_KEYS) {
|
|
335
|
+
if (values.flags.has(flag))
|
|
336
|
+
throw new Error(`cal ${action} does not take ${flag}; it belongs to cal view-settings-change.`);
|
|
337
|
+
}
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
340
|
+
const patch = {};
|
|
341
|
+
const view = optionalNonEmpty(values.flags.get("--view"));
|
|
342
|
+
if (view !== undefined)
|
|
343
|
+
patch.default_view = view;
|
|
344
|
+
const days = optionalNonEmpty(values.flags.get("--days"));
|
|
345
|
+
if (days !== undefined) {
|
|
346
|
+
if (!/^\d+$/.test(days))
|
|
347
|
+
throw new Error("--days takes a whole number from 2 to 9.");
|
|
348
|
+
patch.day_count = Number(days);
|
|
349
|
+
}
|
|
350
|
+
const weekStart = optionalNonEmpty(values.flags.get("--week-start"));
|
|
351
|
+
if (weekStart !== undefined)
|
|
352
|
+
patch.week_start = weekStart;
|
|
353
|
+
const hide = optionalNonEmpty(values.flags.get("--hide-weekends"));
|
|
354
|
+
if (hide !== undefined) {
|
|
355
|
+
if (hide !== "on" && hide !== "off")
|
|
356
|
+
throw new Error("--hide-weekends takes on or off.");
|
|
357
|
+
patch.hide_weekends = hide === "on";
|
|
358
|
+
}
|
|
359
|
+
const density = optionalNonEmpty(values.flags.get("--density"));
|
|
360
|
+
if (density !== undefined)
|
|
361
|
+
patch.density = density;
|
|
362
|
+
const zones = values.flagValues.get("--zone") ?? [];
|
|
363
|
+
if (zones.length) {
|
|
364
|
+
if (zones.includes("none")) {
|
|
365
|
+
if (zones.length > 1)
|
|
366
|
+
throw new Error("--zone none stands alone: it clears the extra zones.");
|
|
367
|
+
patch.extra_zones = [];
|
|
368
|
+
}
|
|
369
|
+
else
|
|
370
|
+
patch.extra_zones = zones;
|
|
371
|
+
}
|
|
372
|
+
return Object.keys(patch).length ? { viewSettings: patch } : {};
|
|
233
373
|
}
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
10
10
|
import { parseDocsExportFlags } from "./local-args-docs-export.js";
|
|
11
|
-
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status"]);
|
|
11
|
+
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status", "share", "access"]);
|
|
12
12
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
13
|
// BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
|
|
14
14
|
// it takes SEVERAL references now, so it no longer shares the "exactly one"
|
|
15
15
|
// rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
|
|
16
16
|
// (the same door, called once per ref), so it joined `read`'s branch rather
|
|
17
17
|
// than this set.
|
|
18
|
-
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status"]);
|
|
18
|
+
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status", "share", "access"]);
|
|
19
19
|
const DOCS_ALLOWED_FLAGS = [
|
|
20
20
|
"--home",
|
|
21
21
|
"--dashboard-url",
|
|
@@ -37,6 +37,9 @@ const DOCS_ALLOWED_FLAGS = [
|
|
|
37
37
|
"--offset",
|
|
38
38
|
"--format",
|
|
39
39
|
"--out",
|
|
40
|
+
"--add",
|
|
41
|
+
"--remove",
|
|
42
|
+
"--reason",
|
|
40
43
|
"--json",
|
|
41
44
|
];
|
|
42
45
|
const DOCS_VALUE_FLAGS = [
|
|
@@ -54,6 +57,9 @@ const DOCS_VALUE_FLAGS = [
|
|
|
54
57
|
"--offset",
|
|
55
58
|
"--format",
|
|
56
59
|
"--out",
|
|
60
|
+
"--add",
|
|
61
|
+
"--remove",
|
|
62
|
+
"--reason",
|
|
57
63
|
];
|
|
58
64
|
export function parseDocsArgs(args) {
|
|
59
65
|
const values = parseNamedArgs(args, { allowedFlags: DOCS_ALLOWED_FLAGS, valueFlags: DOCS_VALUE_FLAGS });
|
|
@@ -123,7 +129,7 @@ function parseDocsAction(positionals) {
|
|
|
123
129
|
const first = positionals[0];
|
|
124
130
|
const action = (first === undefined ? "list" : first);
|
|
125
131
|
if (!DOCS_ACTIONS.has(action)) {
|
|
126
|
-
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export,
|
|
132
|
+
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export, mutation-status, share, or access.`);
|
|
127
133
|
}
|
|
128
134
|
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
129
135
|
}
|
|
@@ -233,6 +239,26 @@ function parseDocsReadFlags(action, values) {
|
|
|
233
239
|
throw new Error("--offset belongs to `cockpit docs read`.");
|
|
234
240
|
return { query, wholeWord, limit, bodies, section, offset };
|
|
235
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* BLI-21313: `docs share <doc> --add <email> | --remove <email> [--reason <text>]` names or removes a page editor;
|
|
244
|
+
* `docs access <doc>` reads who may edit and why. A person is an exact email address, never a fuzzy name.
|
|
245
|
+
*/
|
|
246
|
+
function parseDocsShareFlags(action, values) {
|
|
247
|
+
const addPerson = optionalNonEmpty(values.flags.get("--add"));
|
|
248
|
+
const removePerson = optionalNonEmpty(values.flags.get("--remove"));
|
|
249
|
+
const reason = optionalNonEmpty(values.flags.get("--reason"));
|
|
250
|
+
if ((addPerson || removePerson || reason) && action !== "share") {
|
|
251
|
+
throw new Error("--add, --remove and --reason belong to `cockpit docs share`.");
|
|
252
|
+
}
|
|
253
|
+
if (action === "share") {
|
|
254
|
+
if (Boolean(addPerson) === Boolean(removePerson)) {
|
|
255
|
+
throw new Error("docs share needs exactly one of --add <email> or --remove <email>.");
|
|
256
|
+
}
|
|
257
|
+
if (reason && reason.length > 200)
|
|
258
|
+
throw new Error("--reason can be at most 200 characters.");
|
|
259
|
+
}
|
|
260
|
+
return { ...(addPerson ? { addPerson } : {}), ...(removePerson ? { removePerson } : {}), ...(reason ? { reason } : {}) };
|
|
261
|
+
}
|
|
236
262
|
function parseDocsOffset(raw) {
|
|
237
263
|
const offsetRaw = optionalNonEmpty(raw);
|
|
238
264
|
if (offsetRaw === undefined)
|
|
@@ -268,6 +294,7 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
|
|
|
268
294
|
...(link.notionPage ? { notionPage: link.notionPage } : {}),
|
|
269
295
|
...(link.clearLink ? { clearLink: true } : {}),
|
|
270
296
|
...parseDocsExportFlags(action, values),
|
|
297
|
+
...parseDocsShareFlags(action, values),
|
|
271
298
|
json: values.booleans.has("--json"),
|
|
272
299
|
};
|
|
273
300
|
}
|
|
@@ -54,7 +54,7 @@ export function parseDrillsArgs(args) {
|
|
|
54
54
|
if (action === "theme-set") {
|
|
55
55
|
theme = rest.join(" ").trim();
|
|
56
56
|
if (!theme)
|
|
57
|
-
throw new Error("`cockpit drills theme set` takes a theme: video-game,
|
|
57
|
+
throw new Error("`cockpit drills theme set` takes a theme: plain (no theme, the default), video-game, sports, movies-tv, music, anime, cooking, or anything you're into in your own words.");
|
|
58
58
|
}
|
|
59
59
|
else if (rest.length > 0)
|
|
60
60
|
throw new Error(`\`cockpit drills ${action}\` takes no positional arguments.`);
|