@wayai/cli 0.3.203 → 0.3.205

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/index.js CHANGED
@@ -447,11 +447,11 @@ function captureException2(error, context) {
447
447
  Sentry.captureException(error);
448
448
  });
449
449
  }
450
- function addApiBreadcrumb(method, path39) {
450
+ function addApiBreadcrumb(method, path41) {
451
451
  if (!initialized) return;
452
452
  Sentry.addBreadcrumb({
453
453
  category: "http",
454
- message: `${method} ${path39}`,
454
+ message: `${method} ${path41}`,
455
455
  level: "info"
456
456
  });
457
457
  }
@@ -2944,8 +2944,8 @@ var init_parseUtil = __esm({
2944
2944
  init_errors();
2945
2945
  init_en();
2946
2946
  makeIssue = (params) => {
2947
- const { data, path: path39, errorMaps, issueData } = params;
2948
- const fullPath = [...path39, ...issueData.path || []];
2947
+ const { data, path: path41, errorMaps, issueData } = params;
2948
+ const fullPath = [...path41, ...issueData.path || []];
2949
2949
  const fullIssue = {
2950
2950
  ...issueData,
2951
2951
  path: fullPath
@@ -3256,11 +3256,11 @@ var init_types = __esm({
3256
3256
  init_parseUtil();
3257
3257
  init_util();
3258
3258
  ParseInputLazyPath = class {
3259
- constructor(parent, value, path39, key) {
3259
+ constructor(parent, value, path41, key) {
3260
3260
  this._cachedPath = [];
3261
3261
  this.parent = parent;
3262
3262
  this.data = value;
3263
- this._path = path39;
3263
+ this._path = path41;
3264
3264
  this._key = key;
3265
3265
  }
3266
3266
  get path() {
@@ -6952,12 +6952,12 @@ function typeMatches(typeField, allowed) {
6952
6952
  if (Array.isArray(typeField)) return typeField.every((t) => typeof t === "string" && allowed.has(t));
6953
6953
  return false;
6954
6954
  }
6955
- function validateSchema(schema, path39, errors, opts = {}) {
6955
+ function validateSchema(schema, path41, errors, opts = {}) {
6956
6956
  if (typeof schema === "boolean") return;
6957
6957
  const depth = opts.depth ?? 0;
6958
6958
  if (depth > MAX_SCHEMA_DEPTH) {
6959
6959
  errors.push({
6960
- path: path39 || "<root>",
6960
+ path: path41 || "<root>",
6961
6961
  message: `schema nesting exceeds ${MAX_SCHEMA_DEPTH} levels`,
6962
6962
  suggestion: "Flatten the schema; LLM providers reject deeply nested tool input_schemas."
6963
6963
  });
@@ -6965,7 +6965,7 @@ function validateSchema(schema, path39, errors, opts = {}) {
6965
6965
  }
6966
6966
  if (!isRecord2(schema)) {
6967
6967
  errors.push({
6968
- path: path39,
6968
+ path: path41,
6969
6969
  message: `expected object, got ${schema === null ? "null" : typeof schema}`
6970
6970
  });
6971
6971
  return;
@@ -6973,14 +6973,14 @@ function validateSchema(schema, path39, errors, opts = {}) {
6973
6973
  if (opts.isRoot) {
6974
6974
  if ("type" in schema && schema.type !== "object") {
6975
6975
  errors.push({
6976
- path: path39 ? `${path39}.type` : "type",
6976
+ path: path41 ? `${path41}.type` : "type",
6977
6977
  message: `root type must be 'object', got ${JSON.stringify(schema.type)}`,
6978
6978
  suggestion: "Tool parameters at the root must be an object: `{ type: 'object', properties: { ... } }`."
6979
6979
  });
6980
6980
  }
6981
6981
  } else if ("type" in schema && !typeMatches(schema.type, ALLOWED_TYPES)) {
6982
6982
  errors.push({
6983
- path: `${path39}.type`,
6983
+ path: `${path41}.type`,
6984
6984
  message: `type must be one of ${[...ALLOWED_TYPES].join("/")} or an array of those, got ${JSON.stringify(schema.type)}`
6985
6985
  });
6986
6986
  }
@@ -6990,25 +6990,25 @@ function validateSchema(schema, path39, errors, opts = {}) {
6990
6990
  const isPlaceholder = PLACEHOLDER_TOKENS.includes(e);
6991
6991
  const isOutcomePlaceholder = e === "[KANBAN_OUTCOME_VALUES]";
6992
6992
  errors.push({
6993
- path: `${path39}.enum`,
6993
+ path: `${path41}.enum`,
6994
6994
  message: `enum must be a non-empty array of primitives, got string ${JSON.stringify(e)}`,
6995
6995
  suggestion: isOutcomePlaceholder ? "Declare `outcomes` on the hub's terminal kanban status so the platform can render the placeholder into a valid array; with none configured the platform drops the parameter instead." : isPlaceholder ? "Set `operation: 'update_kanban_status'` on the tool and ensure at least one hub.kanban_status has `allowsAgentUpdate: true` so the platform can render the placeholder into a valid array." : 'Wrap the value in an array: `enum: ["value"]`.'
6996
6996
  });
6997
6997
  } else if (!Array.isArray(e)) {
6998
6998
  errors.push({
6999
- path: `${path39}.enum`,
6999
+ path: `${path41}.enum`,
7000
7000
  message: `enum must be a non-empty array of primitives, got ${typeof e}`
7001
7001
  });
7002
7002
  } else if (e.length === 0) {
7003
7003
  errors.push({
7004
- path: `${path39}.enum`,
7004
+ path: `${path41}.enum`,
7005
7005
  message: "enum must not be empty"
7006
7006
  });
7007
7007
  } else {
7008
7008
  for (let i = 0; i < e.length; i++) {
7009
7009
  if (!isPrimitive(e[i])) {
7010
7010
  errors.push({
7011
- path: `${path39}.enum[${i}]`,
7011
+ path: `${path41}.enum[${i}]`,
7012
7012
  message: `enum entry must be a primitive (string/number/boolean/null), got ${typeof e[i]}`
7013
7013
  });
7014
7014
  }
@@ -7018,7 +7018,7 @@ function validateSchema(schema, path39, errors, opts = {}) {
7018
7018
  for (const key of ["exclusiveMinimum", "exclusiveMaximum"]) {
7019
7019
  if (key in schema && typeof schema[key] === "boolean") {
7020
7020
  errors.push({
7021
- path: `${path39}.${key}`,
7021
+ path: `${path41}.${key}`,
7022
7022
  message: `${key} as boolean is draft-04 syntax; draft 2020-12 requires a numeric value`,
7023
7023
  suggestion: `Replace with the numeric bound, e.g. \`${key}: 0\`.`
7024
7024
  });
@@ -7027,45 +7027,45 @@ function validateSchema(schema, path39, errors, opts = {}) {
7027
7027
  if ("properties" in schema) {
7028
7028
  if (!isRecord2(schema.properties)) {
7029
7029
  errors.push({
7030
- path: `${path39}.properties`,
7030
+ path: `${path41}.properties`,
7031
7031
  message: `properties must be an object, got ${Array.isArray(schema.properties) ? "array" : typeof schema.properties}`
7032
7032
  });
7033
7033
  } else {
7034
7034
  for (const [propName, propSchema] of Object.entries(schema.properties)) {
7035
- validateSchema(propSchema, `${path39}.properties.${propName}`, errors, { depth: depth + 1 });
7035
+ validateSchema(propSchema, `${path41}.properties.${propName}`, errors, { depth: depth + 1 });
7036
7036
  }
7037
7037
  }
7038
7038
  }
7039
7039
  if (schemaTypeIncludes(schema, "array") && "items" in schema) {
7040
7040
  if (Array.isArray(schema.items)) {
7041
- schema.items.forEach((sub, i) => validateSchema(sub, `${path39}.items[${i}]`, errors, { depth: depth + 1 }));
7041
+ schema.items.forEach((sub, i) => validateSchema(sub, `${path41}.items[${i}]`, errors, { depth: depth + 1 }));
7042
7042
  } else {
7043
- validateSchema(schema.items, `${path39}.items`, errors, { depth: depth + 1 });
7043
+ validateSchema(schema.items, `${path41}.items`, errors, { depth: depth + 1 });
7044
7044
  }
7045
7045
  }
7046
7046
  for (const key of SUBSCHEMA_OBJECT_KEYWORDS) {
7047
7047
  if (key in schema && isRecord2(schema[key])) {
7048
- validateSchema(schema[key], `${path39}.${key}`, errors, { depth: depth + 1 });
7048
+ validateSchema(schema[key], `${path41}.${key}`, errors, { depth: depth + 1 });
7049
7049
  }
7050
7050
  }
7051
7051
  for (const key of SUBSCHEMA_LIST_KEYWORDS) {
7052
7052
  const list = schema[key];
7053
7053
  if (Array.isArray(list)) {
7054
- list.forEach((sub, i) => validateSchema(sub, `${path39}.${key}[${i}]`, errors, { depth: depth + 1 }));
7054
+ list.forEach((sub, i) => validateSchema(sub, `${path41}.${key}[${i}]`, errors, { depth: depth + 1 }));
7055
7055
  }
7056
7056
  }
7057
7057
  for (const key of SUBSCHEMA_MAP_KEYWORDS) {
7058
7058
  const map = schema[key];
7059
7059
  if (isRecord2(map)) {
7060
7060
  for (const [name, sub] of Object.entries(map)) {
7061
- validateSchema(sub, `${path39}.${key}.${name}`, errors, { depth: depth + 1 });
7061
+ validateSchema(sub, `${path41}.${key}.${name}`, errors, { depth: depth + 1 });
7062
7062
  }
7063
7063
  }
7064
7064
  }
7065
7065
  const reportedPaths = new Set(errors.map((e) => e.path));
7066
7066
  for (const [k, v] of Object.entries(schema)) {
7067
7067
  if (typeof v !== "string") continue;
7068
- const fieldPath = `${path39}.${k}`;
7068
+ const fieldPath = `${path41}.${k}`;
7069
7069
  if (reportedPaths.has(fieldPath)) continue;
7070
7070
  for (const token of PLACEHOLDER_TOKENS) {
7071
7071
  if (v === token) {
@@ -7121,12 +7121,12 @@ function validateFollowupLinks(followups, label, ctx) {
7121
7121
  const linkTargets = /* @__PURE__ */ new Set();
7122
7122
  followups.forEach((followup, i) => {
7123
7123
  const ref = followup?.after_followup_id;
7124
- const path39 = ["followups", i, "after_followup_id"];
7124
+ const path41 = ["followups", i, "after_followup_id"];
7125
7125
  if (followup?.type !== "inactivity_after_before_event") {
7126
7126
  if (ref !== void 0) {
7127
7127
  ctx.addIssue({
7128
7128
  code: external_exports.ZodIssueCode.custom,
7129
- path: path39,
7129
+ path: path41,
7130
7130
  message: `kanban status ${label}: after_followup_id is only valid on inactivity_after_before_event followups (this one is ${followup?.type})`
7131
7131
  });
7132
7132
  }
@@ -7135,7 +7135,7 @@ function validateFollowupLinks(followups, label, ctx) {
7135
7135
  if (ref === void 0) {
7136
7136
  ctx.addIssue({
7137
7137
  code: external_exports.ZodIssueCode.custom,
7138
- path: path39,
7138
+ path: path41,
7139
7139
  message: `kanban status ${label}: inactivity_after_before_event requires after_followup_id naming the id of a before_event followup in the same status`
7140
7140
  });
7141
7141
  return;
@@ -7145,7 +7145,7 @@ function validateFollowupLinks(followups, label, ctx) {
7145
7145
  if (matches.length > 1) {
7146
7146
  ctx.addIssue({
7147
7147
  code: external_exports.ZodIssueCode.custom,
7148
- path: path39,
7148
+ path: path41,
7149
7149
  message: `kanban status ${label}: after_followup_id "${ref}" matches ${matches.length} followups \u2014 a link must name exactly one`
7150
7150
  });
7151
7151
  return;
@@ -7157,7 +7157,7 @@ function validateFollowupLinks(followups, label, ctx) {
7157
7157
  );
7158
7158
  ctx.addIssue({
7159
7159
  code: external_exports.ZodIssueCode.custom,
7160
- path: path39,
7160
+ path: path41,
7161
7161
  message: hasIdlessBeforeEvent ? `kanban status ${label}: after_followup_id "${ref}" matches no followup \u2014 a link target must declare an explicit id, and this status has before_event followups without one` : `kanban status ${label}: after_followup_id "${ref}" matches no followup in this status`
7162
7162
  });
7163
7163
  return;
@@ -7165,7 +7165,7 @@ function validateFollowupLinks(followups, label, ctx) {
7165
7165
  if (target.type !== "before_event") {
7166
7166
  ctx.addIssue({
7167
7167
  code: external_exports.ZodIssueCode.custom,
7168
- path: path39,
7168
+ path: path41,
7169
7169
  message: `kanban status ${label}: after_followup_id "${ref}" points at a ${target.type} followup \u2014 only before_event followups can anchor a chain`
7170
7170
  });
7171
7171
  }
@@ -7421,9 +7421,9 @@ function monitorRuleActions(monitorConfig) {
7421
7421
  if (!monitorConfig || typeof monitorConfig !== "object") return [];
7422
7422
  const config = monitorConfig;
7423
7423
  const found = [];
7424
- const visit = (action, path39) => {
7424
+ const visit = (action, path41) => {
7425
7425
  if (!action || typeof action !== "object" || Array.isArray(action)) return;
7426
- found.push({ action, path: path39 });
7426
+ found.push({ action, path: path41 });
7427
7427
  };
7428
7428
  if (Array.isArray(config.rules)) {
7429
7429
  config.rules.forEach((rule, index) => {
@@ -7455,25 +7455,25 @@ function collectMonitorRuleIssues(monitorConfig) {
7455
7455
  const trigger = resolveMonitorTrigger(monitorConfig);
7456
7456
  const kinds = monitorRuleActionKinds(trigger);
7457
7457
  const issues = [...callUtteranceRuleIssues(monitorConfig), ...automationGateIssues(monitorConfig)];
7458
- for (const { action, path: path39 } of monitorRuleActions(monitorConfig)) {
7459
- if (trigger === "call_utterance" && path39[0] === "fallback") continue;
7458
+ for (const { action, path: path41 } of monitorRuleActions(monitorConfig)) {
7459
+ if (trigger === "call_utterance" && path41[0] === "fallback") continue;
7460
7460
  const kind = action.kind;
7461
7461
  if (typeof kind === "string" && !kinds.includes(kind)) {
7462
- issues.push({ path: [...path39, "kind"], message: monitorActionKindMessage(kind, trigger) });
7462
+ issues.push({ path: [...path41, "kind"], message: monitorActionKindMessage(kind, trigger) });
7463
7463
  }
7464
7464
  const toolName = action.tool_name;
7465
7465
  if (kind === "call_tool" && typeof toolName === "string" && isRefusedNativeToolName(toolName, trigger)) {
7466
- issues.push({ path: [...path39, "tool_name"], message: monitorRuleToolNotAllowedMessage(toolName, trigger) });
7466
+ issues.push({ path: [...path41, "tool_name"], message: monitorRuleToolNotAllowedMessage(toolName, trigger) });
7467
7467
  continue;
7468
7468
  }
7469
7469
  if (kind === "call_tool" && toolName === INSERT_NOTE_TOOL_NAME) {
7470
- issues.push(...insertNoteArgumentIssues(action, path39));
7470
+ issues.push(...insertNoteArgumentIssues(action, path41));
7471
7471
  }
7472
7472
  if (kind === "call_tool" && toolName === RUN_MONITOR_TOOL_NAME) {
7473
7473
  const callee = readRunMonitorCallee(action);
7474
7474
  if (!callee.ok) {
7475
7475
  issues.push({
7476
- path: [...path39, "args", "monitor_name"],
7476
+ path: [...path41, "args", "monitor_name"],
7477
7477
  message: runMonitorCalleeMessage(callee.reason)
7478
7478
  });
7479
7479
  }
@@ -7533,11 +7533,11 @@ function insertNoteTemplateMessage(reason) {
7533
7533
  return `insert_note's template is longer than the ${MONITOR_NOTE_TEMPLATE_MAX}-character limit.`;
7534
7534
  }
7535
7535
  }
7536
- function insertNoteArgumentIssues(action, path39) {
7536
+ function insertNoteArgumentIssues(action, path41) {
7537
7537
  const result = readInsertNoteTemplate(action);
7538
7538
  if (result.ok) return [];
7539
7539
  return [{
7540
- path: [...path39, "args", "template"],
7540
+ path: [...path41, "args", "template"],
7541
7541
  message: insertNoteTemplateMessage(result.reason)
7542
7542
  }];
7543
7543
  }
@@ -7634,8 +7634,8 @@ function evalInitialStateError(input) {
7634
7634
  const parsed = evalInitialStateEntry.safeParse(entries[i]);
7635
7635
  if (!parsed.success) {
7636
7636
  const issue = parsed.error.issues[0];
7637
- const path39 = issue?.path.join(".") || "?";
7638
- return `initial_state[${i}].${path39} is invalid: ${issue?.message ?? "malformed"}`;
7637
+ const path41 = issue?.path.join(".") || "?";
7638
+ return `initial_state[${i}].${path41} is invalid: ${issue?.message ?? "malformed"}`;
7639
7639
  }
7640
7640
  if (seenSlugs.has(parsed.data.slug)) {
7641
7641
  return `initial_state[${i}].slug "${parsed.data.slug}" is declared more than once`;
@@ -7859,10 +7859,10 @@ function refineHubAsCodeEvals(config, ctx) {
7859
7859
  function refineHubAsCodeEvalAttachments(config, ctx) {
7860
7860
  const cfg = config;
7861
7861
  const referencedHashes = /* @__PURE__ */ new Set();
7862
- const addTurnIssue = (path39, turn, name) => {
7862
+ const addTurnIssue = (path41, turn, name) => {
7863
7863
  const error = evalTurnAttachmentsError(turn);
7864
7864
  if (error) {
7865
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path39, message: `${name}: ${error}` });
7865
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path41, message: `${name}: ${error}` });
7866
7866
  return;
7867
7867
  }
7868
7868
  for (const hash of collectTurnAttachmentHashes(turn)) referencedHashes.add(hash);
@@ -7937,17 +7937,17 @@ function refineHubAsCodeCallOpenings(config, ctx) {
7937
7937
  const agent = entry;
7938
7938
  const openings = agent?.call_openings;
7939
7939
  if (openings === void 0 || openings === null) return;
7940
- const path39 = ["agents", a, "call_openings"];
7940
+ const path41 = ["agents", a, "call_openings"];
7941
7941
  const name = `agent "${typeof agent?.name === "string" ? agent.name : "<unnamed>"}"`;
7942
7942
  if (typeof openings !== "object" || Array.isArray(openings)) {
7943
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path39, message: `${name}: call_openings must map a language to an opening` });
7943
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path41, message: `${name}: call_openings must map a language to an opening` });
7944
7944
  return;
7945
7945
  }
7946
7946
  if (agent?.role !== "pilot_voice" && Object.keys(openings).length > 0) {
7947
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path39, message: `${name}: only a pilot_voice agent has call_openings` });
7947
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path41, message: `${name}: only a pilot_voice agent has call_openings` });
7948
7948
  }
7949
7949
  for (const [language, raw] of Object.entries(openings)) {
7950
- const at = [...path39, language];
7950
+ const at = [...path41, language];
7951
7951
  if (!callOpeningLanguage.safeParse(language).success) {
7952
7952
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: at, message: `${name}: call_openings language must be one of ${CALL_OPENING_LANGUAGES.join(", ")}` });
7953
7953
  continue;
@@ -8074,11 +8074,11 @@ function refineHubAsCodeDelegation(config, ctx) {
8074
8074
  }
8075
8075
  }
8076
8076
  if (del.context_boundary === void 0) return;
8077
- const path39 = ["agents", agentIndex, "tools", "delegation", delIndex, "context_boundary"];
8077
+ const path41 = ["agents", agentIndex, "tools", "delegation", delIndex, "context_boundary"];
8078
8078
  if (del.type !== "hub") {
8079
8079
  ctx.addIssue({
8080
8080
  code: external_exports.ZodIssueCode.custom,
8081
- path: path39,
8081
+ path: path41,
8082
8082
  message: `context_boundary applies only to \`type: hub\` delegations (got type "${String(del.type)}")`
8083
8083
  });
8084
8084
  return;
@@ -8086,7 +8086,7 @@ function refineHubAsCodeDelegation(config, ctx) {
8086
8086
  if (!CONTEXT_BOUNDARIES.includes(del.context_boundary)) {
8087
8087
  ctx.addIssue({
8088
8088
  code: external_exports.ZodIssueCode.custom,
8089
- path: path39,
8089
+ path: path41,
8090
8090
  message: `context_boundary must be one of: ${CONTEXT_BOUNDARIES.join(", ")}`
8091
8091
  });
8092
8092
  }
@@ -9723,7 +9723,7 @@ var init_chunk_46WU2TD4 = __esm({
9723
9723
  }
9724
9724
  });
9725
9725
 
9726
- // ../../packages/core/dist/chunk-BMAOKAMB.js
9726
+ // ../../packages/core/dist/chunk-T3VB6VFG.js
9727
9727
  function cacheReconcileIdCount(body) {
9728
9728
  return body.conversations.length + body.histories.reduce((n, history) => n + history.conversation_ids.length, 0) + body.threads.length;
9729
9729
  }
@@ -9769,23 +9769,29 @@ function parseCommunityLessonVideoUrl(url) {
9769
9769
  const parts = /^https?:\/\/([^/?#]+)([^?#]*)(?:\?([^#]*))?(?:#.*)?$/i.exec(url.trim());
9770
9770
  if (!parts) return null;
9771
9771
  const host = parts[1].toLowerCase();
9772
- const path39 = parts[2] ?? "";
9772
+ const path41 = parts[2] ?? "";
9773
9773
  const query = parts[3] ?? "";
9774
9774
  let id;
9775
9775
  if (host === YOUTU_BE) {
9776
- id = /^\/([A-Za-z0-9_-]{11})\/?$/.exec(path39)?.[1];
9776
+ id = /^\/([A-Za-z0-9_-]{11})\/?$/.exec(path41)?.[1];
9777
9777
  } else if (YOUTUBE_HOSTS.has(host)) {
9778
- if (path39 === "/watch" || path39 === "/watch/") {
9778
+ if (path41 === "/watch" || path41 === "/watch/") {
9779
9779
  id = query.split("&").find((param) => param.startsWith("v="))?.slice(2);
9780
9780
  } else {
9781
- id = YOUTUBE_ID_PATH.exec(path39)?.[1];
9781
+ id = YOUTUBE_ID_PATH.exec(path41)?.[1];
9782
9782
  }
9783
9783
  }
9784
9784
  return id !== void 0 && YOUTUBE_ID.test(id) ? { provider: "youtube", id } : null;
9785
9785
  }
9786
- var connectionIdParam, connectorIdParam, listConnectionsQuery, deleteConnectionQuery, addConnectionBody, editConnectionBody, PRODUCTION_DIRECT_FIELDS, setProductionCredentialBody, registerWhatsappQuery, registerWhatsappBody, retryProvisioningQuery, resendDomainStatusQuery, initialValueCreateSchema, initialValueUpdateSchema, stateIdParam, listStatesQuery, stateNameSchema, createStateBody, updateStateBody, reorderStatesBody, variantAiModeSchema, experimentStatusSchema, overlayUpdateSchema, experimentIdParam, variantIdParam, listExperimentsQuery, listVariantsQuery, listOverridesQuery, deleteOverrideQuery, createExperimentBody, updateExperimentBody, setExperimentStatusBody, createVariantBody, updateVariantBody, upsertOverrideBody, orgTagIdParam, listOrgTagsQuery, ORG_TAG_NAME_REGEX, ORG_TAG_NAME_MAX_LENGTH, createOrgTagBody, updateOrgTagBody, orgCredentialIdParam, listOrgCredentialsQuery, createOrgCredentialBody, updateOrgCredentialBody, BASE_CREDENTIAL_MANAGED_BY, BASE_CREDENTIAL_NAME_MAX, baseCredentialNameSchema, createBaseCredentialLinkBody, baseCredentialLinkParams, baseCredentialLinkQuery, orgResourceIdParam, orgResourceFileIdParam, orgResourceFolderIdParam, hubOrgResourceParam, listOrgResourcesQuery, orgIdQuery, createOrgResourceBody, updateOrgResourceBody, createOrgResourceFolderBody, updateOrgResourceFolderBody, uploadOrgResourceFileBody, updateOrgResourceFileBody, uploadOrgSkillZipBody, linkOrgResourceBody, resourceIdParam, listResourcesQuery, createResourceBody, updateResourceBody, syncSkillsBody, resourceFileIdParam, listResourceFilesQuery, createResourceFileBody, updateResourceFileBody, uploadResourceFileBody, uploadSkillZipBody, resourceFolderIdParam, listResourceFoldersQuery, createResourceFolderBody, updateResourceFolderBody, agentResourceQuery, hubResourceQuery, linkAgentResourceBody, updateAgentResourceBody, unlinkAgentResourceQuery, navItemSchema, hubCountResetBody, navItemCountResetBody, typingBody, analyticsHubIdParam, analyticsVariableIdParam, updateVariablePinBody, updateAnalyticsViewBody, NUMERIC_FILTER_OPS, TEXT_FILTER_OPS, CATEGORICAL_FILTER_OPS, VARIABLE_FILTER_OPS, STRUCTURED_QUERY_OPS, STRUCTURED_QUERY_AGGREGATIONS, filterValueSchema, conversationsBody, analyticsConversationIdParam, messagesQuery, conversationDetailDataQuery, analyticsDataBody, structuredQueryBody, analyticsSqlBody, analyticsSqlTable, analyticsSqlSchemaQuery, getConversationsQuery, getMessagesQuery, appMessageSenderType, appMessageReceiverType, apiChannelMessage, apiChannelMessageBody, appMessageBody, appMessageResponse, CALL_STATUSES, callStatus, CALL_END_REASONS, callEndReason, CALL_TRANSPORTS, callTransport, CALL_PARTICIPANT_TYPES, callParticipantType, callInstant, MAX_SDP_OFFER_LENGTH, sdpDescription, sdpOffer, MAX_CALL_REQUEST_BODY_BYTES, hubScoped, callSummaryFields, callSummary, callIdParam, createCallBody, createCallResponse, callReadyBody, callHangupBody, callStatusQuery, MAX_REPORTED_DELEGATION_IDS, callDelegationId, callDelegationsBody, callResponse, MAX_CALL_RECORDING_GAPS, callRecordingGap, callRecordingNoteMetadata, evalCallInstant, EVAL_CALL_MIN_SECONDS, EVAL_CALL_MAX_SECONDS, EVAL_CALL_SPOKEN_LINE_MAX_CHARS, hubScoped2, createEvalCallBody, createEvalCallResponse, evalCallConversationQuery, evalCallRecordQuery, evalCallSpeaker, evalCallUtterance, evalCallTurn, evalCallRecording, evalCallRecordResponse, evalCallFinishResponse, updateUserBody, updateProfileBody, pathnameRegex, updatePreferencesBody, registerPushTokenBody, deletePushTokenQuery, recordAiDisclosureAcceptanceBody, activityQuery, deletionModeSchema, deletionRequestBody, CACHE_RECONCILE_MAX_IDS, CACHE_RECONCILE_MAX_HUBS, cacheReconcileScopeSchema, cachedConversationPageSchema, cachedHistoryThreadSchema, cachedPeopleThreadSchema, cacheReconcileBody, archiveHubIdParam, archiveConversationParams, archiveListQuery, archiveMessageObservabilityParams, conversationIdParam, conversationHubQuery, claimBody, releaseBody, transferBody, closeBody, additionalContextPayloadSchema, kanbanUpdateBody, annotateConversationBody, observabilityParams, observabilityListParams, deleteUserConversationsBody, deleteUserConversationsResponse, sendMessageBody, listConversationsQuery, flagSourceSchema, getConversationsResponse, getConversationDetailParams, getConversationDetailQuery, getConversationDetailResponse, listWhatsAppTemplatesQuery, sendWhatsAppTemplateBody, copilotSuggestBody, copilotSuggestResponse, CONSULT_THREAD_STATUSES, consultThreadStatus, CONSULT_THREAD_TITLE_MAX, consultThreadTitle, consultThreadSummary, listConsultThreadsQuery, listConsultThreadsResponse, createConsultThreadBody, createConsultThreadResponse, consultThreadIdParam, updateConsultThreadBody, updateConsultThreadResponse, CONSULT_AWAIT_TIMEOUT_MS, billingOrgIdParam, subscriptionItemParams, spendCapBody, growthPacksBody, portalSessionBody, checkoutSessionBody, updateBillingCurrencyBody, invoicesQuery, planKeySchema, billingStatusSchema, billingPlanTypeSchema, dataUsageCountersSchema, REKOR_ENTITLEMENT_CONTRACT_VERSION, entitlementProjectionSchema, rekorEntitlementRefreshMessageSchema, downloadBody, uploadQuery, SIGN_URL_MAX_EXPIRES_IN_SECONDS, signUrlBody, templateIdParam, listTemplatesQuery, deleteTemplateQuery, templateStatusQuery, createTemplateBody, updateTemplateBody, submitTemplateBody, testTemplateBody, CONTACT_ENVIRONMENTS, CONTACT_NAME_MAX, CONTACT_EMAIL_MAX, CONTACT_PHONE_INPUT_MAX, CONTACT_INSTAGRAM_SID_MAX, CONTACT_METADATA_MAX_BYTES, CONTACT_LABEL_MAX, CONTACT_LABELS_MAX, CONTACT_SCOPE_TAGS_MAX, CONTACT_PAGE_MAX, CONTACT_IMPORT_BATCH_MAX, CONTACT_SEARCH_MAX, CONTACT_LIST_NAME_MAX, CONTACT_LIST_NAME_REGEX, CONTACT_LIST_DESCRIPTION_MAX, CONTACT_LIST_FILTER_MAX, CONTACT_LIST_PINS_PER_REQUEST_MAX, SUPPRESSION_SCOPES, SUPPRESSION_ID_REGEX, contactName, contactPhone, contactEmail, contactInstagramSid, contactLabel, contactLabels, contactMetadata, scopeTagIds, contactVisibilitySchema, hasIdentity, IDENTITY_REQUIRED, contactBookOrgQuery, contactBookContactParam, contactBookListParam, contactBookHubParam, contactBookPageQuery, listContactBookContactsQuery, listHubContactBookContactsQuery, contactBookLinkCandidatesQuery, listContactBookListMembersQuery, contactRowFields, createContactBookContactBody, updateContactBookContactBody, contactImportRowSchema, importContactBookContactsBody, linkContactBookContactBody, listName, contactListFilterSchema, createContactBookListBody, updateContactBookListBody, updateContactBookListPinsBody, suppressionIdentitySchema, createContactBookSuppressionBody, contactBookSuppressionParam, listContactBookSuppressionsQuery, CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH, contactBookEmailUnsubscribeTokenSchema, contactBookEmailUnsubscribeQuery, orgListReferenceSchema, hubListNameSchema, HUB_LIST_FILTER_REQUIRED, hubContactListFilterSchema, hubContactListIdParam, hubContactListHubQuery, description, createHubContactListBody, updateHubContactListBody, AUTOMATION_TRIGGER_TYPES, AUTOMATION_EVENT_TYPES, AUTOMATION_TARGET_TYPES, AUTOMATION_ACTION_TYPES, AUTOMATION_CHANNEL_TYPES, AUTOMATION_PURPOSES, AUTOMATION_NAME_MAX, AUTOMATION_DESCRIPTION_MAX, AUTOMATION_INSTRUCTIONS_MAX, AUTOMATION_TEXT_MAX, AUTOMATION_CRON_MAX, AUTOMATION_WEBHOOK_AUTHS, AUTOMATION_WEBHOOK_HEADERS, AUTOMATION_WEBHOOK_BODY_MAX_BYTES, AUTOMATION_WEBHOOK_HEADER_NAME_MAX, AUTOMATION_WEBHOOK_MOUNT, WEBHOOK_RESERVED_HEADERS, automationTriggerSchema, automationTargetSchema, automationActionSchema, automationGateSchema, automationName, automationDescription, automationIdParam, automationHubQuery, automationWebhookParams, AUTOMATION_RUNS_PAGE_MAX, listAutomationRunsQuery, createAutomationBody, updateAutomationBody, AUTOMATION_ALERT_CATEGORIES, AUTOMATION_BLOCKED_ALERT_CATEGORY, automationEventSubscriberParam, VERSION_PART, MOBILE_APP_VERSION_RE, mobileAppVersionSchema, MOBILE_PLATFORMS, mobilePlatformSchema, mobileVersionPolicyQuery, mobileVersionPolicySchema, mobileVersionPolicyResponse, adminOrgIdParam, adminOrgAdminIdParam, adminUserIdParam, PRICING_PLAN_ID_RE, adminPlanIdParam, adminOrganizationsQuery, adminUserSearchQuery, kanbanSlugMigrationQuery, adjustQuotaBody, updatePlanBody, addAdminUserBody, MAX_VOICE_CALL_MINUTE_OPS, voiceCallMinuteOps, AUTOMATION_GATE_EVALUATIONS_PER_OPERATION_BOUND, automationGateEvaluationsPerOperation, AUTOMATION_RUN_RETENTION_DAYS_MAX, automationRunRetentionDays, updatePlatformConfigBody, updateFreeOrgLimitBody, adminScopedTargetShape, adminScopedTargetBody, BRANCHING_ORPHAN_ENTITY_TYPES, BRANCHING_ORPHAN_MAX_TARGETS, adminRepairBranchingOrphansBody, adminRepairLegacyOrgGrantsBody, adminRepairCompedPlansBody, updatePricingPlanBody, dataExplorerSearchQuery, dataExplorerOrgIdParam, dataExplorerHubIdParam, dataExplorerConvIdParam, dataExplorerDoInstancesQuery, dataExplorerNsIdParam, dataExplorerKvKeysBody, dataExplorerKvKeyBody, uuidOrHexId, dataExplorerDeleteOrgParam, dataExplorerDeleteHubParam, dataExplorerDeleteConvParam, dataExplorerDeleteUserParam, dataExplorerDeleteQuery, dataExplorerKvDeleteBody, pitrDoNamespace, pitrBookmarkParam, pitrRestoreBody, healthSamplingTriggerBody, dataExplorerDebugDoType, DEBUG_READ_MAX_LIMIT, DEBUG_READ_DEFAULT_LIMIT, DEBUG_DO_HEX_PREFIX, debugDoEntityId, debugTableName, dataExplorerDebugTablesParam, dataExplorerDebugRowsParam, dataExplorerDebugRowsQuery, dataExplorerDebugAnalyticsTable, debugUuid, dataExplorerDebugAnalyticsRowsParam, dataExplorerDebugAnalyticsRowsQuery, debugHubConvParam, debugMessageIdQuery, sandboxEgressPolicy, SANDBOX_EXEC_MAX_CMD_LENGTH, SANDBOX_EXEC_MAX_ALLOWLIST, SANDBOX_EXEC_MAX_TIMEOUT_MS, adminSandboxExecBody, META_ENTITY_ID_PATTERN, metaEntityId, whatsappCallbackBody, authMeResponse, wsTicketResponse, sessionCheckResponse, logoutResponse, magicCodeSendBody, magicCodeSendResponse, magicCodeVerifyBody, magicCodeVerifyResponse, passwordLoginBody, stateOperationBody, conversationListResourcesQuery, conversationReadResourceQuery, listPendingSchedulesQuery, listHubAggregateSchedulesQuery, reportSourceSchema, intakeReportSourceSchema, reportClassificationSchema, reportStatusSchema, reportLocaleSchema, reportMessageAuthorRoleSchema, reportMessageSchema, sentryRefStatusSchema, createReportBody, editReportBody, reporterReportSummarySchema, reporterListResponseSchema, getReportResponseSchema, reporterTransitionResponseSchema, contestReportBody, reporterListQuery, listReportsQuery, GITHUB_ISSUE_URL_REGEX, githubIssueUrlSchema, transitionReportBody, groupReportsBody, holdReportBody, clickhouseReadyResponse, bootstrapQuery, MAX_NAME_LENGTH, MAX_VALUE_LENGTH, MAX_TAGS, MAX_TAG_LENGTH, MAX_SCOPE_HUBS, MAX_SCOPE_TAGS, vaultSecretScopeSchema, vaultCreateBody, vaultUpdateBody, MAX_NAME_LENGTH2, MAX_HUBS_PER_GRANT, MAX_HUB_TAGS_PER_GRANT, MAX_GRANTS, permissionEnum, grantScopeSchema, grantSchema, createTokenBody, tokenOrgIdParam, invitePreviewQuery, inviteRolePreviewResponse, inviteContactPreviewResponse, invitePreviewResponse, inviteUnsubscribeBody, jsonSchemaSchema, hubUserIdentity, stateValueEntry, getHubUserContextResponse, updateHubUserBody, updateHubUserResponse, unlockHubUserNameResponse, stateResetParams, stateResetResponse, hubAlertsParam, hubAlertSeverity, hubAlert, hubAlertsListResponse, noticeSeveritySchema, noticeStatusSchema, NOTICE_SCOPE_REGEX, noticeScopeSchema, noticeLinkSchema, createNoticeBody, updateNoticeBody, listNoticesQuery, noticeIdParamSchema, ADMIN_SKILL_NAME_REGEX, adminSkillNameParam, REKOR_ACTOR_TYPE_HEADER, REKOR_ACTOR_ID_HEADER, REKOR_ACTOR_LABEL_HEADER, REKOR_ACTOR_ORG_HEADER, REKOR_ACTOR_ADMIN_HEADER, REKOR_CLIENT_HEADER, REKOR_TOOL_ID_HEADER, REKOR_CLIENTS, rekorClientSchema, MAX_REKOR_TOOL_ID_LENGTH, rekorToolIdSchema, DATA_PROXY_MOUNT, DATA_PROXY_PREFIX, REKOR_V1_PREFIX, DATA_PROXY_ORG_QUERY_PARAM, MAX_PROVISIONING_BODY_BYTES, dataProxyQuery, rekorAttestation, REQUIRED_REKOR_ATTESTATION_HEADERS, BASE_DESTRUCTION_MOUNT, BASE_DESTRUCTION_PREFIX, BASE_DESTRUCTION_CONFIRM_PATH, baseDestructionConfirmBody, BASE_DESTRUCTION_GRACE_MS, BASE_DESTRUCTION_CONFIRM_TTL_MS, BASE_DESTRUCTION_ORG_QUERY_PARAM, baseDestructionQuery, baseDestructionParam, baseDestructionInitiateBody, baseDestructionStatus, baseDestructionRequest, baseDestructionListResponse, baseDestructionInitiateResponse, baseDestructionConfirmResponse, baseDestructionCancelResponse, rekorBaseChangeMessageSchema, PEOPLE_CONTACT_PAGE_MAX, PEOPLE_CONTACT_PAGE_DEFAULT, PEOPLE_CONTACT_EMAIL_MAX_LENGTH, peopleUserIdSchema, peopleInviteEmailSchema, peopleContactInviteBody, peopleContactInviteIdParam, peopleContactUserIdParam, peopleContactBlockBody, peopleContactPageQuery, peopleOutgoingInviteSchema, peopleContactPersonSchema, peopleContactSchema, peopleIncomingInviteSchema, peopleBlockedUserSchema, PEOPLE_THREAD_MESSAGE_MAX_LENGTH, PEOPLE_THREAD_MESSAGES_PAGE_MAX, PEOPLE_THREAD_PAGE_MAX, PEOPLE_THREAD_PAGE_DEFAULT, PEOPLE_GROUP_NAME_MAX_LENGTH, PEOPLE_GROUP_AVATAR_URL_MAX_LENGTH, PEOPLE_THREAD_ATTACHMENT_MAX_BYTES, PEOPLE_THREAD_ATTACHMENT_NAME_MAX_LENGTH, PEOPLE_ATTACHMENT_STORAGE_PER_USER_BYTES, PEOPLE_THREAD_ATTACHMENT_MAX_MB, PEOPLE_THREAD_ATTACHMENT_URL_TTL_SECONDS, PEOPLE_THREAD_ATTACHMENT_URLS_MAX, peopleThreadIdParam, peopleThreadMessageIdParam, peopleThreadListQuery, peopleThreadMessagesQuery, peopleThreadAttachmentUploadSchema, peopleThreadSendBody, peopleThreadAttachmentUrlsBody, peopleThreadReadBody, peopleThreadSettingsBody, peopleGroupNameSchema, peopleGroupAvatarUrlSchema, peopleGroupCreateBody, peopleOrgDirectBody, peopleGroupUpdateBody, peopleGroupInviteBody, peopleOrgErasureBody, peopleGroupLeaveBody, peopleGroupInviteIdParam, peopleGroupIncomingInviteIdParam, peopleGroupMemberParam, peopleThreadKindSchema, peopleThreadMemberSchema, peopleThreadLastMessageSchema, peopleGroupRoleSchema, peopleGroupSummarySchema, peopleThreadSummarySchema, peopleThreadAttachmentSchema, peopleThreadMessageSchema, peopleGroupMemberSchema, peopleGroupSentInviteSchema, peopleGroupIncomingInviteSchema, PEOPLE_REPORT_DETAILS_MAX_LENGTH, peopleReportReasonSchema, reportDetailsSchema, reportFields, peopleReportBody, PEOPLE_MODERATION_NOTE_MAX_LENGTH, ADMIN_PEOPLE_REPORTS_PAGE_MAX, ADMIN_PEOPLE_REPORTS_PAGE_DEFAULT, peopleModerationActionSchema, moderationNoteSchema, adminPeopleReportListQuery, adminPeopleReportIdParam, adminPeopleMessageRemovalBody, adminPeopleSuspendBody, adminPeopleNoteBody, COMMUNITIES_PER_ORG_MAX, COMMUNITY_NAME_MAX_LENGTH, COMMUNITY_REGISTRY_CLOSED_REASON, COMMUNITY_JOIN_SOURCE_MAX_LENGTH, COMMUNITY_JOIN_FLOW_TOKEN_MAX_LENGTH, COMMUNITY_ROOMS_MAX, COMMUNITY_ROOM_NAME_MAX_LENGTH, COMMUNITY_MESSAGE_MAX_LENGTH, COMMUNITY_MESSAGES_PAGE_SIZE, COMMUNITY_ATTACHMENT_URLS_MAX, COMMUNITY_POST_TITLE_MAX_LENGTH, COMMUNITY_POST_SEARCH_MAX_LENGTH, COMMUNITY_MEMBER_STATUSES, COMMUNITY_RULES_MAX_LENGTH, COMMUNITY_SLOW_MODE_MAX_SECONDS, COMMUNITY_COURSES_LABEL_MAX_LENGTH, COMMUNITY_LIVE_TICKET_PREFIX, COMMUNITY_STATS_DAYS, communityNameSchema, adminCreateCommunityBody, communityIdParam, communityJoinTokenSchema, communityJoinSourceSchema, communityJoinPreviewQuery, communityJoinBody, communityRoomKindSchema, communityRoomNameSchema, communityRoomIdParam, communityMessageIdParam, communityRoomCreateBody, communityRoomUpdateBody, communityMessagesQuery, communityMessageBodySchema, communityPostTitleSchema, communityAttachmentUploadSchema, communitySendBody, communityEditBody, communityPostIdParam, communityPostsQuery, communityAnswerBody, communityReadBody, communityAttachmentUrlsBody, communityPersonParam, communityOrgParam, communityPeopleQuery, communityCoursesLabelSchema, communitySettingsBody, communityBanBody, communityLockBody, communityPinBody, communityModLogQuery, communityReportsQuery, communityReportIdParam, communityLiveTicketSchema, communityLiveQuery, communityMuteBody, communityEmailUnsubscribeTokenSchema, COMMUNITY_EMAIL_UNSUBSCRIBE_PATH, communityEmailUnsubscribeQuery, COMMUNITY_COURSE_TITLE_MAX_LENGTH, COMMUNITY_COURSE_DESCRIPTION_MAX_LENGTH, COMMUNITY_LESSON_BODY_MAX_LENGTH, COMMUNITY_LESSON_LINKS_MAX, COMMUNITY_LESSON_URL_MAX_LENGTH, YOUTUBE_ID, YOUTUBE_HOSTS, YOUTU_BE, YOUTUBE_ID_PATH, communityCourseIdParam, communityCourseModuleIdParam, communityLessonIdParam, communityCourseTitleSchema, communityCourseDescriptionSchema, communityCoursePositionSchema, communityLessonVideoUrlSchema, communityLessonLinkSchema, communityLessonLinksSchema, communityLessonBodySchema, communityCourseCreateBody, communityCourseUpdateBody, communityModuleCreateBody, communityModuleUpdateBody, communityLessonCreateBody, communityLessonUpdateBody, COMMUNITY_LESSON_TREE_MAX_BYTES, COMMUNITY_LESSON_LINK_PROTOCOLS, LINK_SCHEME, adminSetGuideHubBody, GUIDE_HUB_INELIGIBLE_REASON;
9787
- var init_chunk_BMAOKAMB = __esm({
9788
- "../../packages/core/dist/chunk-BMAOKAMB.js"() {
9786
+ function communityLessonVideoWatchUrl(video) {
9787
+ switch (video.provider) {
9788
+ case "youtube":
9789
+ return `https://www.youtube.com/watch?v=${video.id}`;
9790
+ }
9791
+ }
9792
+ var connectionIdParam, connectorIdParam, listConnectionsQuery, deleteConnectionQuery, addConnectionBody, editConnectionBody, PRODUCTION_DIRECT_FIELDS, setProductionCredentialBody, registerWhatsappQuery, registerWhatsappBody, retryProvisioningQuery, resendDomainStatusQuery, initialValueCreateSchema, initialValueUpdateSchema, stateIdParam, listStatesQuery, stateNameSchema, createStateBody, updateStateBody, reorderStatesBody, variantAiModeSchema, experimentStatusSchema, overlayUpdateSchema, experimentIdParam, variantIdParam, listExperimentsQuery, listVariantsQuery, listOverridesQuery, deleteOverrideQuery, createExperimentBody, updateExperimentBody, setExperimentStatusBody, createVariantBody, updateVariantBody, upsertOverrideBody, orgTagIdParam, listOrgTagsQuery, ORG_TAG_NAME_REGEX, ORG_TAG_NAME_MAX_LENGTH, createOrgTagBody, updateOrgTagBody, orgCredentialIdParam, listOrgCredentialsQuery, createOrgCredentialBody, updateOrgCredentialBody, BASE_CREDENTIAL_MANAGED_BY, BASE_CREDENTIAL_NAME_MAX, baseCredentialNameSchema, createBaseCredentialLinkBody, baseCredentialLinkParams, baseCredentialLinkQuery, orgResourceIdParam, orgResourceFileIdParam, orgResourceFolderIdParam, hubOrgResourceParam, listOrgResourcesQuery, orgIdQuery, createOrgResourceBody, updateOrgResourceBody, createOrgResourceFolderBody, updateOrgResourceFolderBody, uploadOrgResourceFileBody, updateOrgResourceFileBody, uploadOrgSkillZipBody, linkOrgResourceBody, resourceIdParam, listResourcesQuery, createResourceBody, updateResourceBody, syncSkillsBody, resourceFileIdParam, listResourceFilesQuery, createResourceFileBody, updateResourceFileBody, uploadResourceFileBody, uploadSkillZipBody, resourceFolderIdParam, listResourceFoldersQuery, createResourceFolderBody, updateResourceFolderBody, agentResourceQuery, hubResourceQuery, linkAgentResourceBody, updateAgentResourceBody, unlinkAgentResourceQuery, navItemSchema, hubCountResetBody, navItemCountResetBody, typingBody, analyticsHubIdParam, analyticsVariableIdParam, updateVariablePinBody, updateAnalyticsViewBody, NUMERIC_FILTER_OPS, TEXT_FILTER_OPS, CATEGORICAL_FILTER_OPS, VARIABLE_FILTER_OPS, STRUCTURED_QUERY_OPS, STRUCTURED_QUERY_AGGREGATIONS, filterValueSchema, conversationsBody, analyticsConversationIdParam, messagesQuery, conversationDetailDataQuery, analyticsDataBody, structuredQueryBody, analyticsSqlBody, analyticsSqlTable, analyticsSqlSchemaQuery, getConversationsQuery, getMessagesQuery, appMessageSenderType, appMessageReceiverType, apiChannelMessage, apiChannelMessageBody, appMessageBody, appMessageResponse, CALL_STATUSES, callStatus, CALL_END_REASONS, callEndReason, CALL_TRANSPORTS, callTransport, CALL_PARTICIPANT_TYPES, callParticipantType, callInstant, MAX_SDP_OFFER_LENGTH, sdpDescription, sdpOffer, MAX_CALL_REQUEST_BODY_BYTES, hubScoped, callSummaryFields, callSummary, callIdParam, createCallBody, createCallResponse, callReadyBody, callHangupBody, callStatusQuery, MAX_REPORTED_DELEGATION_IDS, callDelegationId, callDelegationsBody, callResponse, MAX_CALL_RECORDING_GAPS, callRecordingGap, callRecordingNoteMetadata, evalCallInstant, EVAL_CALL_MIN_SECONDS, EVAL_CALL_MAX_SECONDS, EVAL_CALL_SPOKEN_LINE_MAX_CHARS, hubScoped2, createEvalCallBody, createEvalCallResponse, evalCallConversationQuery, evalCallRecordQuery, evalCallSpeaker, evalCallUtterance, evalCallTurn, evalCallRecording, evalCallRecordResponse, evalCallFinishResponse, updateUserBody, updateProfileBody, pathnameRegex, updatePreferencesBody, registerPushTokenBody, deletePushTokenQuery, recordAiDisclosureAcceptanceBody, activityQuery, deletionModeSchema, deletionRequestBody, CACHE_RECONCILE_MAX_IDS, CACHE_RECONCILE_MAX_HUBS, cacheReconcileScopeSchema, cachedConversationPageSchema, cachedHistoryThreadSchema, cachedPeopleThreadSchema, cacheReconcileBody, archiveHubIdParam, archiveConversationParams, archiveListQuery, archiveMessageObservabilityParams, conversationIdParam, conversationHubQuery, claimBody, releaseBody, transferBody, closeBody, additionalContextPayloadSchema, kanbanUpdateBody, annotateConversationBody, observabilityParams, observabilityListParams, deleteUserConversationsBody, deleteUserConversationsResponse, sendMessageBody, listConversationsQuery, flagSourceSchema, getConversationsResponse, getConversationDetailParams, getConversationDetailQuery, getConversationDetailResponse, listWhatsAppTemplatesQuery, sendWhatsAppTemplateBody, copilotSuggestBody, copilotSuggestResponse, CONSULT_THREAD_STATUSES, consultThreadStatus, CONSULT_THREAD_TITLE_MAX, consultThreadTitle, consultThreadSummary, listConsultThreadsQuery, listConsultThreadsResponse, createConsultThreadBody, createConsultThreadResponse, consultThreadIdParam, updateConsultThreadBody, updateConsultThreadResponse, CONSULT_AWAIT_TIMEOUT_MS, billingOrgIdParam, subscriptionItemParams, spendCapBody, growthPacksBody, portalSessionBody, checkoutSessionBody, updateBillingCurrencyBody, invoicesQuery, planKeySchema, billingStatusSchema, billingPlanTypeSchema, dataUsageCountersSchema, REKOR_ENTITLEMENT_CONTRACT_VERSION, entitlementProjectionSchema, rekorEntitlementRefreshMessageSchema, downloadBody, uploadQuery, SIGN_URL_MAX_EXPIRES_IN_SECONDS, signUrlBody, templateIdParam, listTemplatesQuery, deleteTemplateQuery, templateStatusQuery, createTemplateBody, updateTemplateBody, submitTemplateBody, testTemplateBody, CONTACT_ENVIRONMENTS, CONTACT_NAME_MAX, CONTACT_EMAIL_MAX, CONTACT_PHONE_INPUT_MAX, CONTACT_INSTAGRAM_SID_MAX, CONTACT_METADATA_MAX_BYTES, CONTACT_LABEL_MAX, CONTACT_LABELS_MAX, CONTACT_SCOPE_TAGS_MAX, CONTACT_PAGE_MAX, CONTACT_IMPORT_BATCH_MAX, CONTACT_SEARCH_MAX, CONTACT_LIST_NAME_MAX, CONTACT_LIST_NAME_REGEX, CONTACT_LIST_DESCRIPTION_MAX, CONTACT_LIST_FILTER_MAX, CONTACT_LIST_PINS_PER_REQUEST_MAX, SUPPRESSION_SCOPES, SUPPRESSION_ID_REGEX, contactName, contactPhone, contactEmail, contactInstagramSid, contactLabel, contactLabels, contactMetadata, scopeTagIds, contactVisibilitySchema, hasIdentity, IDENTITY_REQUIRED, contactBookOrgQuery, contactBookContactParam, contactBookListParam, contactBookHubParam, contactBookPageQuery, listContactBookContactsQuery, listHubContactBookContactsQuery, contactBookLinkCandidatesQuery, listContactBookListMembersQuery, contactRowFields, createContactBookContactBody, updateContactBookContactBody, contactImportRowSchema, importContactBookContactsBody, linkContactBookContactBody, listName, contactListFilterSchema, createContactBookListBody, updateContactBookListBody, updateContactBookListPinsBody, suppressionIdentitySchema, createContactBookSuppressionBody, contactBookSuppressionParam, listContactBookSuppressionsQuery, CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH, contactBookEmailUnsubscribeTokenSchema, contactBookEmailUnsubscribeQuery, orgListReferenceSchema, hubListNameSchema, HUB_LIST_FILTER_REQUIRED, hubContactListFilterSchema, hubContactListIdParam, hubContactListHubQuery, description, createHubContactListBody, updateHubContactListBody, AUTOMATION_TRIGGER_TYPES, AUTOMATION_EVENT_TYPES, AUTOMATION_TARGET_TYPES, AUTOMATION_ACTION_TYPES, AUTOMATION_CHANNEL_TYPES, AUTOMATION_PURPOSES, AUTOMATION_NAME_MAX, AUTOMATION_DESCRIPTION_MAX, AUTOMATION_INSTRUCTIONS_MAX, AUTOMATION_TEXT_MAX, AUTOMATION_CRON_MAX, AUTOMATION_WEBHOOK_AUTHS, AUTOMATION_WEBHOOK_HEADERS, AUTOMATION_WEBHOOK_BODY_MAX_BYTES, AUTOMATION_WEBHOOK_HEADER_NAME_MAX, AUTOMATION_WEBHOOK_MOUNT, WEBHOOK_RESERVED_HEADERS, automationTriggerSchema, automationTargetSchema, automationActionSchema, automationGateSchema, automationName, automationDescription, automationIdParam, automationHubQuery, automationWebhookParams, AUTOMATION_RUNS_PAGE_MAX, listAutomationRunsQuery, createAutomationBody, updateAutomationBody, AUTOMATION_ALERT_CATEGORIES, AUTOMATION_BLOCKED_ALERT_CATEGORY, automationEventSubscriberParam, VERSION_PART, MOBILE_APP_VERSION_RE, mobileAppVersionSchema, MOBILE_PLATFORMS, mobilePlatformSchema, mobileVersionPolicyQuery, mobileVersionPolicySchema, mobileVersionPolicyResponse, adminOrgIdParam, adminOrgAdminIdParam, adminUserIdParam, PRICING_PLAN_ID_RE, adminPlanIdParam, adminOrganizationsQuery, adminUserSearchQuery, kanbanSlugMigrationQuery, adjustQuotaBody, updatePlanBody, addAdminUserBody, MAX_VOICE_CALL_MINUTE_OPS, voiceCallMinuteOps, AUTOMATION_GATE_EVALUATIONS_PER_OPERATION_BOUND, automationGateEvaluationsPerOperation, AUTOMATION_RUN_RETENTION_DAYS_MAX, automationRunRetentionDays, updatePlatformConfigBody, updateFreeOrgLimitBody, adminScopedTargetShape, adminScopedTargetBody, BRANCHING_ORPHAN_ENTITY_TYPES, BRANCHING_ORPHAN_MAX_TARGETS, adminRepairBranchingOrphansBody, adminRepairLegacyOrgGrantsBody, adminRepairCompedPlansBody, updatePricingPlanBody, dataExplorerSearchQuery, dataExplorerOrgIdParam, dataExplorerHubIdParam, dataExplorerConvIdParam, dataExplorerDoInstancesQuery, dataExplorerNsIdParam, dataExplorerKvKeysBody, dataExplorerKvKeyBody, uuidOrHexId, dataExplorerDeleteOrgParam, dataExplorerDeleteHubParam, dataExplorerDeleteConvParam, dataExplorerDeleteUserParam, dataExplorerDeleteQuery, dataExplorerKvDeleteBody, pitrDoNamespace, pitrBookmarkParam, pitrRestoreBody, healthSamplingTriggerBody, dataExplorerDebugDoType, DEBUG_READ_MAX_LIMIT, DEBUG_READ_DEFAULT_LIMIT, DEBUG_DO_HEX_PREFIX, debugDoEntityId, debugTableName, dataExplorerDebugTablesParam, dataExplorerDebugRowsParam, dataExplorerDebugRowsQuery, dataExplorerDebugAnalyticsTable, debugUuid, dataExplorerDebugAnalyticsRowsParam, dataExplorerDebugAnalyticsRowsQuery, debugHubConvParam, debugMessageIdQuery, sandboxEgressPolicy, SANDBOX_EXEC_MAX_CMD_LENGTH, SANDBOX_EXEC_MAX_ALLOWLIST, SANDBOX_EXEC_MAX_TIMEOUT_MS, adminSandboxExecBody, META_ENTITY_ID_PATTERN, metaEntityId, whatsappCallbackBody, authMeResponse, wsTicketResponse, sessionCheckResponse, logoutResponse, magicCodeSendBody, magicCodeSendResponse, magicCodeVerifyBody, magicCodeVerifyResponse, passwordLoginBody, stateOperationBody, conversationListResourcesQuery, conversationReadResourceQuery, listPendingSchedulesQuery, listHubAggregateSchedulesQuery, reportSourceSchema, intakeReportSourceSchema, reportClassificationSchema, reportStatusSchema, reportLocaleSchema, reportMessageAuthorRoleSchema, reportMessageSchema, sentryRefStatusSchema, createReportBody, editReportBody, reporterReportSummarySchema, reporterListResponseSchema, getReportResponseSchema, reporterTransitionResponseSchema, contestReportBody, reporterListQuery, listReportsQuery, GITHUB_ISSUE_URL_REGEX, githubIssueUrlSchema, transitionReportBody, groupReportsBody, holdReportBody, clickhouseReadyResponse, bootstrapQuery, MAX_NAME_LENGTH, MAX_VALUE_LENGTH, MAX_TAGS, MAX_TAG_LENGTH, MAX_SCOPE_HUBS, MAX_SCOPE_TAGS, vaultSecretScopeSchema, vaultCreateBody, vaultUpdateBody, MAX_NAME_LENGTH2, MAX_HUBS_PER_GRANT, MAX_HUB_TAGS_PER_GRANT, MAX_GRANTS, permissionEnum, grantScopeSchema, grantSchema, createTokenBody, tokenOrgIdParam, invitePreviewQuery, inviteRolePreviewResponse, inviteContactPreviewResponse, invitePreviewResponse, inviteUnsubscribeBody, jsonSchemaSchema, hubUserIdentity, stateValueEntry, getHubUserContextResponse, updateHubUserBody, updateHubUserResponse, unlockHubUserNameResponse, stateResetParams, stateResetResponse, hubAlertsParam, hubAlertSeverity, hubAlert, hubAlertsListResponse, noticeSeveritySchema, noticeStatusSchema, NOTICE_SCOPE_REGEX, noticeScopeSchema, noticeLinkSchema, createNoticeBody, updateNoticeBody, listNoticesQuery, noticeIdParamSchema, ADMIN_SKILL_NAME_REGEX, adminSkillNameParam, REKOR_ACTOR_TYPE_HEADER, REKOR_ACTOR_ID_HEADER, REKOR_ACTOR_LABEL_HEADER, REKOR_ACTOR_ORG_HEADER, REKOR_ACTOR_ADMIN_HEADER, REKOR_CLIENT_HEADER, REKOR_TOOL_ID_HEADER, REKOR_CLIENTS, rekorClientSchema, MAX_REKOR_TOOL_ID_LENGTH, rekorToolIdSchema, DATA_PROXY_MOUNT, DATA_PROXY_PREFIX, REKOR_V1_PREFIX, DATA_PROXY_ORG_QUERY_PARAM, MAX_PROVISIONING_BODY_BYTES, dataProxyQuery, rekorAttestation, REQUIRED_REKOR_ATTESTATION_HEADERS, BASE_DESTRUCTION_MOUNT, BASE_DESTRUCTION_PREFIX, BASE_DESTRUCTION_CONFIRM_PATH, baseDestructionConfirmBody, BASE_DESTRUCTION_GRACE_MS, BASE_DESTRUCTION_CONFIRM_TTL_MS, BASE_DESTRUCTION_ORG_QUERY_PARAM, baseDestructionQuery, baseDestructionParam, baseDestructionInitiateBody, baseDestructionStatus, baseDestructionRequest, baseDestructionListResponse, baseDestructionInitiateResponse, baseDestructionConfirmResponse, baseDestructionCancelResponse, rekorBaseChangeMessageSchema, PEOPLE_CONTACT_PAGE_MAX, PEOPLE_CONTACT_PAGE_DEFAULT, PEOPLE_CONTACT_EMAIL_MAX_LENGTH, peopleUserIdSchema, peopleInviteEmailSchema, peopleContactInviteBody, peopleContactInviteIdParam, peopleContactUserIdParam, peopleContactBlockBody, peopleContactPageQuery, peopleOutgoingInviteSchema, peopleContactPersonSchema, peopleContactSchema, peopleIncomingInviteSchema, peopleBlockedUserSchema, PEOPLE_THREAD_MESSAGE_MAX_LENGTH, PEOPLE_THREAD_MESSAGES_PAGE_MAX, PEOPLE_THREAD_PAGE_MAX, PEOPLE_THREAD_PAGE_DEFAULT, PEOPLE_GROUP_NAME_MAX_LENGTH, PEOPLE_GROUP_AVATAR_URL_MAX_LENGTH, PEOPLE_THREAD_ATTACHMENT_MAX_BYTES, PEOPLE_THREAD_ATTACHMENT_NAME_MAX_LENGTH, PEOPLE_ATTACHMENT_STORAGE_PER_USER_BYTES, PEOPLE_THREAD_ATTACHMENT_MAX_MB, PEOPLE_THREAD_ATTACHMENT_URL_TTL_SECONDS, PEOPLE_THREAD_ATTACHMENT_URLS_MAX, peopleThreadIdParam, peopleThreadMessageIdParam, peopleThreadListQuery, peopleThreadMessagesQuery, peopleThreadAttachmentUploadSchema, peopleThreadSendBody, peopleThreadAttachmentUrlsBody, peopleThreadReadBody, peopleThreadSettingsBody, peopleGroupNameSchema, peopleGroupAvatarUrlSchema, peopleGroupCreateBody, peopleOrgDirectBody, peopleGroupUpdateBody, peopleGroupInviteBody, peopleOrgErasureBody, peopleGroupLeaveBody, peopleGroupInviteIdParam, peopleGroupIncomingInviteIdParam, peopleGroupMemberParam, peopleThreadKindSchema, peopleThreadMemberSchema, peopleThreadLastMessageSchema, peopleGroupRoleSchema, peopleGroupSummarySchema, peopleThreadSummarySchema, peopleThreadAttachmentSchema, peopleThreadMessageSchema, peopleGroupMemberSchema, peopleGroupSentInviteSchema, peopleGroupIncomingInviteSchema, PEOPLE_REPORT_DETAILS_MAX_LENGTH, peopleReportReasonSchema, reportDetailsSchema, reportFields, peopleReportBody, PEOPLE_MODERATION_NOTE_MAX_LENGTH, ADMIN_PEOPLE_REPORTS_PAGE_MAX, ADMIN_PEOPLE_REPORTS_PAGE_DEFAULT, peopleModerationActionSchema, moderationNoteSchema, adminPeopleReportListQuery, adminPeopleReportIdParam, adminPeopleMessageRemovalBody, adminPeopleSuspendBody, adminPeopleNoteBody, COMMUNITIES_PER_ORG_MAX, COMMUNITY_NAME_MAX_LENGTH, COMMUNITY_REGISTRY_CLOSED_REASON, COMMUNITY_ADMISSIONS, COMMUNITY_ACCESS_ADD_MAX, COMMUNITY_JOIN_SOURCE_MAX_LENGTH, COMMUNITY_JOIN_FLOW_TOKEN_MAX_LENGTH, COMMUNITY_ROOMS_MAX, COMMUNITY_ROOM_NAME_MAX_LENGTH, COMMUNITY_MESSAGE_MAX_LENGTH, COMMUNITY_MESSAGES_PAGE_SIZE, COMMUNITY_ATTACHMENT_URLS_MAX, COMMUNITY_POST_TITLE_MAX_LENGTH, COMMUNITY_POST_SEARCH_MAX_LENGTH, COMMUNITY_MEMBER_STATUSES, COMMUNITY_RULES_MAX_LENGTH, COMMUNITY_SLOW_MODE_MAX_SECONDS, COMMUNITY_COURSES_LABEL_MAX_LENGTH, COMMUNITY_LIVE_TICKET_PREFIX, COMMUNITY_STATS_DAYS, communityNameSchema, adminCreateCommunityBody, communityIdParam, communityJoinTokenSchema, communityJoinSourceSchema, communityJoinPreviewQuery, communityJoinBody, communityRoomKindSchema, communityRoomNameSchema, communityRoomIdParam, communityMessageIdParam, communityRoomCreateBody, communityRoomUpdateBody, communityMessagesQuery, communityMessageBodySchema, communityPostTitleSchema, communityAttachmentUploadSchema, communitySendBody, communityEditBody, communityPostIdParam, communityPostsQuery, communityAnswerBody, communityReadBody, communityAttachmentUrlsBody, communityPersonParam, communityOrgParam, communityPeopleQuery, communityCoursesLabelSchema, communityAdmissionSchema, communitySettingsBody, communityAccessEmailSchema, communityAccessAddBody, communityAccessQuery, communityAccessIdParam, communityBanBody, communityLockBody, communityPinBody, communityModLogQuery, communityReportsQuery, communityReportIdParam, communityLiveTicketSchema, communityLiveQuery, communityMuteBody, communityEmailUnsubscribeTokenSchema, COMMUNITY_EMAIL_UNSUBSCRIBE_PATH, communityEmailUnsubscribeQuery, COMMUNITY_COURSES_MAX, COMMUNITY_COURSE_MODULES_MAX, COMMUNITY_COURSE_LESSONS_MAX, COMMUNITY_LESSONS_MAX, COMMUNITY_COURSE_TITLE_MAX_LENGTH, COMMUNITY_COURSE_DESCRIPTION_MAX_LENGTH, COMMUNITY_LESSON_BODY_MAX_LENGTH, COMMUNITY_LESSON_LINKS_MAX, COMMUNITY_LESSON_URL_MAX_LENGTH, YOUTUBE_ID, YOUTUBE_HOSTS, YOUTU_BE, YOUTUBE_ID_PATH, communityCourseIdParam, communityCourseModuleIdParam, communityLessonIdParam, communityCourseTitleSchema, communityCourseDescriptionSchema, communityCoursePositionSchema, communityLessonVideoUrlSchema, communityLessonLinkSchema, communityLessonLinksSchema, communityLessonBodySchema, communityCourseCreateBody, communityCourseUpdateBody, communityModuleCreateBody, communityModuleUpdateBody, communityLessonCreateBody, communityLessonUpdateBody, COMMUNITY_LESSON_TREE_MAX_BYTES, COMMUNITY_LESSON_LINK_PROTOCOLS, COMMUNITY_LESSON_TREE_TOO_LARGE_REASON, COMMUNITY_LESSON_TREE_TOO_DEEP_REASON, COMMUNITY_LESSON_LINE_PREFIX_TOO_WIDE_REASON, LINK_SCHEME, adminSetGuideHubBody, GUIDE_HUB_INELIGIBLE_REASON;
9793
+ var init_chunk_T3VB6VFG = __esm({
9794
+ "../../packages/core/dist/chunk-T3VB6VFG.js"() {
9789
9795
  "use strict";
9790
9796
  init_chunk_VORPHGDT();
9791
9797
  init_chunk_POL7B3EM();
@@ -10846,7 +10852,8 @@ var init_chunk_BMAOKAMB = __esm({
10846
10852
  });
10847
10853
  updateUserBody = external_exports.object({
10848
10854
  user_language: external_exports.string().optional(),
10849
- ui_dark_mode: external_exports.boolean().optional(),
10855
+ /** true: dark; false: light; null: follow each device's own setting (`AppearanceMode`). */
10856
+ ui_dark_mode: external_exports.boolean().nullable().optional(),
10850
10857
  profile_name: external_exports.string().optional(),
10851
10858
  link_email_to_identities: external_exports.boolean().optional()
10852
10859
  }).refine(
@@ -12812,6 +12819,8 @@ var init_chunk_BMAOKAMB = __esm({
12812
12819
  COMMUNITIES_PER_ORG_MAX = 10;
12813
12820
  COMMUNITY_NAME_MAX_LENGTH = 100;
12814
12821
  COMMUNITY_REGISTRY_CLOSED_REASON = "community_registry_closed";
12822
+ COMMUNITY_ADMISSIONS = ["link", "invite"];
12823
+ COMMUNITY_ACCESS_ADD_MAX = 500;
12815
12824
  COMMUNITY_JOIN_SOURCE_MAX_LENGTH = 32;
12816
12825
  COMMUNITY_JOIN_FLOW_TOKEN_MAX_LENGTH = 128;
12817
12826
  COMMUNITY_ROOMS_MAX = 20;
@@ -12872,11 +12881,20 @@ var init_chunk_BMAOKAMB = __esm({
12872
12881
  communityOrgParam = external_exports.object({ orgId: uuidSchema });
12873
12882
  communityPeopleQuery = external_exports.object({ after: peopleUserIdSchema.optional() });
12874
12883
  communityCoursesLabelSchema = external_exports.string().trim().max(COMMUNITY_COURSES_LABEL_MAX_LENGTH);
12884
+ communityAdmissionSchema = external_exports.enum(COMMUNITY_ADMISSIONS);
12875
12885
  communitySettingsBody = external_exports.object({
12876
12886
  rules: external_exports.string().trim().max(COMMUNITY_RULES_MAX_LENGTH).nullable().optional().transform((rules) => rules === "" ? null : rules),
12877
12887
  slow_mode_seconds: external_exports.number().int().min(0).max(COMMUNITY_SLOW_MODE_MAX_SECONDS).optional(),
12878
- courses_label: communityCoursesLabelSchema.optional()
12879
- }).strict().refine((b) => b.rules !== void 0 || b.slow_mode_seconds !== void 0 || b.courses_label !== void 0, "nothing to change");
12888
+ courses_label: communityCoursesLabelSchema.optional(),
12889
+ admission: communityAdmissionSchema.optional()
12890
+ }).strict().refine(
12891
+ (b) => b.rules !== void 0 || b.slow_mode_seconds !== void 0 || b.courses_label !== void 0 || b.admission !== void 0,
12892
+ "nothing to change"
12893
+ );
12894
+ communityAccessEmailSchema = external_exports.string().trim().toLowerCase().max(CONTACT_EMAIL_MAX).email();
12895
+ communityAccessAddBody = external_exports.object({ emails: external_exports.array(communityAccessEmailSchema).min(1).max(COMMUNITY_ACCESS_ADD_MAX) }).strict();
12896
+ communityAccessQuery = external_exports.object({ cursor: external_exports.string().min(1).max(256).optional() });
12897
+ communityAccessIdParam = external_exports.object({ communityId: uuidSchema, accessId: uuidSchema });
12880
12898
  communityBanBody = external_exports.object({ delete_messages: external_exports.boolean().optional() }).strict();
12881
12899
  communityLockBody = external_exports.object({ locked: external_exports.boolean() }).strict();
12882
12900
  communityPinBody = external_exports.object({ pinned: external_exports.boolean() }).strict();
@@ -12891,6 +12909,10 @@ var init_chunk_BMAOKAMB = __esm({
12891
12909
  communityEmailUnsubscribeTokenSchema = external_exports.string().max(200).regex(/^[0-9a-f-]{36}\.[A-Za-z0-9_-]{1,64}\.[0-9a-f]{64}$/i);
12892
12910
  COMMUNITY_EMAIL_UNSUBSCRIBE_PATH = "/api/communities/email-unsubscribe";
12893
12911
  communityEmailUnsubscribeQuery = external_exports.object({ token: communityEmailUnsubscribeTokenSchema });
12912
+ COMMUNITY_COURSES_MAX = 20;
12913
+ COMMUNITY_COURSE_MODULES_MAX = 20;
12914
+ COMMUNITY_COURSE_LESSONS_MAX = 100;
12915
+ COMMUNITY_LESSONS_MAX = 500;
12894
12916
  COMMUNITY_COURSE_TITLE_MAX_LENGTH = 200;
12895
12917
  COMMUNITY_COURSE_DESCRIPTION_MAX_LENGTH = 2e3;
12896
12918
  COMMUNITY_LESSON_BODY_MAX_LENGTH = 2e4;
@@ -12951,6 +12973,9 @@ var init_chunk_BMAOKAMB = __esm({
12951
12973
  }).strict().refine((b) => Object.values(b).some((value) => value !== void 0), "nothing to change");
12952
12974
  COMMUNITY_LESSON_TREE_MAX_BYTES = 512 * 1024;
12953
12975
  COMMUNITY_LESSON_LINK_PROTOCOLS = ["http", "https", "mailto"];
12976
+ COMMUNITY_LESSON_TREE_TOO_LARGE_REASON = "lesson_tree_too_large";
12977
+ COMMUNITY_LESSON_TREE_TOO_DEEP_REASON = "lesson_tree_too_deep";
12978
+ COMMUNITY_LESSON_LINE_PREFIX_TOO_WIDE_REASON = "lesson_line_prefix_too_wide";
12954
12979
  LINK_SCHEME = new RegExp(`^(${COMMUNITY_LESSON_LINK_PROTOCOLS.join("|")}):`, "i");
12955
12980
  adminSetGuideHubBody = external_exports.object({
12956
12981
  hub_id: uuidSchema,
@@ -13262,7 +13287,7 @@ var init_chunk_BPDC67ML = __esm({
13262
13287
  var init_contracts = __esm({
13263
13288
  "../../packages/core/dist/contracts/index.js"() {
13264
13289
  "use strict";
13265
- init_chunk_BMAOKAMB();
13290
+ init_chunk_T3VB6VFG();
13266
13291
  init_chunk_VORPHGDT();
13267
13292
  init_chunk_POL7B3EM();
13268
13293
  init_chunk_46WU2TD4();
@@ -13304,10 +13329,10 @@ function toDataProxyPath(v1Path) {
13304
13329
  }
13305
13330
  return `${DATA_PROXY_PREFIX}${v1Path.slice(REKOR_V1_PREFIX.length)}`;
13306
13331
  }
13307
- function withOrgSelector(path39, orgId) {
13308
- if (!orgId) return path39;
13309
- const separator = path39.includes("?") ? "&" : "?";
13310
- return `${path39}${separator}${DATA_PROXY_ORG_QUERY_PARAM}=${encodeURIComponent(orgId)}`;
13332
+ function withOrgSelector(path41, orgId) {
13333
+ if (!orgId) return path41;
13334
+ const separator = path41.includes("?") ? "&" : "?";
13335
+ return `${path41}${separator}${DATA_PROXY_ORG_QUERY_PARAM}=${encodeURIComponent(orgId)}`;
13311
13336
  }
13312
13337
  function dataErrorMessage(err) {
13313
13338
  if (!(err instanceof ApiError)) return null;
@@ -13323,7 +13348,7 @@ function dataErrorMessage(err) {
13323
13348
  const message = parsed?.error?.message;
13324
13349
  return typeof message === "string" && message ? message : null;
13325
13350
  }
13326
- var RETRYABLE_BACKOFF_MS, delay, ApiError, ApiClient;
13351
+ var RETRYABLE_BACKOFF_MS, delay, courseAuthoring, ApiError, ApiClient;
13327
13352
  var init_api_client = __esm({
13328
13353
  "src/lib/api-client.ts"() {
13329
13354
  "use strict";
@@ -13332,6 +13357,7 @@ var init_api_client = __esm({
13332
13357
  init_mask_secrets();
13333
13358
  RETRYABLE_BACKOFF_MS = [500, 1e3, 2e3];
13334
13359
  delay = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
13360
+ courseAuthoring = (communityId, courseId) => `/api/communities/${encodeURIComponent(communityId)}/authoring/courses${courseId === void 0 ? "" : `/${encodeURIComponent(courseId)}`}`;
13335
13361
  ApiError = class extends Error {
13336
13362
  status;
13337
13363
  body;
@@ -13341,13 +13367,13 @@ var init_api_client = __esm({
13341
13367
  * telling them apart by body shape misreads one for the other.
13342
13368
  */
13343
13369
  path;
13344
- constructor(method, path39, status, body) {
13370
+ constructor(method, path41, status, body) {
13345
13371
  const safeBody = maskSecretsInMessage(body);
13346
- super(`API request failed: ${method} ${path39} (${status}): ${safeBody}`);
13372
+ super(`API request failed: ${method} ${path41} (${status}): ${safeBody}`);
13347
13373
  this.name = "ApiError";
13348
13374
  this.status = status;
13349
13375
  this.body = safeBody;
13350
- this.path = path39;
13376
+ this.path = path41;
13351
13377
  }
13352
13378
  /** True when the status code is a 4xx client error (expected user-facing condition, not a bug). */
13353
13379
  get isExpected() {
@@ -13491,6 +13517,33 @@ var init_api_client = __esm({
13491
13517
  body
13492
13518
  );
13493
13519
  }
13520
+ // A community's courses, which `wayai pull`/`push communities/<x>` sync (gh #5003): every course,
13521
+ // drafts included; a course whole in one read (its export); and the authoring writes. A create
13522
+ // with a `client_id` already used answers the course, module or lesson made then, and makes none.
13523
+ async listCommunityCourseDrafts(communityId) {
13524
+ return this.request("GET", courseAuthoring(communityId));
13525
+ }
13526
+ async exportCommunityCourse(communityId, courseId) {
13527
+ return this.request("GET", `${courseAuthoring(communityId, courseId)}/export`);
13528
+ }
13529
+ async createCommunityCourse(communityId, body) {
13530
+ return this.request("POST", courseAuthoring(communityId), body);
13531
+ }
13532
+ async updateCommunityCourse(communityId, courseId, body) {
13533
+ return this.request("PATCH", courseAuthoring(communityId, courseId), body);
13534
+ }
13535
+ async createCommunityModule(communityId, courseId, body) {
13536
+ return this.request("POST", `${courseAuthoring(communityId, courseId)}/modules`, body);
13537
+ }
13538
+ async updateCommunityModule(communityId, courseId, moduleId, body) {
13539
+ return this.request("PATCH", `${courseAuthoring(communityId, courseId)}/modules/${encodeURIComponent(moduleId)}`, body);
13540
+ }
13541
+ async createCommunityLesson(communityId, courseId, body) {
13542
+ return this.request("POST", `${courseAuthoring(communityId, courseId)}/lessons`, body);
13543
+ }
13544
+ async updateCommunityLesson(communityId, courseId, lessonId, body) {
13545
+ return this.request("PATCH", `${courseAuthoring(communityId, courseId)}/lessons/${encodeURIComponent(lessonId)}`, body);
13546
+ }
13494
13547
  // Read which hub is the WayAI Guide (platform-admin gated, wayai#4948). Used by
13495
13548
  // `wayai admin guide status`.
13496
13549
  async adminGetGuide() {
@@ -13519,8 +13572,8 @@ var init_api_client = __esm({
13519
13572
  ...opts?.check && { check: true }
13520
13573
  });
13521
13574
  }
13522
- async lookup(path39, opts) {
13523
- const params = new URLSearchParams({ path: path39 });
13575
+ async lookup(path41, opts) {
13576
+ const params = new URLSearchParams({ path: path41 });
13524
13577
  if (opts?.organizationId) params.set("organization_id", opts.organizationId);
13525
13578
  return this.request("GET", `/api/ci/lookup?${params.toString()}`);
13526
13579
  }
@@ -14028,9 +14081,9 @@ var init_api_client = __esm({
14028
14081
  * sandbox for this conversation, or the blob was purged).
14029
14082
  */
14030
14083
  async downloadArchiveSandboxFs(hubId, conversationId) {
14031
- const path39 = `/api/archive/${encodeURIComponent(hubId)}/conversations/${encodeURIComponent(conversationId)}/sandbox`;
14032
- addApiBreadcrumb("GET", path39);
14033
- const url = `${this.apiUrl}${path39}`;
14084
+ const path41 = `/api/archive/${encodeURIComponent(hubId)}/conversations/${encodeURIComponent(conversationId)}/sandbox`;
14085
+ addApiBreadcrumb("GET", path41);
14086
+ const url = `${this.apiUrl}${path41}`;
14034
14087
  let response = await this.send(url, "GET");
14035
14088
  if (response.status === 401 && this.onUnauthorized) {
14036
14089
  let refreshed;
@@ -14044,7 +14097,7 @@ var init_api_client = __esm({
14044
14097
  }
14045
14098
  }
14046
14099
  if (!response.ok) {
14047
- throw new ApiError("GET", path39, response.status, await response.text());
14100
+ throw new ApiError("GET", path41, response.status, await response.text());
14048
14101
  }
14049
14102
  return new Uint8Array(await response.arrayBuffer());
14050
14103
  }
@@ -14090,9 +14143,9 @@ var init_api_client = __esm({
14090
14143
  `/api/admin/data-explorer/debug/observability/${encodeURIComponent(hubId)}/conversations/${encodeURIComponent(conversationId)}${qs}`
14091
14144
  );
14092
14145
  }
14093
- async request(method, path39, body, extraHeaders, contentType) {
14094
- addApiBreadcrumb(method, path39);
14095
- const url = `${this.apiUrl}${path39}`;
14146
+ async request(method, path41, body, extraHeaders, contentType) {
14147
+ addApiBreadcrumb(method, path41);
14148
+ const url = `${this.apiUrl}${path41}`;
14096
14149
  let refreshedOn401 = false;
14097
14150
  for (let retry = 0; ; retry++) {
14098
14151
  let response = await this.send(url, method, body, extraHeaders, contentType);
@@ -14117,7 +14170,7 @@ var init_api_client = __esm({
14117
14170
  await delay(RETRYABLE_BACKOFF_MS[retry]);
14118
14171
  continue;
14119
14172
  }
14120
- throw new ApiError(method, path39, response.status, errorBody);
14173
+ throw new ApiError(method, path41, response.status, errorBody);
14121
14174
  }
14122
14175
  }
14123
14176
  /**
@@ -14179,9 +14232,9 @@ var init_api_client = __esm({
14179
14232
  * transient-retry loop — re-streaming a blob is not worth a cold-start probe.
14180
14233
  */
14181
14234
  async dataDownload(v1Path, orgId) {
14182
- const path39 = withOrgSelector(toDataProxyPath(v1Path), orgId);
14183
- addApiBreadcrumb("GET", path39);
14184
- const url = `${this.apiUrl}${path39}`;
14235
+ const path41 = withOrgSelector(toDataProxyPath(v1Path), orgId);
14236
+ addApiBreadcrumb("GET", path41);
14237
+ const url = `${this.apiUrl}${path41}`;
14185
14238
  let response = await this.send(url, "GET");
14186
14239
  if (response.status === 401 && this.onUnauthorized) {
14187
14240
  let refreshed;
@@ -14195,7 +14248,7 @@ var init_api_client = __esm({
14195
14248
  }
14196
14249
  }
14197
14250
  if (!response.ok) {
14198
- throw new ApiError("GET", path39, response.status, await response.text());
14251
+ throw new ApiError("GET", path41, response.status, await response.text());
14199
14252
  }
14200
14253
  return {
14201
14254
  bytes: new Uint8Array(await response.arrayBuffer()),
@@ -14266,6 +14319,15 @@ function extractApiErrorCode(err) {
14266
14319
  return null;
14267
14320
  }
14268
14321
  }
14322
+ function extractApiErrorReason(err) {
14323
+ if (!(err instanceof ApiError)) return null;
14324
+ try {
14325
+ const reason = JSON.parse(err.body).details?.reason;
14326
+ return typeof reason === "string" ? reason : null;
14327
+ } catch {
14328
+ return null;
14329
+ }
14330
+ }
14269
14331
  function extractApiRetryAfterSeconds(err) {
14270
14332
  if (!(err instanceof ApiError)) return null;
14271
14333
  try {
@@ -14432,7 +14494,7 @@ var init_dist = __esm({
14432
14494
  "../../packages/core/dist/index.js"() {
14433
14495
  "use strict";
14434
14496
  init_chunk_WSROLRJN();
14435
- init_chunk_BMAOKAMB();
14497
+ init_chunk_T3VB6VFG();
14436
14498
  init_chunk_VORPHGDT();
14437
14499
  init_chunk_POL7B3EM();
14438
14500
  init_chunk_46WU2TD4();
@@ -15105,6 +15167,12 @@ function writeFileNoFollow(root, abs, data) {
15105
15167
  fs.writeFileSync(abs, data);
15106
15168
  return true;
15107
15169
  }
15170
+ function writeFileIfChangedNoFollow(root, abs, content) {
15171
+ const bytes = Buffer.from(content, "utf-8");
15172
+ if (fileHasBytes(abs, bytes)) return false;
15173
+ if (!writeFileNoFollow(root, abs, bytes)) throw symlinkRefusal(path.relative(path.dirname(root), abs), "file");
15174
+ return true;
15175
+ }
15108
15176
  function createFileNoFollow(root, abs, data) {
15109
15177
  if (!ensureRealSubdirNoSymlink(root, path.dirname(abs), true)) return "refused";
15110
15178
  try {
@@ -15597,14 +15665,14 @@ function readOrgFields(doc) {
15597
15665
  if (name) config.organization_name = name;
15598
15666
  return { kind: "declared", config };
15599
15667
  }
15600
- function declarationFrom(load13) {
15601
- if (load13.kind === "absent") return { kind: "none" };
15602
- if (load13.kind === "malformed") return { kind: "invalid", detail: `it does not parse (${load13.reason})` };
15603
- return readOrgFields(load13.doc);
15668
+ function declarationFrom(load14) {
15669
+ if (load14.kind === "absent") return { kind: "none" };
15670
+ if (load14.kind === "malformed") return { kind: "invalid", detail: `it does not parse (${load14.reason})` };
15671
+ return readOrgFields(load14.doc);
15604
15672
  }
15605
- function legacyFieldsIn(load13) {
15606
- if (load13.kind !== "ok") return [];
15607
- return LEGACY_FIELDS.filter((f) => load13.doc[f] !== void 0);
15673
+ function legacyFieldsIn(load14) {
15674
+ if (load14.kind !== "ok") return [];
15675
+ return LEGACY_FIELDS.filter((f) => load14.doc[f] !== void 0);
15608
15676
  }
15609
15677
  function resolve3(gitRoot) {
15610
15678
  const manifestFile = workspaceManifestPath(gitRoot);
@@ -15790,13 +15858,13 @@ function writeRepoConfig(config, root) {
15790
15858
  if (stale.kind === "declared" && stale.config.organization_id !== config.organization_id) {
15791
15859
  throw expected(rebindRefusal(config.organization_id, stale.config.organization_id));
15792
15860
  }
15793
- const load13 = loadWorkspaceManifest(gitRoot);
15794
- if (load13.kind === "malformed") {
15861
+ const load14 = loadWorkspaceManifest(gitRoot);
15862
+ if (load14.kind === "malformed") {
15795
15863
  throw expected(
15796
- `Cannot write the organization binding: ${WORKSPACE_MANIFEST_LABEL} does not parse (${load13.reason}). Fix or delete it, then re-run.`
15864
+ `Cannot write the organization binding: ${WORKSPACE_MANIFEST_LABEL} does not parse (${load14.reason}). Fix or delete it, then re-run.`
15797
15865
  );
15798
15866
  }
15799
- const { organization_id: _id, organization_name: _name, ...rest } = load13.kind === "ok" ? load13.doc : {};
15867
+ const { organization_id: _id, organization_name: _name, ...rest } = load14.kind === "ok" ? load14.doc : {};
15800
15868
  const doc = { organization_id: config.organization_id };
15801
15869
  if (config.organization_name?.trim()) doc.organization_name = config.organization_name.trim();
15802
15870
  Object.assign(doc, rest);
@@ -16354,9 +16422,9 @@ function getVersionCachePath(filename = CLI_CACHE_FILE) {
16354
16422
  }
16355
16423
  function readVersionCache(filename = CLI_CACHE_FILE) {
16356
16424
  try {
16357
- const path39 = getVersionCachePath(filename);
16358
- if (!existsSync3(path39)) return null;
16359
- const parsed = JSON.parse(readFileSync5(path39, "utf-8"));
16425
+ const path41 = getVersionCachePath(filename);
16426
+ if (!existsSync3(path41)) return null;
16427
+ const parsed = JSON.parse(readFileSync5(path41, "utf-8"));
16360
16428
  if (typeof parsed.lastCheck !== "number") return null;
16361
16429
  if (parsed.latest !== null && typeof parsed.latest !== "string") return null;
16362
16430
  return parsed;
@@ -16376,10 +16444,10 @@ function isVersionCacheStale(filename = CLI_CACHE_FILE, maxAgeMs = MAX_AGE_24H_M
16376
16444
  return Date.now() - cache.lastCheck > maxAgeMs;
16377
16445
  }
16378
16446
  function writeVersionCache(filename, cache) {
16379
- const path39 = getVersionCachePath(filename);
16380
- const dir = dirname6(path39);
16447
+ const path41 = getVersionCachePath(filename);
16448
+ const dir = dirname6(path41);
16381
16449
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
16382
- writeFileSync2(path39, JSON.stringify(cache));
16450
+ writeFileSync2(path41, JSON.stringify(cache));
16383
16451
  }
16384
16452
  function touchVersionCache(filename) {
16385
16453
  writeVersionCache(filename, { lastCheck: Date.now(), latest: readVersionCache(filename)?.latest ?? null });
@@ -16418,14 +16486,14 @@ function parseFrontmatterVersion(content) {
16418
16486
  function findInstalledSkills(projectRoot, paths = SKILL_INSTALL_PATHS) {
16419
16487
  const found = [];
16420
16488
  for (const rel of paths) {
16421
- const path39 = join8(projectRoot, rel);
16422
- if (!existsSync4(path39)) continue;
16489
+ const path41 = join8(projectRoot, rel);
16490
+ if (!existsSync4(path41)) continue;
16423
16491
  let version = null;
16424
16492
  try {
16425
- version = parseFrontmatterVersion(readFileSync6(path39, "utf-8"));
16493
+ version = parseFrontmatterVersion(readFileSync6(path41, "utf-8"));
16426
16494
  } catch {
16427
16495
  }
16428
- found.push({ path: path39, version });
16496
+ found.push({ path: path41, version });
16429
16497
  }
16430
16498
  return found;
16431
16499
  }
@@ -17957,18 +18025,22 @@ var init_init = __esm({
17957
18025
  import * as fs9 from "fs";
17958
18026
  import * as path11 from "path";
17959
18027
  import * as yaml6 from "js-yaml";
17960
- function readCommunityId(folder) {
17961
- const bytes = readFileNoFollow(folder, path11.join(folder, COMMUNITY_FILE));
18028
+ function uuidField(doc, key) {
18029
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
18030
+ const id = doc[key];
18031
+ return typeof id === "string" && UUID_RE3.test(id) ? id : null;
18032
+ }
18033
+ function readYamlIdNoFollow(root, file, key) {
18034
+ const bytes = readFileNoFollow(root, file);
17962
18035
  if (bytes === null) return null;
17963
- let doc;
17964
18036
  try {
17965
- doc = yaml6.load(bytes.toString("utf-8"));
18037
+ return uuidField(yaml6.load(bytes.toString("utf-8")), key);
17966
18038
  } catch {
17967
18039
  return null;
17968
18040
  }
17969
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
17970
- const id = doc.community_id;
17971
- return typeof id === "string" && UUID_RE3.test(id) ? id : null;
18041
+ }
18042
+ function readCommunityId(folder) {
18043
+ return readYamlIdNoFollow(folder, path11.join(folder, COMMUNITY_FILE), "community_id");
17972
18044
  }
17973
18045
  function listCommunityFolders(communitiesDir) {
17974
18046
  let entries;
@@ -19606,9 +19678,9 @@ var init_base_workspace = __esm({
19606
19678
  // src/lib/subtree-routing.ts
19607
19679
  import * as path20 from "path";
19608
19680
  function readRepoDefaults(gitRoot) {
19609
- const load13 = loadWorkspaceManifest(gitRoot);
19610
- if (load13.kind !== "ok") return {};
19611
- const raw = load13.doc;
19681
+ const load14 = loadWorkspaceManifest(gitRoot);
19682
+ if (load14.kind !== "ok") return {};
19683
+ const raw = load14.doc;
19612
19684
  const str = (v) => {
19613
19685
  if (typeof v !== "string" || !v.trim()) return void 0;
19614
19686
  const value = v.trim();
@@ -19893,12 +19965,12 @@ async function createDataClient(orgId) {
19893
19965
  return orgId2;
19894
19966
  }
19895
19967
  return {
19896
- async request(method, path39, body) {
19897
- const envelope = await api.dataRequest(method, path39, body, org);
19968
+ async request(method, path41, body) {
19969
+ const envelope = await api.dataRequest(method, path41, body, org);
19898
19970
  return envelope.data;
19899
19971
  },
19900
- async requestRaw(method, path39, body) {
19901
- const body_ = await api.dataRequest(method, path39, body, org);
19972
+ async requestRaw(method, path41, body) {
19973
+ const body_ = await api.dataRequest(method, path41, body, org);
19902
19974
  return body_;
19903
19975
  },
19904
19976
  async collectPages(makePath) {
@@ -20742,6 +20814,133 @@ var init_sync = __esm({
20742
20814
  }
20743
20815
  });
20744
20816
 
20817
+ // src/lib/eval-session-control.ts
20818
+ function formatEvalSessionStopCommand(sessionId, hubId) {
20819
+ return `wayai eval session stop ${sessionId} --hub ${hubId}`;
20820
+ }
20821
+ function formatEvalResultsCommand(sessionId, hubId) {
20822
+ return `wayai eval-results --session ${sessionId} --hub ${hubId}`;
20823
+ }
20824
+ function describeSeedRelease(status) {
20825
+ if (status === "pending") {
20826
+ return "A turn that started before the stop is still finishing \u2014 its fixture stays reserved until that turn settles.";
20827
+ }
20828
+ if (status === "releasing") return "Its fixture is being released.";
20829
+ return "A turn already in progress may still be finishing \u2014 wait for it before starting another run.";
20830
+ }
20831
+ function classifyStopOutcome(response) {
20832
+ return response.data.stopped ? { kind: "cancelled", seedRelease: response.data.seed_release_status } : { kind: "already_finished", status: response.data.session.session_status };
20833
+ }
20834
+ function isTerminalStopFailure(err) {
20835
+ return err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 408 && err.status !== 429;
20836
+ }
20837
+ async function stopEvalSessionWithRetries(stop, opts = {}) {
20838
+ const attempts = opts.attempts ?? STOP_ATTEMPTS;
20839
+ const attemptTimeoutMs = opts.attemptTimeoutMs ?? STOP_ATTEMPT_TIMEOUT_MS;
20840
+ const backoffMs = opts.backoffMs ?? STOP_BACKOFF_MS;
20841
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
20842
+ let lastError = new Error("Stop was never attempted");
20843
+ for (let attempt = 1; attempt <= attempts; attempt++) {
20844
+ let timeout;
20845
+ try {
20846
+ return await Promise.race([
20847
+ stop(),
20848
+ new Promise((_resolve, reject) => {
20849
+ timeout = setTimeout(
20850
+ () => reject(new Error(`Stop attempt ${attempt} timed out after ${attemptTimeoutMs}ms`)),
20851
+ attemptTimeoutMs
20852
+ );
20853
+ })
20854
+ ]);
20855
+ } catch (err) {
20856
+ lastError = err;
20857
+ if (isTerminalStopFailure(err)) break;
20858
+ } finally {
20859
+ if (timeout !== void 0) clearTimeout(timeout);
20860
+ }
20861
+ const delay2 = backoffMs[attempt - 1];
20862
+ if (attempt < attempts && delay2) await sleep3(delay2);
20863
+ }
20864
+ throw lastError;
20865
+ }
20866
+ function installEvalSignalHandlers(input) {
20867
+ const exit = input.exit ?? ((code) => process.exit(code));
20868
+ const manualStopCommand = formatEvalSessionStopCommand(input.sessionId, input.hubId);
20869
+ let signalCount = 0;
20870
+ let disposed = false;
20871
+ let settle;
20872
+ const settled = new Promise((resolve12) => {
20873
+ settle = resolve12;
20874
+ });
20875
+ const listeners = /* @__PURE__ */ new Map();
20876
+ const dispose = () => {
20877
+ if (disposed) return;
20878
+ disposed = true;
20879
+ for (const [signal, listener] of listeners) process.off(signal, listener);
20880
+ };
20881
+ const finish = (exitCode) => {
20882
+ dispose();
20883
+ settle();
20884
+ exit(exitCode);
20885
+ };
20886
+ const handle = (signal) => {
20887
+ signalCount += 1;
20888
+ const signalExitCode = SIGNAL_EXIT_CODE[signal];
20889
+ if (signalCount > 1) {
20890
+ console.error("\nSecond signal received; exiting immediately. The remote eval session may still be running.");
20891
+ console.error(`Run: ${manualStopCommand}`);
20892
+ finish(signalExitCode);
20893
+ return;
20894
+ }
20895
+ console.error(`
20896
+ ${signal} received; stopping remote eval session ${input.sessionId}...`);
20897
+ void stopEvalSessionWithRetries(input.stop).then((response) => {
20898
+ if (signalCount > 1) return;
20899
+ const outcome = classifyStopOutcome(response);
20900
+ if (outcome.kind === "already_finished") {
20901
+ console.error(`Session had already finished (${outcome.status}) \u2014 nothing was cancelled.`);
20902
+ console.error(`See its results: ${formatEvalResultsCommand(input.sessionId, input.hubId)}`);
20903
+ } else {
20904
+ console.error("Remote session cancellation requested.");
20905
+ console.error(describeSeedRelease(outcome.seedRelease));
20906
+ }
20907
+ finish(signalExitCode);
20908
+ }).catch((err) => {
20909
+ if (signalCount > 1) return;
20910
+ console.error(`Failed to confirm remote cancellation: ${extractApiMessage(err)}`);
20911
+ console.error(`The session may still be running. Run: ${manualStopCommand}`);
20912
+ finish(1);
20913
+ });
20914
+ };
20915
+ for (const signal of HANDLED_SIGNALS) {
20916
+ const listener = () => handle(signal);
20917
+ listeners.set(signal, listener);
20918
+ process.on(signal, listener);
20919
+ }
20920
+ return {
20921
+ manualStopCommand,
20922
+ async interrupted() {
20923
+ if (signalCount === 0) return false;
20924
+ await settled;
20925
+ return true;
20926
+ },
20927
+ dispose
20928
+ };
20929
+ }
20930
+ var SIGNAL_EXIT_CODE, HANDLED_SIGNALS, STOP_ATTEMPTS, STOP_ATTEMPT_TIMEOUT_MS, STOP_BACKOFF_MS;
20931
+ var init_eval_session_control = __esm({
20932
+ "src/lib/eval-session-control.ts"() {
20933
+ "use strict";
20934
+ init_api_client();
20935
+ init_errors2();
20936
+ SIGNAL_EXIT_CODE = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 };
20937
+ HANDLED_SIGNALS = Object.keys(SIGNAL_EXIT_CODE);
20938
+ STOP_ATTEMPTS = 3;
20939
+ STOP_ATTEMPT_TIMEOUT_MS = 1e4;
20940
+ STOP_BACKOFF_MS = [500, 1e3];
20941
+ }
20942
+ });
20943
+
20745
20944
  // src/communities/api.ts
20746
20945
  async function communitiesClient() {
20747
20946
  const { config, accessToken } = await requireAuth();
@@ -20874,6 +21073,23 @@ function settingsContent(settings) {
20874
21073
  function roomContent(room) {
20875
21074
  return { name: toLf(room.name).trim(), archived: room.archived };
20876
21075
  }
21076
+ function courseContent(course) {
21077
+ return { title: toLf(course.title).trim(), description: toLf(course.description).trim(), published: course.published };
21078
+ }
21079
+ function moduleContent(module) {
21080
+ return { title: toLf(module.title).trim() };
21081
+ }
21082
+ function lessonContent(lesson) {
21083
+ return {
21084
+ title: toLf(lesson.title).trim(),
21085
+ body: toLf(lesson.body),
21086
+ video: lesson.video ? { provider: lesson.video.provider, id: lesson.video.id } : null,
21087
+ links: lesson.links.map((link) => ({ title: toLf(link.title).trim(), url: link.url.trim() })),
21088
+ room_id: lesson.room_id,
21089
+ requires_paid_plan: lesson.requires_paid_plan,
21090
+ published: lesson.published
21091
+ };
21092
+ }
20877
21093
  function rev(parts) {
20878
21094
  return `${REV_VERSION}:${createHash3("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 12)}`;
20879
21095
  }
@@ -20885,15 +21101,31 @@ function serverRoom(room) {
20885
21101
  const content = roomContent(room);
20886
21102
  return { content, rev: roomRev(room.room_id, content) };
20887
21103
  }
21104
+ function serverCourse(course) {
21105
+ const content = courseContent(course);
21106
+ return { content, rev: courseRev(course.course_id, content) };
21107
+ }
21108
+ function serverModule(module) {
21109
+ const content = moduleContent(module);
21110
+ return { content, rev: moduleRev(module.module_id, content) };
21111
+ }
21112
+ function serverLesson(lesson) {
21113
+ const content = lessonContent(lesson);
21114
+ return { content, rev: lessonRev(lesson.lesson_id, content) };
21115
+ }
21116
+ function knownBase(base) {
21117
+ return base !== void 0 && base.startsWith(`${REV_VERSION}:`) ? base : void 0;
21118
+ }
20888
21119
  function classify(base, file, server) {
20889
- const known = base !== void 0 && base.startsWith(`${REV_VERSION}:`) ? base : void 0;
21120
+ const known = knownBase(base);
20890
21121
  if (known === void 0) return file === server ? "converged" : "conflict";
20891
- const fileChanged = file !== known;
20892
- const serverChanged = server !== known;
21122
+ return threeWay(file !== known, server !== known, file === server);
21123
+ }
21124
+ function threeWay(fileChanged, serverChanged, agree) {
20893
21125
  if (!fileChanged && !serverChanged) return "in_sync";
20894
- if (fileChanged && !serverChanged) return "local";
21126
+ if (!serverChanged) return "local";
20895
21127
  if (!fileChanged) return "remote";
20896
- return file === server ? "converged" : "conflict";
21128
+ return agree ? "converged" : "conflict";
20897
21129
  }
20898
21130
  function createClientId(communityId, kind, localName2) {
20899
21131
  return uuidV5(communityId, `${kind}:${localName2}`);
@@ -20904,7 +21136,7 @@ function localName(name, id, taken, fallback) {
20904
21136
  const withPrefix = `${slug}--${id.slice(0, 8)}`;
20905
21137
  return taken.has(withPrefix) ? `${slug}--${id}` : withPrefix;
20906
21138
  }
20907
- var REV_VERSION, settingsRev, joinLinkRev, roomRev;
21139
+ var REV_VERSION, settingsRev, joinLinkRev, roomRev, courseRev, moduleRev, lessonRev;
20908
21140
  var init_model = __esm({
20909
21141
  "src/communities/model.ts"() {
20910
21142
  "use strict";
@@ -20913,10 +21145,25 @@ var init_model = __esm({
20913
21145
  settingsRev = (communityId, c) => rev(["settings", communityId, c.rules, c.slow_mode_seconds, c.courses_label]);
20914
21146
  joinLinkRev = (communityId, c) => rev(["join_link", communityId, c.enabled]);
20915
21147
  roomRev = (roomId, c) => rev(["room", roomId, c.name, c.archived]);
21148
+ courseRev = (courseId, c) => rev(["course", courseId, c.title, c.description, c.published]);
21149
+ moduleRev = (moduleId, c) => rev(["module", moduleId, c.title]);
21150
+ lessonRev = (lessonId, c) => rev([
21151
+ "lesson",
21152
+ lessonId,
21153
+ c.title,
21154
+ c.body,
21155
+ c.video?.provider ?? null,
21156
+ c.video?.id ?? null,
21157
+ c.links.map((link) => [link.title, link.url]),
21158
+ c.room_id,
21159
+ c.requires_paid_plan,
21160
+ c.published
21161
+ ]);
20916
21162
  }
20917
21163
  });
20918
21164
 
20919
- // src/communities/files.ts
21165
+ // src/communities/course-files.ts
21166
+ import * as fs18 from "fs";
20920
21167
  import * as path24 from "path";
20921
21168
  import * as yaml12 from "js-yaml";
20922
21169
  function isMapping(value) {
@@ -20933,21 +21180,359 @@ function optionalString(doc, key, where, problems2) {
20933
21180
  if (typeof value !== "string") problems2.push(`${where}: ${key} must be text`);
20934
21181
  return typeof value === "string" ? value : void 0;
20935
21182
  }
21183
+ function optionalBoolean(doc, key, where, problems2) {
21184
+ const value = doc[key];
21185
+ if (value === void 0 || value === null) return false;
21186
+ if (typeof value !== "boolean") problems2.push(`${where}: ${key} must be true or false`);
21187
+ return value === true;
21188
+ }
21189
+ function identity(doc, what, where, problems2) {
21190
+ const id = optionalString(doc, "id", where, problems2);
21191
+ if (id !== void 0 && !UUID_RE3.test(id)) problems2.push(`${where}: id must be ${what}'s id (a UUID)`);
21192
+ const rev2 = optionalString(doc, "rev", where, problems2);
21193
+ if (id === void 0 && rev2 !== void 0) problems2.push(`${where}: a rev without an id \u2014 remove the rev to create the ${what}`);
21194
+ return { ...id === void 0 ? {} : { id }, ...rev2 === void 0 ? {} : { rev: rev2 } };
21195
+ }
20936
21196
  function contractProblem(error) {
20937
21197
  const [issue] = error.issues;
20938
21198
  return issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ${issue.message}` : issue.message;
20939
21199
  }
21200
+ function readIdList(raw, where, problems2) {
21201
+ if (raw === void 0 || raw === null) return void 0;
21202
+ if (!Array.isArray(raw) || raw.some((id) => typeof id !== "string" || !UUID_RE3.test(id)) || new Set(raw).size !== raw.length) {
21203
+ problems2.push(`${where} must be a list of ids, each once \u2014 leave it as pull and push write it`);
21204
+ return void 0;
21205
+ }
21206
+ return raw;
21207
+ }
21208
+ function readCourseSynced(raw, where, problems2) {
21209
+ if (raw === void 0 || raw === null) return void 0;
21210
+ if (!isMapping(raw)) {
21211
+ problems2.push(`${where}: synced must be a mapping`);
21212
+ return void 0;
21213
+ }
21214
+ unknownKeys(raw, COURSE_SYNCED_KEYS, `${where} synced`, problems2);
21215
+ const lessons = {};
21216
+ if (raw.lessons !== void 0 && raw.lessons !== null) {
21217
+ if (!isMapping(raw.lessons)) problems2.push(`${where}: synced.lessons must be a mapping of module ids`);
21218
+ else {
21219
+ for (const [moduleId, ids] of Object.entries(raw.lessons)) {
21220
+ if (!UUID_RE3.test(moduleId)) problems2.push(`${where}: synced.lessons: ${JSON.stringify(moduleId)} is not a module id`);
21221
+ const list = readIdList(ids, `${where} synced.lessons.${moduleId}`, problems2);
21222
+ if (list) lessons[moduleId] = list;
21223
+ }
21224
+ }
21225
+ }
21226
+ const modules = readIdList(raw.modules, `${where} synced.modules`, problems2);
21227
+ return { ...modules === void 0 ? {} : { modules }, lessons };
21228
+ }
21229
+ function readNameList(raw, where, what, problems2) {
21230
+ if (raw === void 0 || raw === null) return [];
21231
+ if (!Array.isArray(raw)) {
21232
+ problems2.push(`${where} must be a list of ${what} names`);
21233
+ return [];
21234
+ }
21235
+ const names = [];
21236
+ for (const name of raw) {
21237
+ if (typeof name !== "string" || !isPathSafeId(name)) {
21238
+ problems2.push(`${where}: ${JSON.stringify(name)} is not a ${what} name \u2014 ${PATH_SAFE_ID_RULE}`);
21239
+ } else if (names.includes(name)) {
21240
+ problems2.push(`${where}: ${name} is listed twice`);
21241
+ } else {
21242
+ names.push(name);
21243
+ }
21244
+ }
21245
+ return names;
21246
+ }
21247
+ function readCourseFolders(communityFolder, slugs, problems2) {
21248
+ const coursesDir = path24.join(communityFolder, COURSES_DIR);
21249
+ requireRealSubdirNoSymlink(communityFolder, coursesDir, false);
21250
+ const courses = [];
21251
+ for (const slug of slugs) {
21252
+ const folder = path24.join(coursesDir, slug);
21253
+ requireRealSubdirNoSymlink(communityFolder, folder, false);
21254
+ if (!isRealDirectory(folder)) {
21255
+ problems2.push(`${COMMUNITY_FILE} courses: ${slug} has no folder ${COURSES_DIR}/${slug}/`);
21256
+ continue;
21257
+ }
21258
+ const course = readCourseFolder(communityFolder, folder, slug, problems2);
21259
+ if (course) courses.push(course);
21260
+ }
21261
+ const unlisted = folderEntries(coursesDir).filter((name) => !slugs.includes(name) && isRealDirectory(path24.join(coursesDir, name))).map((name) => ({ name, id: readYamlIdNoFollow(communityFolder, path24.join(coursesDir, name, COURSE_FILE), "id") }));
21262
+ return { courses, unlisted };
21263
+ }
21264
+ function readCourseFolder(communityFolder, folder, slug, problems2) {
21265
+ const where = `${COURSES_DIR}/${slug}/${COURSE_FILE}`;
21266
+ const text = readRealFileOrThrow(communityFolder, path24.join(folder, COURSE_FILE));
21267
+ if (text === null) {
21268
+ problems2.push(`${COURSES_DIR}/${slug}/: no ${COURSE_FILE}`);
21269
+ return null;
21270
+ }
21271
+ let doc;
21272
+ try {
21273
+ doc = yaml12.load(text);
21274
+ } catch (err) {
21275
+ problems2.push(`${where} is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
21276
+ return null;
21277
+ }
21278
+ if (!isMapping(doc)) {
21279
+ problems2.push(`${where} must be a YAML mapping`);
21280
+ return null;
21281
+ }
21282
+ unknownKeys(doc, COURSE_KEYS, where, problems2);
21283
+ const { id, rev: rev2 } = identity(doc, "a course", where, problems2);
21284
+ const synced2 = readCourseSynced(doc.synced, where, problems2);
21285
+ if (id === void 0 && synced2 !== void 0) problems2.push(`${where}: synced without an id \u2014 remove it to create the course`);
21286
+ const description2 = doc.description ?? "";
21287
+ const parsed = communityCourseCreateBody.safeParse({ title: doc.title, description: description2 });
21288
+ if (!parsed.success) problems2.push(`${where}: ${contractProblem(parsed.error)}`);
21289
+ const published = optionalBoolean(doc, "published", where, problems2);
21290
+ const lessonsDir = path24.join(folder, LESSONS_DIR);
21291
+ requireRealSubdirNoSymlink(communityFolder, lessonsDir, false);
21292
+ const modules = readModules(communityFolder, lessonsDir, doc.modules, slug, where, problems2);
21293
+ const listed2 = new Set(modules.flatMap((module) => module.lessons.map((lesson) => lesson.slug)));
21294
+ const unlisted = folderEntries(lessonsDir).filter((name) => name.endsWith(LESSON_FILE_EXT) && !listed2.has(path24.basename(name, LESSON_FILE_EXT))).map((name) => ({ name, id: uuidField(lessonFrontmatter(readFileNoFollow(communityFolder, path24.join(lessonsDir, name))), "id") }));
21295
+ if (!parsed.success) return null;
21296
+ return {
21297
+ slug,
21298
+ ...id === void 0 ? {} : { id },
21299
+ ...rev2 === void 0 ? {} : { rev: rev2 },
21300
+ title: doc.title,
21301
+ description: description2,
21302
+ published,
21303
+ modules,
21304
+ ...synced2 === void 0 ? {} : { synced: synced2 },
21305
+ unlisted
21306
+ };
21307
+ }
21308
+ function readModules(communityFolder, lessonsDir, raw, courseSlug, courseWhere, problems2) {
21309
+ if (raw === void 0 || raw === null) return [];
21310
+ if (!Array.isArray(raw)) {
21311
+ problems2.push(`${courseWhere}: modules must be a list`);
21312
+ return [];
21313
+ }
21314
+ const modules = [];
21315
+ const keys = /* @__PURE__ */ new Set();
21316
+ const listedLessons = /* @__PURE__ */ new Set();
21317
+ raw.forEach((entry, index) => {
21318
+ const at = `${courseWhere} modules[${index}]`;
21319
+ if (!isMapping(entry)) {
21320
+ problems2.push(`${at}: must be a mapping`);
21321
+ return;
21322
+ }
21323
+ unknownKeys(entry, MODULE_KEYS, at, problems2);
21324
+ const key = entry.key;
21325
+ if (typeof key !== "string" || !isPathSafeId(key)) {
21326
+ problems2.push(`${at}: key must be a plain name \u2014 ${PATH_SAFE_ID_RULE}`);
21327
+ return;
21328
+ }
21329
+ const where = `${courseWhere} module ${key}`;
21330
+ if (keys.has(key)) problems2.push(`${where}: the key is used twice`);
21331
+ keys.add(key);
21332
+ const { id, rev: rev2 } = identity(entry, "a module", where, problems2);
21333
+ const parsed = communityModuleCreateBody.safeParse({ title: entry.title });
21334
+ if (!parsed.success) problems2.push(`${where}: ${contractProblem(parsed.error)}`);
21335
+ const lessons = [];
21336
+ for (const slug of readNameList(entry.lessons, `${where} lessons`, "lesson", problems2)) {
21337
+ if (listedLessons.has(slug)) {
21338
+ problems2.push(`${courseWhere}: lesson ${slug} is listed twice`);
21339
+ continue;
21340
+ }
21341
+ listedLessons.add(slug);
21342
+ const lesson = readLessonFile(communityFolder, lessonsDir, courseSlug, slug, problems2);
21343
+ if (lesson) lessons.push(lesson);
21344
+ }
21345
+ if (!parsed.success) return;
21346
+ modules.push({
21347
+ key,
21348
+ ...id === void 0 ? {} : { id },
21349
+ ...rev2 === void 0 ? {} : { rev: rev2 },
21350
+ title: entry.title,
21351
+ lessons
21352
+ });
21353
+ });
21354
+ return modules;
21355
+ }
21356
+ function splitFrontmatter(text) {
21357
+ if (!text.startsWith("---\n")) return null;
21358
+ const close2 = text.indexOf("\n---\n", 3);
21359
+ if (close2 === -1) return text.endsWith("\n---") ? { front: text.slice(4, -3), body: "" } : null;
21360
+ const rest = text.slice(close2 + 5);
21361
+ return { front: text.slice(4, close2 + 1), body: rest.endsWith("\n") ? rest.slice(0, -1) : rest };
21362
+ }
21363
+ function readLessonFile(communityFolder, lessonsDir, courseSlug, slug, problems2) {
21364
+ const name = `${slug}${LESSON_FILE_EXT}`;
21365
+ const where = `${COURSES_DIR}/${courseSlug}/${LESSONS_DIR}/${name}`;
21366
+ const raw = readRealFileOrThrow(communityFolder, path24.join(lessonsDir, name));
21367
+ if (raw === null) {
21368
+ const hint = slug.endsWith(LESSON_FILE_EXT) ? ` (list a lesson by its file's name less ${LESSON_FILE_EXT})` : "";
21369
+ problems2.push(`${COURSES_DIR}/${courseSlug}/${COURSE_FILE}: lesson ${slug} has no file ${LESSONS_DIR}/${name}${hint}`);
21370
+ return null;
21371
+ }
21372
+ const parts = splitFrontmatter(toLf(raw));
21373
+ if (!parts) {
21374
+ problems2.push(`${where}: must start with a frontmatter block (a line --- , the lesson's fields, a line ---)`);
21375
+ return null;
21376
+ }
21377
+ let doc;
21378
+ try {
21379
+ doc = parts.front.trim() === "" ? {} : yaml12.load(parts.front);
21380
+ } catch (err) {
21381
+ problems2.push(`${where}: the frontmatter is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
21382
+ return null;
21383
+ }
21384
+ if (!isMapping(doc)) {
21385
+ problems2.push(`${where}: the frontmatter must be a YAML mapping`);
21386
+ return null;
21387
+ }
21388
+ unknownKeys(doc, LESSON_KEYS, where, problems2);
21389
+ const { id, rev: rev2 } = identity(doc, "a lesson", where, problems2);
21390
+ const room = optionalString(doc, "room", where, problems2);
21391
+ if (room !== void 0 && !isPathSafeId(room)) problems2.push(`${where}: room must be a room's key in ${COMMUNITY_FILE}`);
21392
+ const videoUrl = optionalString(doc, "video_url", where, problems2);
21393
+ const published = optionalBoolean(doc, "published", where, problems2);
21394
+ const requiresPaidPlan = optionalBoolean(doc, "requires_paid_plan", where, problems2);
21395
+ const links = doc.links ?? [];
21396
+ const parsed = lessonFileBody.safeParse({
21397
+ title: doc.title,
21398
+ ...videoUrl === void 0 ? {} : { video_url: videoUrl },
21399
+ body: parts.body,
21400
+ links
21401
+ });
21402
+ if (!parsed.success) {
21403
+ problems2.push(`${where}: ${contractProblem(parsed.error)}`);
21404
+ return null;
21405
+ }
21406
+ return {
21407
+ slug,
21408
+ ...id === void 0 ? {} : { id },
21409
+ ...rev2 === void 0 ? {} : { rev: rev2 },
21410
+ title: doc.title,
21411
+ published,
21412
+ ...room === void 0 ? {} : { room },
21413
+ ...videoUrl === void 0 ? {} : { video_url: videoUrl },
21414
+ links: links.map((link) => ({ title: link.title, url: link.url })),
21415
+ requires_paid_plan: requiresPaidPlan,
21416
+ body: parts.body
21417
+ };
21418
+ }
21419
+ function folderEntries(dir) {
21420
+ try {
21421
+ return fs18.readdirSync(dir).filter((name) => !name.startsWith(".")).sort();
21422
+ } catch {
21423
+ return [];
21424
+ }
21425
+ }
21426
+ function lessonFrontmatter(bytes) {
21427
+ const parts = bytes ? splitFrontmatter(toLf(bytes.toString("utf-8"))) : null;
21428
+ if (!parts) return null;
21429
+ try {
21430
+ return yaml12.load(parts.front);
21431
+ } catch {
21432
+ return null;
21433
+ }
21434
+ }
21435
+ function renderCourseYaml(course) {
21436
+ const doc = {
21437
+ ...course.id ? { id: course.id } : {},
21438
+ ...course.rev ? { rev: course.rev } : {},
21439
+ title: course.title,
21440
+ description: course.description,
21441
+ published: course.published,
21442
+ modules: course.modules.map((module) => ({
21443
+ key: module.key,
21444
+ ...module.id ? { id: module.id } : {},
21445
+ ...module.rev ? { rev: module.rev } : {},
21446
+ title: module.title,
21447
+ lessons: module.lessons.map((lesson) => lesson.slug)
21448
+ })),
21449
+ ...course.synced ? {
21450
+ synced: {
21451
+ ...course.synced.modules ? { modules: course.synced.modules } : {},
21452
+ lessons: Object.fromEntries(
21453
+ Object.entries(course.synced.lessons).sort(([a], [b]) => syncedRank(course, a) - syncedRank(course, b) || a.localeCompare(b))
21454
+ )
21455
+ }
21456
+ } : {}
21457
+ };
21458
+ return yaml12.dump(doc, YAML_DUMP_OPTIONS);
21459
+ }
21460
+ function syncedRank(course, moduleId) {
21461
+ const at = course.synced?.modules?.indexOf(moduleId) ?? -1;
21462
+ return at === -1 ? Number.MAX_SAFE_INTEGER : at;
21463
+ }
21464
+ function renderLessonFile(lesson) {
21465
+ const front = {
21466
+ ...lesson.id ? { id: lesson.id } : {},
21467
+ ...lesson.rev ? { rev: lesson.rev } : {},
21468
+ title: lesson.title,
21469
+ published: lesson.published,
21470
+ ...lesson.room ? { room: lesson.room } : {},
21471
+ ...lesson.video_url ? { video_url: lesson.video_url } : {},
21472
+ ...lesson.links.length > 0 ? { links: lesson.links.map((link) => ({ title: link.title, url: link.url })) } : {},
21473
+ requires_paid_plan: lesson.requires_paid_plan
21474
+ };
21475
+ return `---
21476
+ ${yaml12.dump(front, YAML_DUMP_OPTIONS)}---
21477
+ ${toLf(lesson.body)}
21478
+ `;
21479
+ }
21480
+ function writeCourseYaml(root, communityFolder, course) {
21481
+ return writeFileIfChangedNoFollow(root, path24.join(communityFolder, coursePath(course)), renderCourseYaml(course));
21482
+ }
21483
+ function writeLessonFile(root, communityFolder, course, lesson) {
21484
+ return writeFileIfChangedNoFollow(root, path24.join(communityFolder, lessonPath(course, lesson)), renderLessonFile(lesson));
21485
+ }
21486
+ function writeCourseFolder(root, communityFolder, course) {
21487
+ const written = writeCourseYaml(root, communityFolder, course) ? [coursePath(course)] : [];
21488
+ for (const module of course.modules) {
21489
+ for (const lesson of module.lessons) {
21490
+ if (writeLessonFile(root, communityFolder, course, lesson)) written.push(lessonPath(course, lesson));
21491
+ }
21492
+ }
21493
+ return written;
21494
+ }
21495
+ var COURSES_DIR, COURSE_FILE, LESSONS_DIR, LESSON_FILE_EXT, COURSE_KEYS, MODULE_KEYS, COURSE_SYNCED_KEYS, LESSON_KEYS, lessonFileBody, coursePath, lessonPath, unlistedPath;
21496
+ var init_course_files = __esm({
21497
+ "src/communities/course-files.ts"() {
21498
+ "use strict";
21499
+ init_contracts();
21500
+ init_base_id();
21501
+ init_community_workspace();
21502
+ init_fs_safety();
21503
+ init_layout();
21504
+ init_yaml_writer();
21505
+ init_utils();
21506
+ init_model();
21507
+ COURSES_DIR = "courses";
21508
+ COURSE_FILE = "course.yaml";
21509
+ LESSONS_DIR = "lessons";
21510
+ LESSON_FILE_EXT = ".md";
21511
+ COURSE_KEYS = ["id", "rev", "title", "description", "published", "modules", "synced"];
21512
+ MODULE_KEYS = ["key", "id", "rev", "title", "lessons"];
21513
+ COURSE_SYNCED_KEYS = ["modules", "lessons"];
21514
+ LESSON_KEYS = ["id", "rev", "title", "published", "room", "video_url", "links", "requires_paid_plan"];
21515
+ lessonFileBody = communityLessonCreateBody.omit({ module_id: true, room_id: true, client_id: true });
21516
+ coursePath = (course) => path24.join(COURSES_DIR, course.slug, COURSE_FILE);
21517
+ lessonPath = (course, lesson) => path24.join(COURSES_DIR, course.slug, LESSONS_DIR, `${lesson.slug}${LESSON_FILE_EXT}`);
21518
+ unlistedPath = (name, course) => course ? path24.join(COURSES_DIR, course.slug, LESSONS_DIR, name) : `${path24.join(COURSES_DIR, name)}/`;
21519
+ }
21520
+ });
21521
+
21522
+ // src/communities/files.ts
21523
+ import * as path25 from "path";
21524
+ import * as yaml13 from "js-yaml";
20940
21525
  function readCommunityFolder(folder) {
20941
- const where = path24.basename(folder);
20942
- const yamlPath = path24.join(folder, COMMUNITY_FILE);
21526
+ const where = path25.basename(folder);
21527
+ const yamlPath = path25.join(folder, COMMUNITY_FILE);
20943
21528
  const text = readRealFileOrThrow(folder, yamlPath);
20944
21529
  if (text === null) {
20945
21530
  throw expected(`No ${COMMUNITY_FILE} in ${where}/: pull the community first (wayai pull communities/<community_id>).`);
20946
21531
  }
20947
- const rules = toLf(readRealFileOrThrow(folder, path24.join(folder, RULES_FILE)) ?? "");
21532
+ const rules = toLf(readRealFileOrThrow(folder, path25.join(folder, RULES_FILE)) ?? "");
20948
21533
  let doc;
20949
21534
  try {
20950
- doc = yaml12.load(text);
21535
+ doc = yaml13.load(text);
20951
21536
  } catch (err) {
20952
21537
  throw expected(`${where}/${COMMUNITY_FILE} is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
20953
21538
  }
@@ -20974,6 +21559,9 @@ function readCommunityFolder(folder) {
20974
21559
  if (typeof doc.join_link_enabled !== "boolean") problems2.push(`${COMMUNITY_FILE}: join_link_enabled must be true or false`);
20975
21560
  const joinLinkRev2 = optionalString(doc, "join_link_rev", COMMUNITY_FILE, problems2);
20976
21561
  const rooms = readRooms(doc.rooms, problems2);
21562
+ const synced2 = readCommunitySynced(doc.synced, problems2);
21563
+ const { courses, unlisted } = readCourseFolders(folder, readNameList(doc.courses, `${COMMUNITY_FILE} courses`, "course folder", problems2), problems2);
21564
+ problems2.push(...crossFileProblems(rooms, courses));
20977
21565
  if (problems2.length > 0) {
20978
21566
  throw expected(`${where}/ cannot be synced:
20979
21567
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
@@ -20989,8 +21577,44 @@ ${problems2.map((p) => ` - ${p}`).join("\n")}`);
20989
21577
  rules,
20990
21578
  join_link_enabled: doc.join_link_enabled,
20991
21579
  ...joinLinkRev2 === void 0 ? {} : { join_link_rev: joinLinkRev2 },
20992
- rooms
21580
+ rooms,
21581
+ courses,
21582
+ ...synced2 === void 0 ? {} : { synced_courses: synced2 },
21583
+ unlistedCourses: unlisted
21584
+ };
21585
+ }
21586
+ function readCommunitySynced(raw, problems2) {
21587
+ if (raw === void 0 || raw === null) return void 0;
21588
+ if (!isMapping(raw)) {
21589
+ problems2.push(`${COMMUNITY_FILE}: synced must be a mapping`);
21590
+ return void 0;
21591
+ }
21592
+ unknownKeys(raw, ["courses"], `${COMMUNITY_FILE} synced`, problems2);
21593
+ return readIdList(raw.courses, `${COMMUNITY_FILE} synced.courses`, problems2);
21594
+ }
21595
+ function crossFileProblems(rooms, courses) {
21596
+ const problems2 = [];
21597
+ const seen = /* @__PURE__ */ new Map();
21598
+ const once = (id, where) => {
21599
+ if (id === void 0) return;
21600
+ const first = seen.get(id);
21601
+ if (first) problems2.push(`${where}: id ${id} is also ${first}'s \u2014 a copy keeps its original's id; remove the id (and its revs) to make a new one`);
21602
+ else seen.set(id, where);
20993
21603
  };
21604
+ const roomKeys = new Set(rooms.map((room) => room.key));
21605
+ for (const course of courses) {
21606
+ once(course.id, `course ${course.slug}`);
21607
+ for (const module of course.modules) {
21608
+ once(module.id, `module ${course.slug}/${module.key}`);
21609
+ for (const lesson of module.lessons) {
21610
+ once(lesson.id, `lesson ${course.slug}/${lesson.slug}`);
21611
+ if (lesson.room !== void 0 && !roomKeys.has(lesson.room)) {
21612
+ problems2.push(`lesson ${course.slug}/${lesson.slug}: room ${lesson.room} is not a room key in ${COMMUNITY_FILE}`);
21613
+ }
21614
+ }
21615
+ }
21616
+ }
21617
+ return problems2;
20994
21618
  }
20995
21619
  function readRooms(raw, problems2) {
20996
21620
  if (raw === void 0 || raw === null) return [];
@@ -21062,30 +21686,38 @@ function renderCommunityYaml(files) {
21062
21686
  name: room.name,
21063
21687
  kind: room.kind,
21064
21688
  ...room.archived ? { archived: true } : {}
21065
- }))
21689
+ })),
21690
+ courses: files.courses.map((course) => course.slug),
21691
+ ...files.synced_courses ? { synced: { courses: files.synced_courses } } : {}
21066
21692
  };
21067
- return yaml12.dump(doc, YAML_DUMP_OPTIONS);
21693
+ return yaml13.dump(doc, YAML_DUMP_OPTIONS);
21068
21694
  }
21069
21695
  function renderRules(rules) {
21070
21696
  const text = toLf(rules).replace(/\n+$/, "");
21071
21697
  return text === "" ? "" : `${text}
21072
21698
  `;
21073
21699
  }
21074
- function writeCommunityFolder(root, folder, files) {
21075
- const written = [];
21076
- for (const [name, content] of [
21700
+ function renderCommunityFiles(files) {
21701
+ return [
21077
21702
  [COMMUNITY_FILE, renderCommunityYaml(files)],
21078
21703
  [RULES_FILE, renderRules(files.rules)]
21079
- ]) {
21080
- const abs = path24.join(folder, name);
21081
- const bytes = Buffer.from(content, "utf-8");
21082
- if (fileHasBytes(abs, bytes)) continue;
21083
- if (!writeFileNoFollow(root, abs, bytes)) {
21084
- throw symlinkRefusal(path24.relative(path24.dirname(root), abs), "file");
21704
+ ];
21705
+ }
21706
+ function writeCommunityFiles(root, folder, files) {
21707
+ return renderCommunityFiles(files).flatMap(([name, content]) => writeFileIfChangedNoFollow(root, path25.join(folder, name), content) ? [name] : []);
21708
+ }
21709
+ function communityFilesSaver(root, folder, files) {
21710
+ const saved = new Map(renderCommunityFiles(files));
21711
+ return () => {
21712
+ for (const [name, content] of renderCommunityFiles(files)) {
21713
+ if (saved.get(name) === content) continue;
21714
+ writeFileIfChangedNoFollow(root, path25.join(folder, name), content);
21715
+ saved.set(name, content);
21085
21716
  }
21086
- written.push(name);
21087
- }
21088
- return written;
21717
+ };
21718
+ }
21719
+ function writeCommunityFolder(root, folder, files) {
21720
+ return [...writeCommunityFiles(root, folder, files), ...files.courses.flatMap((course) => writeCourseFolder(root, folder, course))];
21089
21721
  }
21090
21722
  var TOP_KEYS, SETTINGS_KEYS, ROOM_KEYS;
21091
21723
  var init_files = __esm({
@@ -21099,20 +21731,1365 @@ var init_files = __esm({
21099
21731
  init_community_workspace();
21100
21732
  init_utils();
21101
21733
  init_model();
21102
- TOP_KEYS = ["community_id", "name", "settings", "join_link_enabled", "join_link_rev", "rooms"];
21734
+ init_course_files();
21735
+ TOP_KEYS = ["community_id", "name", "settings", "join_link_enabled", "join_link_rev", "rooms", "courses", "synced"];
21103
21736
  SETTINGS_KEYS = ["rev", "slow_mode_seconds", "courses_label"];
21104
21737
  ROOM_KEYS = ["key", "id", "rev", "name", "kind", "archived"];
21105
21738
  }
21106
21739
  });
21107
21740
 
21741
+ // src/communities/listing.ts
21742
+ function lineDiff(from, to) {
21743
+ const a = from ? sanitizeTerminalText(from).split("\n") : [];
21744
+ const b = to ? sanitizeTerminalText(to).split("\n") : [];
21745
+ if (a.length * b.length > 25e4) return [...a.map((l) => `- ${l}`), ...b.map((l) => `+ ${l}`)];
21746
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
21747
+ for (let i2 = a.length - 1; i2 >= 0; i2--) {
21748
+ for (let j2 = b.length - 1; j2 >= 0; j2--) {
21749
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
21750
+ }
21751
+ }
21752
+ const out = [];
21753
+ let i = 0;
21754
+ let j = 0;
21755
+ while (i < a.length || j < b.length) {
21756
+ if (i < a.length && j < b.length && a[i] === b[j]) {
21757
+ i++;
21758
+ j++;
21759
+ } else if (j >= b.length || i < a.length && lcs[i + 1][j] >= lcs[i][j + 1]) {
21760
+ out.push(`- ${a[i++]}`);
21761
+ } else {
21762
+ out.push(`+ ${b[j++]}`);
21763
+ }
21764
+ }
21765
+ return out;
21766
+ }
21767
+ function printSection(title, lines) {
21768
+ if (lines.length === 0) return;
21769
+ console.log(`
21770
+ ${title}`);
21771
+ for (const line of lines) console.log(` ${line}`);
21772
+ }
21773
+ function printListing(listing) {
21774
+ printSection("Changes:", listing.changes);
21775
+ printSection("Changed on the server since your last pull \u2014 left as is; pull to take it:", listing.serverChanged);
21776
+ printSection("Kept \u2014 edited in your files, not pushed yet:", listing.kept);
21777
+ printSection("Not in your files \u2014 left as is:", listing.leftAsIs);
21778
+ printSection("Conflicts \u2014 changed in your files and on the server since your last pull:", listing.conflicts);
21779
+ }
21780
+ var show, emptyListing;
21781
+ var init_listing = __esm({
21782
+ "src/communities/listing.ts"() {
21783
+ "use strict";
21784
+ init_terminal_output();
21785
+ show = (text) => JSON.stringify(sanitizeTerminalText(text));
21786
+ emptyListing = () => ({ changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] });
21787
+ }
21788
+ });
21789
+
21790
+ // src/communities/order.ts
21791
+ function idsOf(entries) {
21792
+ return new Set(entries.flatMap((entry) => entry.id === void 0 ? [] : [entry.id]));
21793
+ }
21794
+ function restrict(order, ids) {
21795
+ const keep = new Set(ids);
21796
+ return order.filter((id) => keep.has(id));
21797
+ }
21798
+ function mergeOrder(listed2, server, named) {
21799
+ const listedIds2 = idsOf(listed2);
21800
+ const first = [];
21801
+ const after = /* @__PURE__ */ new Map();
21802
+ let anchor = null;
21803
+ for (const id of server) {
21804
+ if (listedIds2.has(id)) {
21805
+ anchor = id;
21806
+ } else if (!named.has(id)) {
21807
+ if (anchor === null) first.push(id);
21808
+ else after.set(anchor, [...after.get(anchor) ?? [], id]);
21809
+ }
21810
+ }
21811
+ const slots = first.map((id) => ({ untracked: id }));
21812
+ for (const child of listed2) {
21813
+ slots.push({ entry: child.entry });
21814
+ if (child.id !== void 0) for (const id of after.get(child.id) ?? []) slots.push({ untracked: id });
21815
+ }
21816
+ return slots;
21817
+ }
21818
+ function filesOrder(listed2, server, named) {
21819
+ const byEntry = new Map(listed2.map((child) => [child.entry, child.id]));
21820
+ return mergeOrder(listed2, server, named).flatMap((slot) => {
21821
+ const id = "untracked" in slot ? slot.untracked : byEntry.get(slot.entry);
21822
+ return id === void 0 ? [] : [id];
21823
+ });
21824
+ }
21825
+ function placeAt(list, id, position) {
21826
+ const at = list.indexOf(id);
21827
+ if (at >= 0) list.splice(at, 1);
21828
+ list.splice(Math.min(position, list.length), 0, id);
21829
+ }
21830
+ function sameOrder(a, b) {
21831
+ return a.length === b.length && a.every((id, i) => id === b[i]);
21832
+ }
21833
+ function reorderMoves(current, target) {
21834
+ const list = [...current];
21835
+ const present = target.filter((id) => list.includes(id));
21836
+ const stay = longestRising(present, present.map((id) => list.indexOf(id)));
21837
+ const moves = [];
21838
+ target.forEach((id, i) => {
21839
+ if (stay.has(id)) return;
21840
+ const others = list.filter((other) => other !== id);
21841
+ const position = i === 0 ? 0 : others.indexOf(target[i - 1]) + 1;
21842
+ moves.push({ id, position });
21843
+ placeAt(list, id, position);
21844
+ });
21845
+ const placed = list.filter((id) => target.includes(id));
21846
+ if (!sameOrder(placed, target)) {
21847
+ throw new Error(`reorderMoves: ${JSON.stringify(current)} \u2192 ${JSON.stringify(target)} left ${JSON.stringify(placed)}`);
21848
+ }
21849
+ return moves;
21850
+ }
21851
+ function longestRising(ids, positions) {
21852
+ const length = positions.map(() => 1);
21853
+ const previous = positions.map(() => -1);
21854
+ let best = -1;
21855
+ positions.forEach((position, i) => {
21856
+ for (let j = 0; j < i; j++) {
21857
+ if (positions[j] < position && length[j] + 1 > length[i]) {
21858
+ length[i] = length[j] + 1;
21859
+ previous[i] = j;
21860
+ }
21861
+ }
21862
+ if (best === -1 || length[i] > length[best]) best = i;
21863
+ });
21864
+ const stay = /* @__PURE__ */ new Set();
21865
+ for (let i = best; i >= 0; i = previous[i]) stay.add(ids[i]);
21866
+ return stay;
21867
+ }
21868
+ var init_order = __esm({
21869
+ "src/communities/order.ts"() {
21870
+ "use strict";
21871
+ }
21872
+ });
21873
+
21874
+ // src/communities/courses.ts
21875
+ import { isDeepStrictEqual } from "util";
21876
+ import * as path26 from "path";
21877
+ function roomIdOf(rooms, key) {
21878
+ if (key === void 0) return null;
21879
+ const room = rooms.find((entry) => entry.key === key);
21880
+ return room?.id ?? `${PENDING_ROOM}${key}`;
21881
+ }
21882
+ function fileLesson(rooms, lesson) {
21883
+ return lessonContent({
21884
+ title: lesson.title,
21885
+ body: lesson.body,
21886
+ video: lesson.video_url === void 0 ? null : parseCommunityLessonVideoUrl(lesson.video_url),
21887
+ links: lesson.links,
21888
+ room_id: roomIdOf(rooms, lesson.room),
21889
+ requires_paid_plan: lesson.requires_paid_plan,
21890
+ published: lesson.published
21891
+ });
21892
+ }
21893
+ function roomName(rooms, id) {
21894
+ if (id === null) return "none";
21895
+ if (id.startsWith(PENDING_ROOM)) return id.slice(PENDING_ROOM.length);
21896
+ return rooms.find((room) => room.id === id)?.key ?? id;
21897
+ }
21898
+ function roomKeyOf(rooms, roomId) {
21899
+ if (roomId === null) return void 0;
21900
+ const room = rooms.find((entry) => entry.id === roomId);
21901
+ if (!room) throw expected(`room ${roomId} is not one of the rooms ${COMMUNITY_FILE} lists: pull the community first.`);
21902
+ return room.key;
21903
+ }
21904
+ function takeLesson(rooms, lesson, server, fields) {
21905
+ const file = fileLesson(rooms, lesson);
21906
+ const side = lessonContent(server);
21907
+ const take = (field, differs) => differs && (fields === void 0 || fields.has(field));
21908
+ if (take("title", file.title !== side.title)) lesson.title = server.title;
21909
+ if (take("body", file.body !== side.body)) lesson.body = side.body;
21910
+ if (take("video_url", !isDeepStrictEqual(file.video, side.video))) lesson.video_url = side.video ? communityLessonVideoWatchUrl(side.video) : void 0;
21911
+ if (take("links", !isDeepStrictEqual(file.links, side.links))) lesson.links = server.links.map((link) => ({ title: link.title, url: link.url }));
21912
+ if (take("room_id", file.room_id !== side.room_id)) lesson.room = roomKeyOf(rooms, side.room_id);
21913
+ if (take("requires_paid_plan", file.requires_paid_plan !== side.requires_paid_plan)) lesson.requires_paid_plan = side.requires_paid_plan;
21914
+ if (take("published", file.published !== side.published)) lesson.published = side.published;
21915
+ }
21916
+ function takeCourse(folder, server, fields) {
21917
+ const file = courseContent(folder);
21918
+ const side = courseContent(server);
21919
+ const take = (field, differs) => differs && (fields === void 0 || fields.has(field));
21920
+ if (take("title", file.title !== side.title)) folder.title = server.title;
21921
+ if (take("description", file.description !== side.description)) folder.description = server.description;
21922
+ if (take("published", file.published !== side.published)) folder.published = side.published;
21923
+ }
21924
+ function orderVerdict(list, base, server) {
21925
+ const onServer = new Set(server);
21926
+ if (base === void 0) {
21927
+ const shared2 = list.filter((id) => onServer.has(id));
21928
+ return sameOrder(shared2, restrict(server, shared2)) ? "converged" : "conflict";
21929
+ }
21930
+ const inBase = new Set(base);
21931
+ const shared = list.filter((id) => inBase.has(id) && onServer.has(id));
21932
+ const added = list.filter((id) => !inBase.has(id) && onServer.has(id));
21933
+ if (added.length > 0 && !sameGaps(added, shared, list, server)) return "conflict";
21934
+ const was = restrict(base, shared);
21935
+ const now = restrict(server, shared);
21936
+ return threeWay(!sameOrder(shared, was), !sameOrder(now, was), sameOrder(shared, now));
21937
+ }
21938
+ function sameGaps(added, shared, a, b) {
21939
+ const before = (order, id) => new Set(order.slice(0, order.indexOf(id)).filter((other) => shared.includes(other)));
21940
+ const same = (x, y) => x.size === y.size && [...x].every((id) => y.has(id));
21941
+ return sameOrder(restrict(a, added), restrict(b, added)) && added.every((id) => same(before(a, id), before(b, id)));
21942
+ }
21943
+ function placementVerdict(files, base, server) {
21944
+ if (base === void 0) return files === server ? "converged" : "conflict";
21945
+ return threeWay(files !== base, server !== base, files === server);
21946
+ }
21947
+ function planOrder(entries, server, base) {
21948
+ return { state: orderVerdict([...idsOf(entries)], base, server), server: [...server], base: base && [...base] };
21949
+ }
21950
+ function baseModule(synced2, lessonId) {
21951
+ const holding = Object.entries(synced2?.lessons ?? {}).filter(([, ids]) => ids.includes(lessonId));
21952
+ return holding.length === 1 ? holding[0][0] : void 0;
21953
+ }
21954
+ function courseName(files, server, courseId) {
21955
+ const folder = files.courses.find((course) => course.id === courseId);
21956
+ return folder ? `course ${folder.slug}` : `course ${show(server.course.get(courseId)?.course.title ?? courseId)}`;
21957
+ }
21958
+ function planCourses(files, server) {
21959
+ const problems2 = [];
21960
+ const named = idsOf(files.courses);
21961
+ return {
21962
+ server,
21963
+ order: planOrder(files.courses, server.ids, files.synced_courses),
21964
+ courses: files.courses.map((folder) => planCourse(files, folder, server, problems2)),
21965
+ untracked: server.courses.filter((course) => !named.has(course.course.course_id)),
21966
+ problems: problems2
21967
+ };
21968
+ }
21969
+ function planCourse(files, folder, server, problems2) {
21970
+ const found = folder.id === void 0 ? void 0 : server.course.get(folder.id);
21971
+ if (folder.id !== void 0 && !found) problems2.push(`course ${folder.slug}: ${folder.id} is not a course of this community`);
21972
+ const courseId = found?.course.course_id;
21973
+ const plan = {
21974
+ folder,
21975
+ server: null,
21976
+ export: found ?? null,
21977
+ modulesOrder: null,
21978
+ modules: [],
21979
+ lessons: [],
21980
+ untrackedModules: [],
21981
+ untrackedLessons: []
21982
+ };
21983
+ const moduleIds = idsOf(folder.modules);
21984
+ const lessonIds = idsOf(folder.modules.flatMap((module) => module.lessons));
21985
+ if (found) {
21986
+ const side = serverCourse(found.course);
21987
+ plan.server = { state: classify(folder.rev, courseRev(found.course.course_id, courseContent(folder)), side.rev), server: found.course, serverRev: side.rev };
21988
+ plan.modulesOrder = planOrder(folder.modules, found.modules.map((m) => m.module_id), folder.synced?.modules);
21989
+ plan.untrackedModules = found.modules.filter((module) => !moduleIds.has(module.module_id));
21990
+ plan.untrackedLessons = found.modules.flatMap((module) => module.lessons).filter((lesson) => !lessonIds.has(lesson.lesson_id));
21991
+ }
21992
+ const elsewhere = (what, at, id, owner) => {
21993
+ if (owner === void 0) problems2.push(`${what} ${at}: ${id} is not a ${what} of this community`);
21994
+ else problems2.push(`${what} ${at}: ${what} ${id} is one of ${courseName(files, server, owner)}'s on the server, and a ${what} does not move between courses`);
21995
+ };
21996
+ for (const entry of folder.modules) {
21997
+ const at = `${folder.slug}/${entry.key}`;
21998
+ const module = entry.id === void 0 ? void 0 : server.module.get(entry.id);
21999
+ if (entry.id !== void 0 && module?.courseId !== courseId) elsewhere("module", at, entry.id, module?.courseId);
22000
+ const mine = module && module.courseId === courseId ? module.module : void 0;
22001
+ const modulePlan = { course: folder, entry, server: null, lessonsOrder: null };
22002
+ if (mine) {
22003
+ const side = serverModule(mine);
22004
+ modulePlan.server = { state: classify(entry.rev, moduleRev(mine.module_id, moduleContent(entry)), side.rev), server: mine, serverRev: side.rev };
22005
+ modulePlan.lessonsOrder = planOrder(entry.lessons, mine.lessons.map((l) => l.lesson_id), folder.synced?.lessons[mine.module_id]);
22006
+ }
22007
+ plan.modules.push(modulePlan);
22008
+ for (const lesson of entry.lessons) {
22009
+ const where = server.lesson.get(lesson.id ?? "");
22010
+ if (lesson.id !== void 0 && where?.courseId !== courseId) elsewhere("lesson", `${folder.slug}/${lesson.slug}`, lesson.id, where?.courseId);
22011
+ const lessonPlan = { course: folder, module: entry, lesson, server: null, placement: null };
22012
+ if (where && where.courseId === courseId) {
22013
+ const side = serverLesson(where.lesson);
22014
+ lessonPlan.server = { state: classify(lesson.rev, lessonRev(where.lesson.lesson_id, fileLesson(files.rooms, lesson)), side.rev), server: where.lesson, serverRev: side.rev };
22015
+ const filesModule = plannedIds.module(folder, entry);
22016
+ const base = baseModule(folder.synced, where.lesson.lesson_id);
22017
+ lessonPlan.placement = { state: placementVerdict(filesModule, base, where.moduleId), base, files: filesModule, server: where.moduleId };
22018
+ }
22019
+ plan.lessons.push(lessonPlan);
22020
+ }
22021
+ }
22022
+ return plan;
22023
+ }
22024
+ function courseCapProblems(files, plan) {
22025
+ const problems2 = [];
22026
+ const title = (entity) => toLf(entity.title).trim();
22027
+ const courseCreates = plan.courses.filter((course) => course.folder.id === void 0).map((course) => title(course.folder));
22028
+ const courses = plan.server.courses.length;
22029
+ if (courses + certainlyNew(courseCreates, plan.untracked.map((course) => title(course.course))) > COMMUNITY_COURSES_MAX) {
22030
+ problems2.push(`courses: a community holds at most ${COMMUNITY_COURSES_MAX} courses, drafts included; it has ${courses}, and the files create ${courseCreates.length}`);
22031
+ }
22032
+ const named = idsOf(files.courses.flatMap((course) => course.modules.flatMap((module) => module.lessons)));
22033
+ const lessonCreates = [];
22034
+ for (const course of plan.courses) {
22035
+ const moduleCreates = course.modules.filter((module) => module.entry.id === void 0).map((module) => title(module.entry));
22036
+ const creates = course.lessons.filter((lesson) => lesson.lesson.id === void 0).map((lesson) => title(lesson.lesson));
22037
+ lessonCreates.push(...creates);
22038
+ const modules = course.export?.modules.length ?? 0;
22039
+ const lessons2 = course.export?.modules.reduce((n, module) => n + module.lessons.length, 0) ?? 0;
22040
+ if (modules + certainlyNew(moduleCreates, course.untrackedModules.map(title)) > COMMUNITY_COURSE_MODULES_MAX) {
22041
+ problems2.push(`course ${course.folder.slug}: a course holds at most ${COMMUNITY_COURSE_MODULES_MAX} modules; it has ${modules}, and the files create ${moduleCreates.length}`);
22042
+ }
22043
+ if (lessons2 + certainlyNew(creates, course.untrackedLessons.map(title)) > COMMUNITY_COURSE_LESSONS_MAX) {
22044
+ problems2.push(`course ${course.folder.slug}: a course holds at most ${COMMUNITY_COURSE_LESSONS_MAX} lessons, drafts included; it has ${lessons2}, and the files create ${creates.length}`);
22045
+ }
22046
+ }
22047
+ const lessons = plan.server.lesson.size;
22048
+ const untracked = [...plan.server.lesson.values()].filter(({ lesson }) => !named.has(lesson.lesson_id)).map(({ lesson }) => title(lesson));
22049
+ if (lessons + certainlyNew(lessonCreates, untracked) > COMMUNITY_LESSONS_MAX) {
22050
+ problems2.push(`lessons: a community holds at most ${COMMUNITY_LESSONS_MAX} lessons, drafts included; it has ${lessons}, and the files create ${lessonCreates.length}`);
22051
+ }
22052
+ return problems2;
22053
+ }
22054
+ function certainlyNew(creates, untracked) {
22055
+ const left = [...untracked];
22056
+ return creates.filter((title) => {
22057
+ const at = left.indexOf(title);
22058
+ if (at >= 0) left.splice(at, 1);
22059
+ return at < 0;
22060
+ }).length;
22061
+ }
22062
+ function append(order, id) {
22063
+ order.server.push(id);
22064
+ order.pinned?.push(id);
22065
+ order.touched = true;
22066
+ }
22067
+ function insertAfter(list, item, order) {
22068
+ let at = 0;
22069
+ for (let i = order.indexOf(item) - 1; i >= 0; i--) {
22070
+ const k = list.indexOf(order[i]);
22071
+ if (k >= 0) {
22072
+ at = k + 1;
22073
+ break;
22074
+ }
22075
+ }
22076
+ list.splice(at, 0, item);
22077
+ }
22078
+ function shift(order, id, position) {
22079
+ placeAt(order.server, id, position);
22080
+ order.touched = true;
22081
+ }
22082
+ function leave(order, id) {
22083
+ for (const list of [order.server, order.pinned ?? []]) {
22084
+ const at = list.indexOf(id);
22085
+ if (at >= 0) list.splice(at, 1);
22086
+ }
22087
+ order.touched = true;
22088
+ }
22089
+ function parents(files, sim, ids) {
22090
+ const out = [];
22091
+ const listedOf = (entries, id) => [...idsOf(listedIds(entries, id))];
22092
+ out.push({ what: "courses order", kind: "courses", id: files.community_id, order: sim.courses, courseId: null, listed: listedOf(files.courses, ids.course) });
22093
+ for (const course of files.courses) {
22094
+ const courseId = ids.course(course);
22095
+ const order = courseId === void 0 ? void 0 : sim.modules.get(courseId);
22096
+ if (courseId === void 0 || !order) continue;
22097
+ out.push({
22098
+ what: `modules order of course ${course.slug}`,
22099
+ kind: "modules",
22100
+ id: courseId,
22101
+ order,
22102
+ courseId,
22103
+ listed: listedOf(course.modules, (module) => ids.module(course, module))
22104
+ });
22105
+ }
22106
+ for (const course of files.courses) {
22107
+ const courseId = ids.course(course);
22108
+ if (courseId === void 0 || !sim.modules.has(courseId)) continue;
22109
+ for (const module of course.modules) {
22110
+ const moduleId = ids.module(course, module);
22111
+ const order = moduleId === void 0 ? void 0 : sim.lessons.get(moduleId);
22112
+ if (moduleId === void 0 || !order) continue;
22113
+ out.push({
22114
+ what: `lessons order of module ${course.slug}/${module.key}`,
22115
+ kind: "lessons",
22116
+ id: moduleId,
22117
+ order,
22118
+ courseId,
22119
+ listed: listedOf(module.lessons, (lesson) => ids.lesson(course, lesson))
22120
+ });
22121
+ }
22122
+ }
22123
+ return out;
22124
+ }
22125
+ function listedIds(entries, id) {
22126
+ return entries.map((entry) => ({ entry, id: id(entry) }));
22127
+ }
22128
+ function computePlaces(files, sim, ids, placements) {
22129
+ const out = { moves: [], conflicts: [], reorders: [] };
22130
+ const placementOf = (id) => placements.get(id);
22131
+ for (const parent of parents(files, sim, ids)) {
22132
+ const { order } = parent;
22133
+ if (order.conflict) continue;
22134
+ const lessons = parent.kind === "lessons";
22135
+ const wanted = lessons ? parent.listed.filter((id) => placementOf(id)?.state !== "remote") : parent.listed;
22136
+ const leaving = lessons ? order.server.filter((id) => placementOf(id)?.state === "local" && !parent.listed.includes(id)) : [];
22137
+ const arriving = wanted.filter((id) => !order.server.includes(id));
22138
+ if (order.pinned) {
22139
+ const kept = order.pinned;
22140
+ const shared = wanted.filter((id) => kept.includes(id));
22141
+ if (arriving.length > 0 || !sameOrder(shared, restrict(kept, shared))) out.conflicts.push(parent.what);
22142
+ continue;
22143
+ }
22144
+ if (arriving.length === 0 && sameOrder(restrict(order.server, wanted), wanted)) continue;
22145
+ const target = filesOrder(wanted.map((id) => ({ entry: id, id })), order.server, /* @__PURE__ */ new Set([...wanted, ...leaving]));
22146
+ const from = [...order.server];
22147
+ for (const move of reorderMoves(order.server, target)) {
22148
+ if (lessons) {
22149
+ const left = sim.at.get(move.id);
22150
+ if (left === void 0) throw new Error(`computePlaces: lesson ${move.id} is in no module`);
22151
+ if (left !== parent.id) leave(sim.lessons.get(left), move.id);
22152
+ sim.at.set(move.id, parent.id);
22153
+ }
22154
+ shift(order, move.id, move.position);
22155
+ const { id, position } = move;
22156
+ out.moves.push(
22157
+ parent.kind === "courses" ? { kind: "course", id, position } : parent.kind === "modules" ? { kind: "module", id, courseId: parent.id, position } : { kind: "lesson", id, courseId: parent.courseId, moduleId: parent.id, position }
22158
+ );
22159
+ }
22160
+ out.reorders.push({ what: parent.what, from, to: target });
22161
+ }
22162
+ return out;
22163
+ }
22164
+ function outcomeUnknown(err) {
22165
+ return err?.isExpected !== true;
22166
+ }
22167
+ function coursePatch(folder, now, publish) {
22168
+ const file = courseContent(folder);
22169
+ const server = courseContent(now);
22170
+ const body = {
22171
+ ...file.title !== server.title ? { title: folder.title } : {},
22172
+ ...file.description !== server.description ? { description: folder.description } : {},
22173
+ ...file.published !== server.published && (publish || !file.published) ? { published: file.published } : {}
22174
+ };
22175
+ return Object.keys(body).length > 0 ? body : null;
22176
+ }
22177
+ function lessonPatch(rooms, lesson, now, waitForRooms) {
22178
+ const file = fileLesson(rooms, lesson);
22179
+ const server = lessonContent(now);
22180
+ const pending = file.room_id?.startsWith(PENDING_ROOM) ?? false;
22181
+ if (pending && !waitForRooms) throw new Error(`lessonPatch: room ${lesson.room} has no id yet`);
22182
+ const body = {
22183
+ ...file.title !== server.title ? { title: lesson.title } : {},
22184
+ ...file.body !== server.body ? { body: lesson.body } : {},
22185
+ ...!isDeepStrictEqual(file.video, server.video) ? { video_url: lesson.video_url ?? null } : {},
22186
+ ...!isDeepStrictEqual(file.links, server.links) ? { links: lesson.links } : {},
22187
+ ...file.room_id !== server.room_id && !pending ? { room_id: file.room_id } : {},
22188
+ ...file.requires_paid_plan !== server.requires_paid_plan ? { requires_paid_plan: lesson.requires_paid_plan } : {},
22189
+ ...file.published !== server.published ? { published: lesson.published } : {}
22190
+ };
22191
+ return Object.keys(body).length > 0 ? body : null;
22192
+ }
22193
+ function courseChanges(from, to) {
22194
+ return [
22195
+ ...from.title !== to.title ? [`title ${show(from.title)} \u2192 ${show(to.title)}`] : [],
22196
+ ...from.description !== to.description ? ["description"] : [],
22197
+ ...from.published !== to.published ? [to.published ? "published" : "unpublished"] : []
22198
+ ];
22199
+ }
22200
+ function lessonChanges(from, to, room) {
22201
+ return [
22202
+ ...from.title !== to.title ? [`title ${show(from.title)} \u2192 ${show(to.title)}`] : [],
22203
+ ...from.body !== to.body ? ["text"] : [],
22204
+ ...!isDeepStrictEqual(from.video, to.video) ? [to.video ? from.video ? "video" : "video added" : "video removed"] : [],
22205
+ ...!isDeepStrictEqual(from.links, to.links) ? ["links"] : [],
22206
+ ...from.room_id !== to.room_id ? [`room ${room(from.room_id)} \u2192 ${room(to.room_id)}`] : [],
22207
+ ...from.requires_paid_plan !== to.requires_paid_plan ? [to.requires_paid_plan ? "needs a paid plan" : "no longer needs a paid plan"] : [],
22208
+ ...from.published !== to.published ? [to.published ? "published" : "unpublished"] : []
22209
+ ];
22210
+ }
22211
+ async function lessonWrite(course, lesson, write) {
22212
+ try {
22213
+ await write();
22214
+ } catch (err) {
22215
+ const reason = extractApiErrorReason(err);
22216
+ if (reason === null || !LESSON_TEXT_REASONS.includes(reason)) throw err;
22217
+ throw new Error(`${extractApiMessage(err)} \u2014 change the lesson's text in ${lessonPath(course, lesson)}`);
22218
+ }
22219
+ }
22220
+ function lessonFromServer(rooms, lesson, slug) {
22221
+ const room = roomKeyOf(rooms, lesson.room_id);
22222
+ return {
22223
+ slug,
22224
+ id: lesson.lesson_id,
22225
+ rev: serverLesson(lesson).rev,
22226
+ title: lesson.title,
22227
+ published: lesson.published,
22228
+ ...room === void 0 ? {} : { room },
22229
+ ...lesson.video ? { video_url: communityLessonVideoWatchUrl(lesson.video) } : {},
22230
+ links: lesson.links.map((link) => ({ title: link.title, url: link.url })),
22231
+ requires_paid_plan: lesson.requires_paid_plan,
22232
+ body: toLf(lesson.body)
22233
+ };
22234
+ }
22235
+ function moduleFromServer(module, key) {
22236
+ return { key, id: module.module_id, rev: serverModule(module).rev, title: module.title, lessons: [] };
22237
+ }
22238
+ function syncedFromServer(course) {
22239
+ return {
22240
+ modules: course.modules.map((module) => module.module_id),
22241
+ lessons: Object.fromEntries(course.modules.map((module) => [module.module_id, module.lessons.map((lesson) => lesson.lesson_id)]))
22242
+ };
22243
+ }
22244
+ function courseFromServer(rooms, course, slug) {
22245
+ const keys = /* @__PURE__ */ new Set();
22246
+ const slugs = /* @__PURE__ */ new Set();
22247
+ return {
22248
+ slug,
22249
+ id: course.course.course_id,
22250
+ rev: serverCourse(course.course).rev,
22251
+ title: course.course.title,
22252
+ description: course.course.description,
22253
+ published: course.course.published,
22254
+ modules: course.modules.map((module) => {
22255
+ const key = localName(module.title, module.module_id, keys, "module");
22256
+ keys.add(key);
22257
+ const entry = moduleFromServer(module, key);
22258
+ entry.lessons = module.lessons.map((lesson) => {
22259
+ const lessonSlug = localName(lesson.title, lesson.lesson_id, slugs, "lesson");
22260
+ slugs.add(lessonSlug);
22261
+ return lessonFromServer(rooms, lesson, lessonSlug);
22262
+ });
22263
+ return entry;
22264
+ }),
22265
+ synced: syncedFromServer(course),
22266
+ unlisted: []
22267
+ };
22268
+ }
22269
+ function coursesFromServer(files, server) {
22270
+ const slugs = /* @__PURE__ */ new Set();
22271
+ files.courses = server.courses.map((course) => {
22272
+ const slug = localName(course.course.title, course.course.course_id, slugs, "course");
22273
+ slugs.add(slug);
22274
+ return courseFromServer(files.rooms, course, slug);
22275
+ });
22276
+ files.synced_courses = server.ids;
22277
+ }
22278
+ function takeOrder(entries, placed) {
22279
+ const out = [...placed];
22280
+ for (const entry of entries) if (!out.includes(entry)) insertAfter(out, entry, entries);
22281
+ return out;
22282
+ }
22283
+ function mergeArrivals(entries, server, arrivals) {
22284
+ const named = new Set(server.filter((id) => !arrivals.has(id)));
22285
+ return mergeOrder(listed(entries), server, named).flatMap((slot) => "entry" in slot ? [slot.entry] : arrivals.get(slot.untracked) ?? []);
22286
+ }
22287
+ function pullCourses(files, plan, takes, note, listing) {
22288
+ for (const course of plan.courses) pullContent(files, course, takes, note, listing);
22289
+ const tracked = new Map(files.courses.flatMap((course) => course.id === void 0 ? [] : [[course.id, course]]));
22290
+ const held = new Set(files.unlistedCourses.flatMap((folder) => folder.id === null ? [] : [folder.id]));
22291
+ const slugs = new Set([...files.courses.map((course) => course.slug), ...files.unlistedCourses.map((folder) => folder.name)].map((name) => name.toLowerCase()));
22292
+ const courseFor = (id) => {
22293
+ const known = tracked.get(id);
22294
+ if (known) return known;
22295
+ const course = plan.server.course.get(id);
22296
+ if (held.has(id)) {
22297
+ listing.kept.push(`course ${show(course.course.title)} (${id}): its folder is not listed in ${COMMUNITY_FILE} courses \u2014 list it to pull it`);
22298
+ return null;
22299
+ }
22300
+ const slug = localName(course.course.title, id, slugs, "course");
22301
+ slugs.add(slug);
22302
+ listing.changes.push(`+ course ${slug}: ${show(course.course.title)}`);
22303
+ return courseFromServer(files.rooms, course, slug);
22304
+ };
22305
+ const order = plan.order;
22306
+ if (takes(order.state)) {
22307
+ files.courses = takeOrder(files.courses, order.server.flatMap((id) => courseFor(id) ?? []));
22308
+ listing.changes.push("~ courses order");
22309
+ } else {
22310
+ const arrivals = new Map(order.server.filter((id) => !tracked.has(id)).flatMap((id) => {
22311
+ const course = courseFor(id);
22312
+ return course ? [[id, course]] : [];
22313
+ }));
22314
+ files.courses = mergeArrivals(files.courses, order.server, arrivals);
22315
+ note(order.state, "courses order");
22316
+ }
22317
+ if (synced(order.state, takes)) files.synced_courses = [...order.server];
22318
+ for (const course of plan.courses) if (course.export) pullTree(files, plan.server, course, takes, note, listing);
22319
+ }
22320
+ function pullContent(files, course, takes, note, listing) {
22321
+ const { folder } = course;
22322
+ if (course.server) {
22323
+ const { state, server, serverRev } = course.server;
22324
+ if (takes(state)) {
22325
+ listing.changes.push(`~ course ${folder.slug}: ${courseChanges(courseContent(folder), courseContent(server)).join("; ")}`);
22326
+ takeCourse(folder, server);
22327
+ }
22328
+ if (takes(state) || state === "converged") folder.rev = serverRev;
22329
+ note(state, `course ${folder.slug}`);
22330
+ } else if (folder.id === void 0) {
22331
+ listing.kept.push(`course ${folder.slug} (to create)`);
22332
+ }
22333
+ for (const module of course.modules) {
22334
+ const at = `module ${folder.slug}/${module.entry.key}`;
22335
+ if (!module.server) {
22336
+ if (module.entry.id === void 0) listing.kept.push(`${at} (to create)`);
22337
+ continue;
22338
+ }
22339
+ const { state, server, serverRev } = module.server;
22340
+ if (takes(state)) {
22341
+ listing.changes.push(`~ ${at}: title ${show(module.entry.title)} \u2192 ${show(server.title)}`);
22342
+ if (moduleContent(module.entry).title !== moduleContent(server).title) module.entry.title = server.title;
22343
+ }
22344
+ if (takes(state) || state === "converged") module.entry.rev = serverRev;
22345
+ note(state, at);
22346
+ }
22347
+ for (const lesson of course.lessons) {
22348
+ const at = `lesson ${folder.slug}/${lesson.lesson.slug}`;
22349
+ if (!lesson.server) {
22350
+ if (lesson.lesson.id === void 0) listing.kept.push(`${at} (to create)`);
22351
+ continue;
22352
+ }
22353
+ const { state, server, serverRev } = lesson.server;
22354
+ if (takes(state)) {
22355
+ const changes = lessonChanges(fileLesson(files.rooms, lesson.lesson), lessonContent(server), (id) => roomName(files.rooms, id));
22356
+ listing.changes.push(`~ ${at}: ${changes.join("; ")}`);
22357
+ takeLesson(files.rooms, lesson.lesson, server);
22358
+ }
22359
+ if (takes(state) || state === "converged") lesson.lesson.rev = serverRev;
22360
+ note(state, at);
22361
+ }
22362
+ }
22363
+ function pullTree(files, index, course, takes, note, listing) {
22364
+ const { folder } = course;
22365
+ const server = course.export;
22366
+ const keys = new Set(folder.modules.map((module) => module.key));
22367
+ const unlisted = folder.unlisted.map((file) => ({ slug: path26.basename(file.name, LESSON_FILE_EXT), id: file.id }));
22368
+ const slugs = new Set([...folder.modules.flatMap((module) => module.lessons.map((lesson) => lesson.slug)), ...unlisted.map((file) => file.slug)].map((name) => name.toLowerCase()));
22369
+ const held = new Set(unlisted.flatMap((file) => file.id === null ? [] : [file.id]));
22370
+ const fresh = /* @__PURE__ */ new Set();
22371
+ const moduleFor = (id) => {
22372
+ const known = folder.modules.find((module2) => module2.id === id);
22373
+ if (known) return known;
22374
+ const { module } = index.module.get(id);
22375
+ const key = localName(module.title, id, keys, "module");
22376
+ keys.add(key);
22377
+ listing.changes.push(`+ module ${folder.slug}/${key}: ${show(module.title)}`);
22378
+ const entry = moduleFromServer(module, key);
22379
+ fresh.add(entry);
22380
+ return entry;
22381
+ };
22382
+ const modulesOrder = course.modulesOrder;
22383
+ const what = `modules order of course ${folder.slug}`;
22384
+ if (takes(modulesOrder.state)) {
22385
+ folder.modules = takeOrder(folder.modules, modulesOrder.server.flatMap((id) => moduleFor(id) ?? []));
22386
+ listing.changes.push(`~ ${what}`);
22387
+ } else {
22388
+ const arrivals = new Map(modulesOrder.server.filter((id) => !folder.modules.some((m) => m.id === id)).flatMap((id) => {
22389
+ const entry = moduleFor(id);
22390
+ return entry ? [[id, entry]] : [];
22391
+ }));
22392
+ folder.modules = mergeArrivals(folder.modules, modulesOrder.server, arrivals);
22393
+ note(modulesOrder.state, what);
22394
+ }
22395
+ const entryOf = (moduleId) => folder.modules.find((module) => module.id === moduleId);
22396
+ const home = /* @__PURE__ */ new Map();
22397
+ const keptApart = /* @__PURE__ */ new Set();
22398
+ for (const lesson of course.lessons) {
22399
+ const { placement } = lesson;
22400
+ if (!placement) {
22401
+ home.set(lesson.lesson, lesson.module);
22402
+ continue;
22403
+ }
22404
+ const at = `lesson ${folder.slug}/${lesson.lesson.slug}`;
22405
+ if (placement.state === "conflict") note("conflict", `${at}: moved in the files and on the server`);
22406
+ if (placement.state === "local") listing.kept.push(`${at}: moved to module ${lesson.module.key}`);
22407
+ const taken = takes(placement.state) && placement.state !== "converged";
22408
+ const to = taken ? entryOf(placement.server) : void 0;
22409
+ if (to && to !== lesson.module) listing.changes.push(`~ ${at}: moved to module ${to.key}`);
22410
+ if (placement.state === "conflict" && !taken) keptApart.add(lesson);
22411
+ home.set(lesson.lesson, to ?? lesson.module);
22412
+ }
22413
+ const named = idsOf(course.lessons.map((lesson) => lesson.lesson));
22414
+ for (const module of server.modules) {
22415
+ const entry = entryOf(module.module_id);
22416
+ if (!entry) continue;
22417
+ for (const lesson of module.lessons) {
22418
+ if (named.has(lesson.lesson_id)) continue;
22419
+ if (held.has(lesson.lesson_id)) {
22420
+ listing.kept.push(`lesson ${show(lesson.title)} (${lesson.lesson_id}) of course ${folder.slug}: its file is listed by no module \u2014 list it to pull it`);
22421
+ continue;
22422
+ }
22423
+ const slug = localName(lesson.title, lesson.lesson_id, slugs, "lesson");
22424
+ slugs.add(slug);
22425
+ listing.changes.push(`+ lesson ${folder.slug}/${slug}: ${show(lesson.title)}`);
22426
+ home.set(lessonFromServer(files.rooms, lesson, slug), entry);
22427
+ }
22428
+ }
22429
+ for (const entry of folder.modules) {
22430
+ const members = [...home].filter(([, at]) => at === entry).map(([lesson]) => lesson);
22431
+ const own = entry.lessons.filter((lesson) => lesson.id === void 0 || members.includes(lesson));
22432
+ const serverIds = entry.id === void 0 ? [] : index.module.get(entry.id)?.module.lessons.map((lesson) => lesson.lesson_id) ?? [];
22433
+ const plan = course.modules.find((m) => m.entry === entry);
22434
+ const take = fresh.has(entry) || plan?.lessonsOrder !== null && plan?.lessonsOrder !== void 0 && takes(plan.lessonsOrder.state);
22435
+ if (take) {
22436
+ const byId = new Map(members.flatMap((lesson) => lesson.id === void 0 ? [] : [[lesson.id, lesson]]));
22437
+ entry.lessons = takeOrder(own, serverIds.flatMap((id) => byId.get(id) ?? []));
22438
+ if (!fresh.has(entry)) listing.changes.push(`~ lessons order of module ${folder.slug}/${entry.key}`);
22439
+ } else {
22440
+ const arrivals = new Map(members.filter((lesson) => !own.includes(lesson)).flatMap((lesson) => lesson.id === void 0 ? [] : [[lesson.id, lesson]]));
22441
+ entry.lessons = mergeArrivals(own, serverIds, arrivals);
22442
+ if (plan?.lessonsOrder) note(plan.lessonsOrder.state, `lessons order of module ${folder.slug}/${entry.key}`);
22443
+ }
22444
+ }
22445
+ const was = folder.synced;
22446
+ const lessons = {};
22447
+ for (const module of server.modules) {
22448
+ const verdict = course.modules.find((m) => m.entry.id === module.module_id)?.lessonsOrder;
22449
+ if (verdict && !synced(verdict.state, takes) && was?.lessons[module.module_id]) {
22450
+ lessons[module.module_id] = was.lessons[module.module_id];
22451
+ continue;
22452
+ }
22453
+ const kept = [...keptApart].filter((lesson) => lesson.placement.server === module.module_id).map((lesson) => lesson.lesson.id);
22454
+ const based = [...keptApart].filter((lesson) => lesson.placement.base === module.module_id).map((lesson) => lesson.lesson.id);
22455
+ const ids = module.lessons.map((lesson) => lesson.lesson_id).filter((id) => !kept.includes(id));
22456
+ for (const id of based) if (!ids.includes(id)) insertAfter(ids, id, was?.lessons[module.module_id] ?? []);
22457
+ lessons[module.module_id] = ids;
22458
+ }
22459
+ folder.synced = {
22460
+ modules: synced(modulesOrder.state, takes) || !was?.modules ? server.modules.map((module) => module.module_id) : was.modules,
22461
+ lessons
22462
+ };
22463
+ }
22464
+ function coursesChangedSince(before, after) {
22465
+ const states = (server) => {
22466
+ const out = /* @__PURE__ */ new Map();
22467
+ out.set("courses", { label: "courses order", rev: server.ids.join() });
22468
+ for (const course of server.courses) {
22469
+ const courseId = course.course.course_id;
22470
+ const label = `course ${show(course.course.title)}`;
22471
+ out.set(courseId, { label, rev: serverCourse(course.course).rev });
22472
+ out.set(`modules:${courseId}`, { label, rev: course.modules.map((m) => m.module_id).join() });
22473
+ for (const module of course.modules) {
22474
+ const moduleLabel = `module ${show(module.title)}`;
22475
+ out.set(module.module_id, { label: moduleLabel, rev: serverModule(module).rev });
22476
+ out.set(`lessons:${module.module_id}`, { label: moduleLabel, rev: module.lessons.map((l) => l.lesson_id).join() });
22477
+ for (const lesson of module.lessons) out.set(lesson.lesson_id, { label: `lesson ${show(lesson.title)}`, rev: serverLesson(lesson).rev });
22478
+ }
22479
+ }
22480
+ return out;
22481
+ };
22482
+ const was = states(before);
22483
+ const changed = /* @__PURE__ */ new Set();
22484
+ for (const [key, now] of states(after)) if (was.get(key)?.rev !== now.rev) changed.add(now.label);
22485
+ return [...changed];
22486
+ }
22487
+ var ServerCourses, PENDING_ROOM, listed, Sim, freshOrder, realIds, plannedIds, LESSON_TEXT_REASONS, CoursePush, synced;
22488
+ var init_courses = __esm({
22489
+ "src/communities/courses.ts"() {
22490
+ "use strict";
22491
+ init_contracts();
22492
+ init_community_workspace();
22493
+ init_errors2();
22494
+ init_expected();
22495
+ init_course_files();
22496
+ init_listing();
22497
+ init_model();
22498
+ init_order();
22499
+ ServerCourses = class {
22500
+ constructor(courses) {
22501
+ this.courses = courses;
22502
+ for (const course of courses) {
22503
+ const courseId = course.course.course_id;
22504
+ this.course.set(courseId, course);
22505
+ for (const module of course.modules) {
22506
+ this.module.set(module.module_id, { courseId, module });
22507
+ for (const lesson of module.lessons) this.lesson.set(lesson.lesson_id, { courseId, moduleId: module.module_id, lesson });
22508
+ }
22509
+ }
22510
+ }
22511
+ course = /* @__PURE__ */ new Map();
22512
+ module = /* @__PURE__ */ new Map();
22513
+ lesson = /* @__PURE__ */ new Map();
22514
+ get ids() {
22515
+ return this.courses.map((course) => course.course.course_id);
22516
+ }
22517
+ };
22518
+ PENDING_ROOM = "pending:";
22519
+ listed = (entries) => listedIds(entries, (entry) => entry.id);
22520
+ Sim = class _Sim {
22521
+ constructor(courses, modules = /* @__PURE__ */ new Map(), lessons = /* @__PURE__ */ new Map(), at = /* @__PURE__ */ new Map()) {
22522
+ this.courses = courses;
22523
+ this.modules = modules;
22524
+ this.lessons = lessons;
22525
+ this.at = at;
22526
+ }
22527
+ static from(plan) {
22528
+ const order = (ids, verdict) => ({
22529
+ server: [...ids],
22530
+ pinned: verdict?.state === "remote" ? [...verdict.base ?? []] : null,
22531
+ conflict: verdict?.state === "conflict",
22532
+ touched: false
22533
+ });
22534
+ const sim = new _Sim(order(plan.order.server, plan.order));
22535
+ for (const course of plan.server.courses) {
22536
+ const courseId = course.course.course_id;
22537
+ const tracked = plan.courses.find((c) => c.export?.course.course_id === courseId);
22538
+ sim.modules.set(courseId, order(course.modules.map((m) => m.module_id), tracked?.modulesOrder));
22539
+ for (const module of course.modules) {
22540
+ const entry = tracked?.modules.find((m) => m.server?.server.module_id === module.module_id);
22541
+ sim.lessons.set(module.module_id, order(module.lessons.map((l) => l.lesson_id), entry?.lessonsOrder));
22542
+ for (const lesson of module.lessons) sim.at.set(lesson.lesson_id, module.module_id);
22543
+ }
22544
+ }
22545
+ return sim;
22546
+ }
22547
+ clone() {
22548
+ const copy = (order) => ({ ...order, server: [...order.server], pinned: order.pinned && [...order.pinned] });
22549
+ const map = (orders) => new Map([...orders].map(([id, order]) => [id, copy(order)]));
22550
+ return new _Sim(copy(this.courses), map(this.modules), map(this.lessons), new Map(this.at));
22551
+ }
22552
+ };
22553
+ freshOrder = () => ({ server: [], pinned: null, conflict: false, touched: true });
22554
+ realIds = { course: (c) => c.id, module: (_c, m) => m.id, lesson: (_c, l) => l.id };
22555
+ plannedIds = {
22556
+ course: (c) => c.id ?? `new:course:${c.slug}`,
22557
+ module: (c, m) => m.id ?? `new:module:${c.slug}/${m.key}`,
22558
+ lesson: (c, l) => l.id ?? `new:lesson:${c.slug}/${l.slug}`
22559
+ };
22560
+ LESSON_TEXT_REASONS = [
22561
+ COMMUNITY_LESSON_LINE_PREFIX_TOO_WIDE_REASON,
22562
+ COMMUNITY_LESSON_TREE_TOO_DEEP_REASON,
22563
+ COMMUNITY_LESSON_TREE_TOO_LARGE_REASON
22564
+ ];
22565
+ CoursePush = class {
22566
+ constructor(files, plan, remote, save) {
22567
+ this.files = files;
22568
+ this.plan = plan;
22569
+ this.remote = remote;
22570
+ this.save = save;
22571
+ this.sim = Sim.from(plan);
22572
+ for (const lesson of plan.courses.flatMap((course) => course.lessons)) {
22573
+ const { placement } = lesson;
22574
+ if (!placement || lesson.lesson.id === void 0) continue;
22575
+ this.placements.set(lesson.lesson.id, placement);
22576
+ if (placement.state !== "remote") continue;
22577
+ this.movedIn.set(placement.server, /* @__PURE__ */ new Set([...this.movedIn.get(placement.server) ?? [], lesson.lesson.id]));
22578
+ if (placement.base !== void 0) this.movedOut.set(placement.base, [...this.movedOut.get(placement.base) ?? [], lesson.lesson.id]);
22579
+ }
22580
+ for (const course of plan.server.courses) {
22581
+ this.courseNow.set(course.course.course_id, course.course);
22582
+ this.names.set(course.course.course_id, show(course.course.title));
22583
+ for (const module of course.modules) {
22584
+ this.moduleNow.set(module.module_id, module);
22585
+ this.names.set(module.module_id, show(module.title));
22586
+ for (const lesson of module.lessons) {
22587
+ this.lessonNow.set(lesson.lesson_id, lesson);
22588
+ this.names.set(lesson.lesson_id, show(lesson.title));
22589
+ }
22590
+ }
22591
+ }
22592
+ for (const course of files.courses) {
22593
+ this.names.set(plannedIds.course(course), course.slug);
22594
+ for (const module of course.modules) {
22595
+ this.names.set(plannedIds.module(course, module), module.key);
22596
+ for (const lesson of module.lessons) this.names.set(plannedIds.lesson(course, lesson), lesson.slug);
22597
+ }
22598
+ }
22599
+ this.predicted = this.predict();
22600
+ }
22601
+ sim;
22602
+ courseNow = /* @__PURE__ */ new Map();
22603
+ moduleNow = /* @__PURE__ */ new Map();
22604
+ lessonNow = /* @__PURE__ */ new Map();
22605
+ names = /* @__PURE__ */ new Map();
22606
+ /** Files whose synced lists changed since they were last saved. */
22607
+ unsaved = /* @__PURE__ */ new Set();
22608
+ /** Each listed lesson's place's verdict, by id. */
22609
+ placements = /* @__PURE__ */ new Map();
22610
+ /** The lessons the server moved since, which push leaves where they are: by module, those it moved in and out. */
22611
+ movedIn = /* @__PURE__ */ new Map();
22612
+ movedOut = /* @__PURE__ */ new Map();
22613
+ /** The places phase as the plan predicts it, every create landing as a new entity. */
22614
+ predicted;
22615
+ get communityId() {
22616
+ return this.files.community_id;
22617
+ }
22618
+ lessons() {
22619
+ return this.plan.courses.flatMap((course) => course.lessons);
22620
+ }
22621
+ /** The places phase on a copy of the server's orders where every create has landed, last. */
22622
+ predict() {
22623
+ const sim = this.sim.clone();
22624
+ for (const course of this.files.courses) {
22625
+ const courseId = plannedIds.course(course);
22626
+ if (course.id === void 0) {
22627
+ append(sim.courses, courseId);
22628
+ sim.modules.set(courseId, freshOrder());
22629
+ }
22630
+ for (const module of course.modules) {
22631
+ const moduleId = plannedIds.module(course, module);
22632
+ if (module.id === void 0) {
22633
+ const modules = sim.modules.get(courseId);
22634
+ if (!modules) continue;
22635
+ append(modules, moduleId);
22636
+ sim.lessons.set(moduleId, freshOrder());
22637
+ }
22638
+ for (const lesson of module.lessons) {
22639
+ const lessons = sim.lessons.get(moduleId);
22640
+ if (lesson.id !== void 0 || !lessons) continue;
22641
+ const lessonId = plannedIds.lesson(course, lesson);
22642
+ append(lessons, lessonId);
22643
+ sim.at.set(lessonId, moduleId);
22644
+ }
22645
+ }
22646
+ }
22647
+ return computePlaces(this.files, sim, plannedIds, this.placements);
22648
+ }
22649
+ // ── The listing ──
22650
+ roomName = (id) => roomName(this.files.rooms, id);
22651
+ /** Push's listing of the courses, server → files. */
22652
+ list(listing) {
22653
+ const { plan, files } = this;
22654
+ for (const course of plan.courses) {
22655
+ const { folder } = course;
22656
+ if (!course.server) {
22657
+ listing.changes.push(`+ course ${folder.slug}: create ${show(folder.title)}, ${folder.published ? "published once its lessons are in place" : "a draft"}`);
22658
+ } else if (course.server.state === "local") {
22659
+ listing.changes.push(`~ course ${folder.slug}: ${courseChanges(courseContent(course.server.server), courseContent(folder)).join("; ")}`);
22660
+ }
22661
+ for (const module of course.modules) {
22662
+ const at = `${folder.slug}/${module.entry.key}`;
22663
+ if (!module.server) listing.changes.push(`+ module ${at}: create ${show(module.entry.title)}`);
22664
+ else if (module.server.state === "local") listing.changes.push(`~ module ${at}: title ${show(module.server.server.title)} \u2192 ${show(module.entry.title)}`);
22665
+ }
22666
+ for (const lesson of course.lessons) {
22667
+ const at = `${folder.slug}/${lesson.lesson.slug}`;
22668
+ const file = fileLesson(files.rooms, lesson.lesson);
22669
+ if (!lesson.server) {
22670
+ const flags = [...file.published ? ["published"] : [], ...file.room_id ? [`room ${this.roomName(file.room_id)}`] : []];
22671
+ listing.changes.push(`+ lesson ${at}: create ${show(lesson.lesson.title)} in module ${lesson.module.key}${flags.length > 0 ? `, ${flags.join(", ")}` : ""}`);
22672
+ } else if (lesson.server.state === "local") {
22673
+ const server = lessonContent(lesson.server.server);
22674
+ listing.changes.push(`~ lesson ${at}: ${lessonChanges(server, file, this.roomName).join("; ")}`);
22675
+ if (server.body !== file.body) listing.changes.push(...lineDiff(server.body, file.body).map((line) => ` ${line}`));
22676
+ }
22677
+ }
22678
+ }
22679
+ for (const reorder of this.predicted.reorders) {
22680
+ listing.changes.push(`~ ${reorder.what}: ${reorder.from.map(this.name).join(", ")} \u2192 ${reorder.to.map(this.name).join(", ")}`);
22681
+ }
22682
+ const verdicts = (state) => [
22683
+ ...plan.order.state === state ? ["courses order"] : [],
22684
+ ...plan.courses.flatMap((course) => [
22685
+ ...course.server?.state === state ? [`course ${course.folder.slug}`] : [],
22686
+ ...course.modulesOrder?.state === state ? [`modules order of course ${course.folder.slug}`] : [],
22687
+ ...course.modules.flatMap((module) => [
22688
+ ...module.server?.state === state ? [`module ${course.folder.slug}/${module.entry.key}`] : [],
22689
+ ...module.lessonsOrder?.state === state ? [`lessons order of module ${course.folder.slug}/${module.entry.key}`] : []
22690
+ ]),
22691
+ ...course.lessons.flatMap((lesson) => [
22692
+ ...lesson.server?.state === state ? [`lesson ${course.folder.slug}/${lesson.lesson.slug}`] : [],
22693
+ ...lesson.placement?.state === state ? [`lesson ${course.folder.slug}/${lesson.lesson.slug}: ${state === "conflict" ? "moved in the files and on the server" : `moved to module ${this.pathOf(lesson.placement.server)}`}`] : []
22694
+ ])
22695
+ ])
22696
+ ];
22697
+ listing.serverChanged.push(...verdicts("remote"));
22698
+ listing.conflicts.push(.../* @__PURE__ */ new Set([...verdicts("conflict"), ...this.predicted.conflicts]));
22699
+ listing.leftAsIs.push(
22700
+ ...plan.untracked.map((course) => `course ${show(course.course.title)} (${course.course.course_id})`),
22701
+ ...plan.courses.flatMap((course) => [
22702
+ ...course.untrackedModules.map((module) => `module ${show(module.title)} (${module.module_id}) of course ${course.folder.slug}`),
22703
+ ...course.untrackedLessons.map((lesson) => `lesson ${show(lesson.title)} (${lesson.lesson_id}) of course ${course.folder.slug}`)
22704
+ ])
22705
+ );
22706
+ }
22707
+ name = (id) => this.names.get(id) ?? id;
22708
+ /** Revs to refresh without a write: both sides agree, or the files carried no base. Saves the files they live in. */
22709
+ refreshConverged() {
22710
+ const { plan, files } = this;
22711
+ let refreshed = 0;
22712
+ const refresh = (converged, owner, set) => {
22713
+ if (!converged) return;
22714
+ set();
22715
+ this.unsaved.add(owner);
22716
+ refreshed++;
22717
+ };
22718
+ const rebase = (converged, order) => {
22719
+ if (!converged || !order) return;
22720
+ order.touched = true;
22721
+ refreshed++;
22722
+ };
22723
+ rebase(plan.order.state === "converged", this.sim.courses);
22724
+ for (const course of plan.courses) {
22725
+ const { folder } = course;
22726
+ refresh(course.server?.state === "converged", folder, () => {
22727
+ folder.rev = course.server.serverRev;
22728
+ });
22729
+ if (folder.id !== void 0) rebase(course.modulesOrder?.state === "converged", this.sim.modules.get(folder.id));
22730
+ for (const module of course.modules) {
22731
+ refresh(module.server?.state === "converged", folder, () => {
22732
+ module.entry.rev = module.server.serverRev;
22733
+ });
22734
+ if (module.entry.id !== void 0) rebase(module.lessonsOrder?.state === "converged", this.sim.lessons.get(module.entry.id));
22735
+ }
22736
+ for (const lesson of course.lessons) {
22737
+ const placement = lesson.placement;
22738
+ if (placement?.state !== "converged") continue;
22739
+ const was = placement.base === void 0 ? void 0 : this.sim.lessons.get(placement.base);
22740
+ if (was) {
22741
+ const at = was.pinned?.indexOf(lesson.lesson.id) ?? -1;
22742
+ if (at >= 0) was.pinned.splice(at, 1);
22743
+ was.touched = true;
22744
+ }
22745
+ const now = this.sim.lessons.get(placement.server);
22746
+ if (now) {
22747
+ if (now.pinned && !now.pinned.includes(lesson.lesson.id)) insertAfter(now.pinned, lesson.lesson.id, lesson.module.lessons.map((l) => l.id ?? ""));
22748
+ now.touched = true;
22749
+ }
22750
+ refreshed++;
22751
+ }
22752
+ for (const lesson of course.lessons) {
22753
+ if (lesson.server?.state === "converged") {
22754
+ lesson.lesson.rev = lesson.server.serverRev;
22755
+ this.save.lesson(folder, lesson.lesson);
22756
+ refreshed++;
22757
+ }
22758
+ }
22759
+ }
22760
+ this.settle();
22761
+ return refreshed;
22762
+ }
22763
+ /** Save the files whose synced lists changed. */
22764
+ flush() {
22765
+ for (const owner of this.unsaved) {
22766
+ if (owner === null) this.save.community();
22767
+ else this.save.course(owner);
22768
+ }
22769
+ this.unsaved.clear();
22770
+ }
22771
+ // ── The writes, phase by phase ──
22772
+ /** Courses, then lessons, that the files take off the community: first, before anything else changes. */
22773
+ unpublishWrites() {
22774
+ const writes = [];
22775
+ for (const course of this.plan.courses) {
22776
+ const { server, folder } = course;
22777
+ if (server?.state === "local" && server.server.published && !folder.published) {
22778
+ writes.push({ label: `course ${folder.slug} (unpublish)`, requests: 1, apply: () => this.updateCourse(folder, false) });
22779
+ }
22780
+ }
22781
+ for (const lesson of this.lessons()) {
22782
+ const { server, lesson: file, course } = lesson;
22783
+ if (server?.state === "local" && server.server.published && !file.published) {
22784
+ writes.push({ label: `lesson ${course.slug}/${file.slug} (unpublish)`, requests: 1, apply: () => lessonWrite(course, file, () => this.updateLesson(course, file, true)) });
22785
+ }
22786
+ }
22787
+ return writes;
22788
+ }
22789
+ /** New courses, as drafts; and the courses whose content changed, but for their publishing. */
22790
+ courseWrites() {
22791
+ return this.plan.courses.flatMap((course) => {
22792
+ const { server, folder } = course;
22793
+ if (!server) return [{ label: `course ${folder.slug} (create)`, requests: 1, apply: () => this.createCourse(folder) }];
22794
+ if (server.state !== "local" || server.server.published !== folder.published) return [];
22795
+ return [{ label: `course ${folder.slug}`, requests: 1, apply: () => this.updateCourse(folder, false) }];
22796
+ });
22797
+ }
22798
+ moduleWrites() {
22799
+ return this.plan.courses.flatMap((course) => course.modules.flatMap((module) => {
22800
+ const at = `${course.folder.slug}/${module.entry.key}`;
22801
+ if (!module.server) return [{ label: `module ${at} (create)`, requests: 1, apply: () => this.createModule(course.folder, module.entry) }];
22802
+ if (module.server.state !== "local") return [];
22803
+ return [{ label: `module ${at}`, requests: 1, apply: () => this.updateModule(course.folder, module.entry) }];
22804
+ }));
22805
+ }
22806
+ lessonWrites() {
22807
+ return this.lessons().flatMap((lesson) => {
22808
+ const { server, course, module, lesson: file } = lesson;
22809
+ const at = `${course.slug}/${file.slug}`;
22810
+ if (!server) return [{ label: `lesson ${at} (create)`, requests: 1, apply: () => lessonWrite(course, file, () => this.createLesson(course, module, file)) }];
22811
+ if (server.state !== "local") return [];
22812
+ const unpublished = server.server.published && !file.published;
22813
+ if (unpublished && !fileLesson(this.files.rooms, file).room_id?.startsWith(PENDING_ROOM)) return [];
22814
+ return [{ label: `lesson ${at}`, requests: 1, apply: () => lessonWrite(course, file, () => this.updateLesson(course, file, false)) }];
22815
+ });
22816
+ }
22817
+ /** How many position writes the plan predicts, every create landing as a new entity. */
22818
+ get plannedPlaces() {
22819
+ return this.predicted.moves.length;
22820
+ }
22821
+ /** The position writes, from the server's orders as the creates left them: when the phase is reached. */
22822
+ placeWrites() {
22823
+ return computePlaces(this.files, this.sim.clone(), realIds, this.placements).moves.map((move) => ({
22824
+ label: move.kind === "lesson" && this.sim.at.get(move.id) !== move.moduleId ? `lesson ${this.pathOf(move.id)} \u2192 module ${this.pathOf(move.moduleId)}` : `place of ${move.kind} ${this.pathOf(move.id)}`,
22825
+ requests: 1,
22826
+ apply: () => this.place(move)
22827
+ }));
22828
+ }
22829
+ /** Courses the files publish: last, once their lessons are in place. */
22830
+ publishWrites() {
22831
+ return this.plan.courses.flatMap((course) => {
22832
+ const { folder, server } = course;
22833
+ const now = folder.id === void 0 ? void 0 : this.courseNow.get(folder.id);
22834
+ if (!folder.published || now?.published || server !== null && server.state !== "local") return [];
22835
+ return [{ label: `course ${folder.slug} (publish)`, requests: 1, apply: () => this.updateCourse(folder, true) }];
22836
+ });
22837
+ }
22838
+ pathOf(id) {
22839
+ for (const course of this.files.courses) {
22840
+ if (course.id === id) return course.slug;
22841
+ for (const module of course.modules) {
22842
+ if (module.id === id) return `${course.slug}/${module.key}`;
22843
+ for (const lesson of module.lessons) if (lesson.id === id) return `${course.slug}/${lesson.slug}`;
22844
+ }
22845
+ }
22846
+ return this.name(id);
22847
+ }
22848
+ // ── Each write ──
22849
+ async createCourse(folder) {
22850
+ const made = await this.remote.createCourse({
22851
+ title: folder.title,
22852
+ description: folder.description,
22853
+ client_id: await createClientId(this.communityId, "course", folder.slug)
22854
+ });
22855
+ const twin = this.files.courses.find((other) => other !== folder && other.id === made.course_id);
22856
+ if (twin) {
22857
+ throw expected(
22858
+ `the name ${folder.slug} already names course ${made.course_id}, which your files also list as ${twin.slug}: a course folder's name keeps naming its course. Remove one of the two from ${COMMUNITY_FILE}'s courses.`
22859
+ );
22860
+ }
22861
+ const existed = this.sim.modules.get(made.course_id);
22862
+ folder.id = made.course_id;
22863
+ folder.rev = serverCourse(made).rev;
22864
+ this.courseNow.set(made.course_id, made);
22865
+ if (!existed) {
22866
+ append(this.sim.courses, made.course_id);
22867
+ this.sim.modules.set(made.course_id, freshOrder());
22868
+ }
22869
+ this.unsaved.add(folder);
22870
+ this.settle();
22871
+ await this.updateCourse(folder, false);
22872
+ }
22873
+ async updateCourse(folder, publish) {
22874
+ const now = this.courseNow.get(folder.id);
22875
+ const body = coursePatch(folder, now, publish);
22876
+ if (!body) return;
22877
+ const answer = await this.remote.updateCourse(folder.id, body);
22878
+ takeCourse(folder, answer, new Set(Object.keys(body)));
22879
+ folder.rev = serverCourse(answer).rev;
22880
+ this.courseNow.set(answer.course_id, answer);
22881
+ this.save.course(folder);
22882
+ }
22883
+ async createModule(course, entry) {
22884
+ const courseId = course.id;
22885
+ const made = await this.remote.createModule(courseId, {
22886
+ title: entry.title,
22887
+ client_id: await createClientId(this.communityId, "module", `${course.slug}/${entry.key}`)
22888
+ });
22889
+ if (made.course_id !== courseId) {
22890
+ throw expected(
22891
+ `the key ${entry.key} already names module ${show(made.title)} (${made.module_id}) of ${courseName(this.files, this.plan.server, made.course_id)}: a key keeps naming its module, and a module does not move between courses. Give the new module another key.`
22892
+ );
22893
+ }
22894
+ const twin = this.files.courses.flatMap((c) => c.modules.map((m) => ({ c, m }))).find(({ m }) => m !== entry && m.id === made.module_id);
22895
+ if (twin) {
22896
+ throw expected(`the key ${entry.key} already names module ${made.module_id}, which your files also list as ${twin.c.slug}/${twin.m.key}: remove one of the two entries.`);
22897
+ }
22898
+ const existed = this.sim.lessons.get(made.module_id);
22899
+ entry.id = made.module_id;
22900
+ entry.rev = serverModule(made).rev;
22901
+ this.moduleNow.set(made.module_id, made);
22902
+ if (!existed) {
22903
+ append(this.sim.modules.get(courseId), made.module_id);
22904
+ this.sim.lessons.set(made.module_id, freshOrder());
22905
+ }
22906
+ this.unsaved.add(course);
22907
+ this.settle();
22908
+ await this.updateModule(course, entry);
22909
+ }
22910
+ async updateModule(course, entry) {
22911
+ const now = this.moduleNow.get(entry.id);
22912
+ if (moduleContent(entry).title === moduleContent(now).title) return;
22913
+ const answer = await this.remote.updateModule(course.id, entry.id, { title: entry.title });
22914
+ if (moduleContent(answer).title !== moduleContent(entry).title) entry.title = answer.title;
22915
+ entry.rev = serverModule(answer).rev;
22916
+ this.moduleNow.set(answer.module_id, answer);
22917
+ this.save.course(course);
22918
+ }
22919
+ async createLesson(course, module, lesson) {
22920
+ const courseId = course.id;
22921
+ const roomId = fileLesson(this.files.rooms, lesson).room_id;
22922
+ if (roomId?.startsWith(PENDING_ROOM)) throw new Error(`createLesson: room ${lesson.room} has no id yet`);
22923
+ const made = await this.remote.createLesson(courseId, {
22924
+ module_id: module.id,
22925
+ title: lesson.title,
22926
+ ...lesson.video_url === void 0 ? {} : { video_url: lesson.video_url },
22927
+ body: lesson.body,
22928
+ links: lesson.links,
22929
+ ...roomId === null ? {} : { room_id: roomId },
22930
+ requires_paid_plan: lesson.requires_paid_plan,
22931
+ published: lesson.published,
22932
+ client_id: await createClientId(this.communityId, "lesson", `${course.slug}/${lesson.slug}`)
22933
+ });
22934
+ if (made.course_id !== courseId) {
22935
+ throw expected(
22936
+ `the file name ${course.slug}/${lesson.slug} already names lesson ${show(made.title)} (${made.lesson_id}) of ${courseName(this.files, this.plan.server, made.course_id)}: a lesson's file name keeps naming its lesson, and a lesson does not move between courses. Give the new lesson another file name.`
22937
+ );
22938
+ }
22939
+ const twin = this.lessons().find((other) => other.lesson !== lesson && other.lesson.id === made.lesson_id);
22940
+ if (twin) {
22941
+ throw expected(`the file name ${lesson.slug} already names lesson ${made.lesson_id}, which your files also list as ${twin.course.slug}/${twin.lesson.slug}: remove one of the two.`);
22942
+ }
22943
+ const existed = this.sim.at.has(made.lesson_id);
22944
+ lesson.id = made.lesson_id;
22945
+ lesson.rev = serverLesson(made).rev;
22946
+ this.lessonNow.set(made.lesson_id, made);
22947
+ if (!existed) {
22948
+ append(this.sim.lessons.get(made.module_id), made.lesson_id);
22949
+ this.sim.at.set(made.lesson_id, made.module_id);
22950
+ }
22951
+ this.save.lesson(course, lesson);
22952
+ this.settle();
22953
+ await this.updateLesson(course, lesson, false);
22954
+ }
22955
+ async updateLesson(course, lesson, waitForRooms) {
22956
+ const now = this.lessonNow.get(lesson.id);
22957
+ const body = lessonPatch(this.files.rooms, lesson, now, waitForRooms);
22958
+ if (!body) return;
22959
+ const answer = await this.remote.updateLesson(course.id, lesson.id, body);
22960
+ takeLesson(this.files.rooms, lesson, answer, new Set(Object.keys(body)));
22961
+ lesson.rev = serverLesson(answer).rev;
22962
+ this.lessonNow.set(answer.lesson_id, answer);
22963
+ this.save.lesson(course, lesson);
22964
+ }
22965
+ /**
22966
+ * A position write. One whose answer is lost may have moved the child or not: the server is read
22967
+ * again, and when the child stands where the write put it, the write is recorded as if answered —
22968
+ * the orders it changed, and no other — before the failure stands. A refusal is not read back.
22969
+ */
22970
+ async place(move) {
22971
+ try {
22972
+ await this.sendPlace(move);
22973
+ } catch (err) {
22974
+ if (outcomeUnknown(err)) await this.recordIfLanded(move).catch(() => void 0);
22975
+ throw err;
22976
+ }
22977
+ }
22978
+ async sendPlace(move) {
22979
+ if (move.kind === "course") {
22980
+ const answer = await this.remote.updateCourse(move.id, { position: move.position });
22981
+ this.placedCourse(answer, answer.position);
22982
+ } else if (move.kind === "module") {
22983
+ const answer = await this.remote.updateModule(move.courseId, move.id, { position: move.position });
22984
+ this.placedModule(move.courseId, answer, answer.position);
22985
+ } else {
22986
+ const from = this.sim.at.get(move.id);
22987
+ const body = from === move.moduleId ? { position: move.position } : { module_id: move.moduleId, position: move.position };
22988
+ const answer = await this.remote.updateLesson(move.courseId, move.id, body);
22989
+ this.placedLesson(answer, answer.module_id, answer.position);
22990
+ }
22991
+ }
22992
+ /** A position write whose answer was lost, recorded when a read finds its child where it put it. */
22993
+ async recordIfLanded(move) {
22994
+ const wanted = (order) => {
22995
+ const ids = [...order.server];
22996
+ placeAt(ids, move.id, move.position);
22997
+ return ids.indexOf(move.id);
22998
+ };
22999
+ if (move.kind === "course") {
23000
+ const courses = await this.remote.listCourses();
23001
+ const at2 = courses.findIndex((course2) => course2.course_id === move.id);
23002
+ if (at2 >= 0 && at2 === wanted(this.sim.courses)) this.placedCourse(courses[at2], at2);
23003
+ return;
23004
+ }
23005
+ const course = await this.remote.exportCourse(move.courseId);
23006
+ if (move.kind === "module") {
23007
+ const at2 = course.modules.findIndex((module) => module.module_id === move.id);
23008
+ if (at2 >= 0 && at2 === wanted(this.sim.modules.get(move.courseId))) this.placedModule(move.courseId, course.modules[at2], at2);
23009
+ return;
23010
+ }
23011
+ const lessons = course.modules.find((module) => module.module_id === move.moduleId)?.lessons ?? [];
23012
+ const at = lessons.findIndex((lesson) => lesson.lesson_id === move.id);
23013
+ if (at >= 0 && at === wanted(this.sim.lessons.get(move.moduleId))) this.placedLesson(lessons[at], move.moduleId, at);
23014
+ }
23015
+ placedCourse(now, position) {
23016
+ shift(this.sim.courses, now.course_id, position);
23017
+ this.courseNow.set(now.course_id, now);
23018
+ this.settle();
23019
+ }
23020
+ placedModule(courseId, now, position) {
23021
+ shift(this.sim.modules.get(courseId), now.module_id, position);
23022
+ this.moduleNow.set(now.module_id, now);
23023
+ this.settle();
23024
+ }
23025
+ placedLesson(now, moduleId, position) {
23026
+ const from = this.sim.at.get(now.lesson_id);
23027
+ if (moduleId !== from) leave(this.sim.lessons.get(from), now.lesson_id);
23028
+ shift(this.sim.lessons.get(moduleId), now.lesson_id, position);
23029
+ this.sim.at.set(now.lesson_id, moduleId);
23030
+ this.lessonNow.set(now.lesson_id, now);
23031
+ this.settle();
23032
+ }
23033
+ /**
23034
+ * After a write that changed a parent's order (or a refresh), each such parent's synced list is
23035
+ * recorded and the files keeping a changed one are saved. A synced list is the server's order as
23036
+ * its own placement leaves it — less a lesson the server moved in since, and with one it moved
23037
+ * out, which the files have not taken: their base stays the module the files last synced them in
23038
+ * — or, for a parent whose order changed on the server only, the files' synced list as push left
23039
+ * it (`OrderSim.pinned`).
23040
+ */
23041
+ settle() {
23042
+ const record = (order, current, base, set, owner) => {
23043
+ if (!order?.touched || order.conflict) return;
23044
+ order.touched = false;
23045
+ const ids = order.pinned ?? base(order, current ?? []);
23046
+ if (current && sameOrder(current, ids)) return;
23047
+ set([...ids]);
23048
+ this.unsaved.add(owner);
23049
+ };
23050
+ const asIs = (order) => order.server;
23051
+ record(this.sim.courses, this.files.synced_courses, asIs, (ids) => {
23052
+ this.files.synced_courses = ids;
23053
+ }, null);
23054
+ for (const course of this.files.courses) {
23055
+ const modules = course.id === void 0 ? void 0 : this.sim.modules.get(course.id);
23056
+ if (!modules) continue;
23057
+ const synced2 = () => course.synced ??= { lessons: {} };
23058
+ record(modules, course.synced?.modules, asIs, (ids) => {
23059
+ synced2().modules = ids;
23060
+ }, course);
23061
+ for (const moduleId of modules.server) {
23062
+ record(this.sim.lessons.get(moduleId), course.synced?.lessons[moduleId], (order, current) => this.lessonsBase(moduleId, order.server, current), (ids) => {
23063
+ synced2().lessons[moduleId] = ids;
23064
+ }, course);
23065
+ }
23066
+ }
23067
+ this.flush();
23068
+ }
23069
+ /**
23070
+ * A module's synced lessons: the server's, less those it moved in and with those it moved out, the
23071
+ * files not having taken either — each one moved out back in its place in the synced list as it
23072
+ * stood (`current`).
23073
+ */
23074
+ lessonsBase(moduleId, server, current) {
23075
+ const movedIn = this.movedIn.get(moduleId);
23076
+ const ids = server.filter((id) => !movedIn?.has(id));
23077
+ for (const id of this.movedOut.get(moduleId) ?? []) if (!ids.includes(id)) insertAfter(ids, id, current);
23078
+ return ids;
23079
+ }
23080
+ };
23081
+ synced = (state, takes) => state !== "conflict" || takes(state);
23082
+ }
23083
+ });
23084
+
21108
23085
  // src/communities/sync.ts
21109
23086
  var sync_exports2 = {};
21110
23087
  __export(sync_exports2, {
21111
23088
  pullCommunity: () => pullCommunity,
21112
23089
  pushCommunity: () => pushCommunity
21113
23090
  });
21114
- import * as fs18 from "fs";
21115
- import * as path25 from "path";
23091
+ import * as fs19 from "fs";
23092
+ import * as path27 from "path";
21116
23093
  function parseArgs5(args2) {
21117
23094
  return {
21118
23095
  autoConfirm: args2.includes("--yes") || args2.includes("-y"),
@@ -21173,11 +23150,22 @@ function planSync(files, server) {
21173
23150
  },
21174
23151
  rooms,
21175
23152
  untracked: server.rooms.filter((room) => !tracked.has(room.room_id)),
21176
- unknownIds
23153
+ unknownIds,
23154
+ courses: planCourses(files, server.courses)
21177
23155
  };
21178
23156
  }
21179
23157
  function unknownIdProblems(plan) {
21180
- return plan.unknownIds.map((entry) => `room ${entry.key}: ${entry.id} is not a room of this community`);
23158
+ return [...plan.unknownIds.map((entry) => `room ${entry.key}: ${entry.id} is not a room of this community`), ...plan.courses.problems];
23159
+ }
23160
+ function warnUnlisted(files) {
23161
+ for (const folder of files.unlistedCourses) {
23162
+ console.warn(`Skipped: ${unlistedPath(folder.name)} \u2014 ${COMMUNITY_FILE}'s courses does not list it.`);
23163
+ }
23164
+ for (const course of files.courses) {
23165
+ for (const file of course.unlisted) {
23166
+ console.warn(`Skipped: ${unlistedPath(file.name, course)} \u2014 no module of the course lists it.`);
23167
+ }
23168
+ }
21181
23169
  }
21182
23170
  function settingsChanges(from, to) {
21183
23171
  const out = [];
@@ -21186,31 +23174,6 @@ function settingsChanges(from, to) {
21186
23174
  if (from.rules !== to.rules) out.push("rules (rules.md)");
21187
23175
  return out;
21188
23176
  }
21189
- function lineDiff(from, to) {
21190
- const a = from ? sanitizeTerminalText(from).split("\n") : [];
21191
- const b = to ? sanitizeTerminalText(to).split("\n") : [];
21192
- if (a.length * b.length > 25e4) return [...a.map((l) => `- ${l}`), ...b.map((l) => `+ ${l}`)];
21193
- const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
21194
- for (let i2 = a.length - 1; i2 >= 0; i2--) {
21195
- for (let j2 = b.length - 1; j2 >= 0; j2--) {
21196
- lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
21197
- }
21198
- }
21199
- const out = [];
21200
- let i = 0;
21201
- let j = 0;
21202
- while (i < a.length || j < b.length) {
21203
- if (i < a.length && j < b.length && a[i] === b[j]) {
21204
- i++;
21205
- j++;
21206
- } else if (j >= b.length || i < a.length && lcs[i + 1][j] >= lcs[i][j + 1]) {
21207
- out.push(`- ${a[i++]}`);
21208
- } else {
21209
- out.push(`+ ${b[j++]}`);
21210
- }
21211
- }
21212
- return out;
21213
- }
21214
23177
  function roomChanges(from, to) {
21215
23178
  const out = [];
21216
23179
  if (from.name !== to.name) out.push(`name ${show(from.name)} \u2192 ${show(to.name)}`);
@@ -21220,19 +23183,6 @@ function roomChanges(from, to) {
21220
23183
  function joinLinkChange(from, to) {
21221
23184
  return to ? `${onOff(from)} \u2192 on: a new link \u2014 every link shared before stays dead` : `${onOff(from)} \u2192 off: every link shared before stops working`;
21222
23185
  }
21223
- function printSection(title, lines) {
21224
- if (lines.length === 0) return;
21225
- console.log(`
21226
- ${title}`);
21227
- for (const line of lines) console.log(` ${line}`);
21228
- }
21229
- function printListing(listing) {
21230
- printSection("Changes:", listing.changes);
21231
- printSection("Changed on the server since your last pull \u2014 left as is; pull to take it:", listing.serverChanged);
21232
- printSection("Kept \u2014 edited in your files, not pushed yet:", listing.kept);
21233
- printSection("Not in your files \u2014 left as is:", listing.leftAsIs);
21234
- printSection("Conflicts \u2014 changed in your files and on the server since your last pull:", listing.conflicts);
21235
- }
21236
23186
  function pushProblems(files, server, plan) {
21237
23187
  const problems2 = unknownIdProblems(plan);
21238
23188
  if (files.name !== server.community.name) {
@@ -21244,12 +23194,13 @@ function pushProblems(files, server, plan) {
21244
23194
  }
21245
23195
  }
21246
23196
  const creates = plan.rooms.filter((room) => room.kind === "create").length;
21247
- const certainlyNew = Math.max(0, creates - plan.untracked.length);
21248
- if (server.rooms.length + certainlyNew > COMMUNITY_ROOMS_MAX) {
23197
+ const certainlyNew2 = Math.max(0, creates - plan.untracked.length);
23198
+ if (server.rooms.length + certainlyNew2 > COMMUNITY_ROOMS_MAX) {
21249
23199
  problems2.push(
21250
23200
  `rooms: a community holds at most ${COMMUNITY_ROOMS_MAX} rooms, archived ones included; it has ${server.rooms.length}, and the files create ${creates}`
21251
23201
  );
21252
23202
  }
23203
+ problems2.push(...courseCapProblems(files, plan.courses));
21253
23204
  return problems2;
21254
23205
  }
21255
23206
  function settingsPatch(files, server) {
@@ -21289,7 +23240,18 @@ function takeJoinLink(files, enabled) {
21289
23240
  files.join_link_enabled = enabled;
21290
23241
  files.join_link_rev = joinLinkRev(files.community_id, { enabled });
21291
23242
  }
21292
- function pushWrites(files, plan, remote, save) {
23243
+ function pushPhases(files, plan, remote, save, courses) {
23244
+ return [
23245
+ courses.unpublishWrites(),
23246
+ communityWrites(files, plan, remote, save),
23247
+ courses.courseWrites(),
23248
+ courses.moduleWrites(),
23249
+ courses.lessonWrites(),
23250
+ { planned: courses.plannedPlaces, later: () => courses.placeWrites() },
23251
+ { planned: courses.publishWrites().length, later: () => courses.publishWrites() }
23252
+ ];
23253
+ }
23254
+ function communityWrites(files, plan, remote, save) {
21293
23255
  const writes = [];
21294
23256
  if (plan.settings.state === "local") {
21295
23257
  const body = settingsPatch(files, plan.settings.server);
@@ -21379,8 +23341,8 @@ function refreshConverged(files, plan) {
21379
23341
  }
21380
23342
  return refreshed;
21381
23343
  }
21382
- function pushListing(plan) {
21383
- const listing = { changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] };
23344
+ function pushListing(plan, courses) {
23345
+ const listing = emptyListing();
21384
23346
  const { settings, joinLink } = plan;
21385
23347
  if (settings.state === "local") {
21386
23348
  listing.changes.push(`~ settings: ${settingsChanges(settings.server, settings.file).join("; ")}`);
@@ -21405,6 +23367,7 @@ function pushListing(plan) {
21405
23367
  listing.serverChanged = named("remote");
21406
23368
  listing.conflicts = named("conflict");
21407
23369
  listing.leftAsIs = plan.untracked.map((room) => `room ${show(room.name)} (${room.room_id})`);
23370
+ courses.list(listing);
21408
23371
  return listing;
21409
23372
  }
21410
23373
  async function pushCommunity(gitRootOrNull, selector, args2) {
@@ -21423,37 +23386,37 @@ async function pushCommunity(gitRootOrNull, selector, args2) {
21423
23386
  throw expected("Push changes the live community and asks first: pass -y when not running in a terminal, or --dry-run to only list the changes.");
21424
23387
  }
21425
23388
  const { organization_id: orgId } = requireRepoConfig(gitRoot);
21426
- const label = path25.basename(folder);
23389
+ const label = path27.basename(folder);
21427
23390
  assertInScope("communities", files.community_id, label);
23391
+ warnUnlisted(files);
21428
23392
  const remote = await openRemote(files.community_id);
21429
23393
  const server = await remote.read(orgId);
21430
23394
  const plan = planSync(files, server);
21431
- console.log(`Community ${show(server.community.name)} (${server.community.community_id}) \u2014 ${path25.relative(gitRoot, folder)}/`);
23395
+ console.log(`Community ${show(server.community.name)} (${server.community.community_id}) \u2014 ${path27.relative(gitRoot, folder)}/`);
21432
23396
  const problems2 = pushProblems(files, server, plan);
21433
23397
  if (problems2.length > 0) {
21434
23398
  throw expected(`Nothing was pushed:
21435
23399
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
21436
23400
  }
21437
- const listing = pushListing(plan);
23401
+ const { communitiesDir } = resolveLayout(gitRoot);
23402
+ const save = communityFilesSaver(communitiesDir, folder, files);
23403
+ const courses = new CoursePush(files, plan.courses, remote, courseSaver(communitiesDir, folder, save));
23404
+ const listing = pushListing(plan, courses);
21438
23405
  printListing(listing);
21439
23406
  if (listing.conflicts.length > 0) {
21440
23407
  throw expected(`
21441
23408
  Nothing was pushed: ${listing.conflicts.length} conflict(s). ${resolveConflicts(folder)}`);
21442
23409
  }
21443
- const { communitiesDir } = resolveLayout(gitRoot);
21444
- const save = () => {
21445
- writeCommunityFolder(communitiesDir, folder, files);
21446
- };
21447
- const writes = pushWrites(files, plan, remote, save);
21448
- if (writes.length === 0) {
21449
- const refreshed = dryRun ? 0 : refreshConverged(files, plan);
23410
+ const phases = pushPhases(files, plan, remote, save, courses);
23411
+ const requests = phases.reduce((sum, phase) => sum + plannedRequests(phase), 0);
23412
+ if (requests === 0) {
23413
+ const refreshed = dryRun ? 0 : refreshConverged(files, plan) + courses.refreshConverged();
21450
23414
  if (refreshed > 0) save();
21451
23415
  console.log(`
21452
- No changes to push${refreshed > 0 ? ` (${refreshed} rev(s) refreshed in ${COMMUNITY_FILE})` : ""}.`);
23416
+ No changes to push${refreshed > 0 ? ` (${refreshed} rev(s) refreshed)` : ""}.`);
21453
23417
  if (!dryRun) seedScopeIfEmpty("communities", files.community_id);
21454
23418
  return;
21455
23419
  }
21456
- const requests = writes.reduce((sum, write) => sum + write.requests, 0);
21457
23420
  console.log(
21458
23421
  `
21459
23422
  This push makes ${requests} request(s) to the live community \u2014 ${paceDuration(requests)} at ${COMMUNITY_REQUESTS_PER_MINUTE} a minute.`
@@ -21475,24 +23438,67 @@ This push makes ${requests} request(s) to the live community \u2014 ${paceDurati
21475
23438
  }
21476
23439
  }
21477
23440
  refreshConverged(files, plan);
23441
+ courses.refreshConverged();
21478
23442
  save();
21479
- for (const [index, write] of writes.entries()) {
21480
- try {
21481
- await write.apply();
21482
- } catch (err) {
21483
- throw expected(
21484
- `Push stopped at ${write.label}: ${stopReason(err)}
21485
- What it applied before stopping is on the community, and ${COMMUNITY_FILE} records it. Fix the cause and push again.`
21486
- );
23443
+ let total = phases.reduce((sum, phase) => sum + plannedCount(phase), 0);
23444
+ let done = 0;
23445
+ const stop = stopAfterWrite();
23446
+ try {
23447
+ for (const phase of phases) {
23448
+ const writes = Array.isArray(phase) ? phase : phase.later();
23449
+ total += writes.length - plannedCount(phase);
23450
+ for (const write of writes) {
23451
+ if (stop.requested()) {
23452
+ throw expected(
23453
+ `Push stopped on request after ${done} change(s). What it applied is on the community, and the files record it: push again to go on.`
23454
+ );
23455
+ }
23456
+ try {
23457
+ await write.apply();
23458
+ } catch (err) {
23459
+ throw expected(
23460
+ `Push stopped at ${write.label}: ${stopReason(err)}
23461
+ What it applied before stopping is on the community, and the files record it. Fix the cause and push again.`
23462
+ );
23463
+ }
23464
+ console.log(` [${++done}/${total}] ${write.label}`);
23465
+ }
21487
23466
  }
21488
- console.log(` [${index + 1}/${writes.length}] ${write.label}`);
23467
+ } finally {
23468
+ stop.release();
21489
23469
  }
21490
23470
  console.log(
21491
23471
  `
21492
- Pushed ${writes.length} change(s). Commit ${path25.relative(gitRoot, folder)}/: ${COMMUNITY_FILE} now records the ids and revs this push wrote.`
23472
+ Pushed ${done} change(s). Commit ${path27.relative(gitRoot, folder)}/: the files now record the ids and revs this push wrote.`
21493
23473
  );
21494
23474
  seedScopeIfEmpty("communities", files.community_id);
21495
23475
  }
23476
+ function stopAfterWrite() {
23477
+ let asked = false;
23478
+ const onSignal = (signal) => {
23479
+ if (asked) process.exit(SIGNAL_EXIT_CODE[signal]);
23480
+ asked = true;
23481
+ console.error("\n Stopping after the write in progress, once the files record it. Press Ctrl-C again to leave at once.");
23482
+ };
23483
+ for (const signal of HANDLED_SIGNALS) process.on(signal, onSignal);
23484
+ return {
23485
+ requested: () => asked,
23486
+ release: () => {
23487
+ for (const signal of HANDLED_SIGNALS) process.off(signal, onSignal);
23488
+ }
23489
+ };
23490
+ }
23491
+ function courseSaver(root, folder, community) {
23492
+ return {
23493
+ community,
23494
+ course: (course) => {
23495
+ writeCourseYaml(root, folder, course);
23496
+ },
23497
+ lesson: (course, lesson) => {
23498
+ writeLessonFile(root, folder, course, lesson);
23499
+ }
23500
+ };
23501
+ }
21496
23502
  function changedSince(before, after) {
21497
23503
  const id = before.community.community_id;
21498
23504
  const changed = [];
@@ -21504,6 +23510,7 @@ function changedSince(before, after) {
21504
23510
  for (const room of after.rooms) {
21505
23511
  if (revs.get(room.room_id) !== serverRoom(room).rev) changed.push(`room ${show(room.name)}`);
21506
23512
  }
23513
+ changed.push(...coursesChangedSince(before.courses, after.courses));
21507
23514
  return changed;
21508
23515
  }
21509
23516
  function stopReason(err) {
@@ -21527,15 +23534,18 @@ function filesFromServer(server) {
21527
23534
  settings: { slow_mode_seconds: 0, courses_label: "" },
21528
23535
  rules: "",
21529
23536
  join_link_enabled: false,
21530
- rooms: []
23537
+ rooms: [],
23538
+ courses: [],
23539
+ unlistedCourses: []
21531
23540
  };
21532
23541
  takeSettings(files, server.settings);
21533
23542
  takeJoinLink(files, server.joinLink.enabled);
21534
23543
  addUntracked(files, server.rooms);
23544
+ coursesFromServer(files, server.courses);
21535
23545
  return files;
21536
23546
  }
21537
23547
  function pullMerge(files, server, plan, theirs) {
21538
- const listing = { changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] };
23548
+ const listing = emptyListing();
21539
23549
  const takes = (state) => state === "remote" || state === "conflict" && theirs;
21540
23550
  const note = (state, what) => {
21541
23551
  if (state === "local") listing.kept.push(what);
@@ -21573,6 +23583,7 @@ function pullMerge(files, server, plan, theirs) {
21573
23583
  }
21574
23584
  refreshConverged(files, plan);
21575
23585
  listing.changes.push(...addUntracked(files, plan.untracked));
23586
+ pullCourses(files, plan.courses, takes, note, listing);
21576
23587
  return listing;
21577
23588
  }
21578
23589
  async function pullCommunity(gitRootOrNull, selector, args2) {
@@ -21588,26 +23599,28 @@ async function pullCommunity(gitRootOrNull, selector, args2) {
21588
23599
  communityId = existing.files.community_id;
21589
23600
  }
21590
23601
  const { organization_id: orgId } = requireRepoConfig(gitRoot);
21591
- assertInScope("communities", communityId, existing ? path25.basename(existing.folder) : void 0);
23602
+ assertInScope("communities", communityId, existing ? path27.basename(existing.folder) : void 0);
21592
23603
  const { communitiesDir } = resolveLayout(gitRoot);
21593
23604
  const remote = await openRemote(communityId);
21594
23605
  const server = await remote.read(orgId);
21595
23606
  if (!existing) {
21596
- const taken = new Set(safeReaddir2(communitiesDir));
21597
- const folder2 = path25.join(communitiesDir, localName(server.community.name, communityId, taken, "community"));
23607
+ const taken = new Set(safeReaddir2(communitiesDir).map((name) => name.toLowerCase()));
23608
+ const folder2 = path27.join(communitiesDir, localName(server.community.name, communityId, taken, "community"));
21598
23609
  const files = filesFromServer(server);
21599
- const where = `${path25.relative(gitRoot, folder2)}/`;
23610
+ const where = `${path27.relative(gitRoot, folder2)}/`;
23611
+ const counts = `${files.rooms.length} room(s), ${files.courses.length} course(s)`;
21600
23612
  if (dryRun) {
21601
- console.log(`Dry run \u2014 would write community ${show(server.community.name)} to ${where} with ${files.rooms.length} room(s).`);
23613
+ console.log(`Dry run \u2014 would write community ${show(server.community.name)} to ${where} with ${counts}.`);
21602
23614
  return;
21603
23615
  }
21604
23616
  requireRealSubdirNoSymlink(gitRoot, communitiesDir, true);
21605
23617
  writeCommunityFolder(communitiesDir, folder2, files);
21606
- console.log(`Community ${show(server.community.name)} written to ${where} (${files.rooms.length} room(s)). Commit it: its ids and revs are the base the next push compares against.`);
23618
+ console.log(`Community ${show(server.community.name)} written to ${where} (${counts}). Commit it: its ids and revs are the base the next push compares against.`);
21607
23619
  seedScopeIfEmpty("communities", communityId);
21608
23620
  return;
21609
23621
  }
21610
23622
  const { folder, files: local } = existing;
23623
+ warnUnlisted(local);
21611
23624
  const plan = planSync(local, server);
21612
23625
  const problems2 = unknownIdProblems(plan);
21613
23626
  if (problems2.length > 0) {
@@ -21615,7 +23628,7 @@ async function pullCommunity(gitRootOrNull, selector, args2) {
21615
23628
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
21616
23629
  }
21617
23630
  const listing = pullMerge(local, server, plan, theirs);
21618
- console.log(`Community ${show(server.community.name)} (${communityId}) \u2014 ${path25.relative(gitRoot, folder)}/`);
23631
+ console.log(`Community ${show(server.community.name)} (${communityId}) \u2014 ${path27.relative(gitRoot, folder)}/`);
21619
23632
  printListing(listing);
21620
23633
  if (dryRun) {
21621
23634
  console.log("\nDry run \u2014 nothing was written.");
@@ -21630,16 +23643,16 @@ Wrote ${written.join(", ")}.` : "\nAlready up to date.");
21630
23643
  }
21631
23644
  }
21632
23645
  function resolveConflicts(folder) {
21633
- return `To resolve, take the server's side \u2014 wayai pull ${WAYAI_LAYOUT.communitiesSubdir}/${path25.basename(folder)} --theirs \u2014 then make again any edit of yours to keep, and push.`;
23646
+ return `To resolve, take the server's side \u2014 wayai pull ${WAYAI_LAYOUT.communitiesSubdir}/${path27.basename(folder)} --theirs \u2014 then make again any edit of yours to keep, and push.`;
21634
23647
  }
21635
23648
  function safeReaddir2(dir) {
21636
23649
  try {
21637
- return fs18.readdirSync(dir);
23650
+ return fs19.readdirSync(dir);
21638
23651
  } catch {
21639
23652
  return [];
21640
23653
  }
21641
23654
  }
21642
- var show, Remote, onOff;
23655
+ var Remote, onOff, plannedCount, plannedRequests;
21643
23656
  var init_sync2 = __esm({
21644
23657
  "src/communities/sync.ts"() {
21645
23658
  "use strict";
@@ -21655,11 +23668,14 @@ var init_sync2 = __esm({
21655
23668
  init_worktree_scope();
21656
23669
  init_community_workspace();
21657
23670
  init_helpers();
23671
+ init_eval_session_control();
21658
23672
  init_api2();
21659
23673
  init_pacer();
21660
23674
  init_files();
23675
+ init_course_files();
23676
+ init_courses();
23677
+ init_listing();
21661
23678
  init_model();
21662
- show = (text) => JSON.stringify(sanitizeTerminalText(text));
21663
23679
  Remote = class {
21664
23680
  constructor(api, pacer, communityId) {
21665
23681
  this.api = api;
@@ -21670,8 +23686,9 @@ var init_sync2 = __esm({
21670
23686
  return (await callCommunities(this.pacer.run(request), notFound)).data;
21671
23687
  }
21672
23688
  /**
21673
- * The four reads (gh #5003 Decision 8's bounded read, courses aside). The org's list both binds
21674
- * the community to the workspace's organization and names it.
23689
+ * gh #5003 Decision 8's bounded read: the org's list, settings, rooms, the join link and the
23690
+ * courses, then each course whole — one request per course. The org's list both binds the
23691
+ * community to the workspace's organization and names it.
21675
23692
  */
21676
23693
  async read(orgId) {
21677
23694
  const communities = await this.send(() => this.api.listOrgCommunities(orgId), orgNotFound(orgId));
@@ -21681,12 +23698,14 @@ var init_sync2 = __esm({
21681
23698
  `Community ${this.communityId} is not one of the communities of organization ${orgId}, the one ${WORKSPACE_MANIFEST_LABEL} names. A community is pulled and pushed from a workspace bound to the organization that owns it.`
21682
23699
  );
21683
23700
  }
21684
- const [settings, rooms, joinLink] = await Promise.all([
23701
+ const [settings, rooms, joinLink, courses] = await Promise.all([
21685
23702
  this.send(() => this.api.getCommunitySettings(this.communityId)),
21686
23703
  this.send(() => this.api.listCommunityRooms(this.communityId)),
21687
- this.send(() => this.api.getCommunityJoinLink(this.communityId))
23704
+ this.send(() => this.api.getCommunityJoinLink(this.communityId)),
23705
+ // Each course whole, as soon as the list names it.
23706
+ this.listCourses().then((list) => Promise.all(list.map((course) => this.exportCourse(course.course_id))))
21688
23707
  ]);
21689
- return { community, settings, rooms, joinLink };
23708
+ return { community, settings, rooms, joinLink, courses: new ServerCourses(courses) };
21690
23709
  }
21691
23710
  updateSettings(body) {
21692
23711
  return this.send(() => this.api.updateCommunitySettings(this.communityId, body));
@@ -21701,8 +23720,34 @@ var init_sync2 = __esm({
21701
23720
  updateRoom(roomId, body) {
21702
23721
  return this.send(() => this.api.updateCommunityRoom(this.communityId, roomId, body));
21703
23722
  }
23723
+ createCourse(body) {
23724
+ return this.send(() => this.api.createCommunityCourse(this.communityId, body));
23725
+ }
23726
+ updateCourse(courseId, body) {
23727
+ return this.send(() => this.api.updateCommunityCourse(this.communityId, courseId, body));
23728
+ }
23729
+ createModule(courseId, body) {
23730
+ return this.send(() => this.api.createCommunityModule(this.communityId, courseId, body));
23731
+ }
23732
+ updateModule(courseId, moduleId, body) {
23733
+ return this.send(() => this.api.updateCommunityModule(this.communityId, courseId, moduleId, body));
23734
+ }
23735
+ createLesson(courseId, body) {
23736
+ return this.send(() => this.api.createCommunityLesson(this.communityId, courseId, body));
23737
+ }
23738
+ updateLesson(courseId, lessonId, body) {
23739
+ return this.send(() => this.api.updateCommunityLesson(this.communityId, courseId, lessonId, body));
23740
+ }
23741
+ listCourses() {
23742
+ return this.send(() => this.api.listCommunityCourseDrafts(this.communityId));
23743
+ }
23744
+ exportCourse(courseId) {
23745
+ return this.send(() => this.api.exportCommunityCourse(this.communityId, courseId));
23746
+ }
21704
23747
  };
21705
23748
  onOff = (enabled) => enabled ? "on" : "off";
23749
+ plannedCount = (phase) => Array.isArray(phase) ? phase.length : phase.planned;
23750
+ plannedRequests = (phase) => Array.isArray(phase) ? phase.reduce((n, write) => n + write.requests, 0) : phase.planned;
21706
23751
  }
21707
23752
  });
21708
23753
 
@@ -21721,9 +23766,9 @@ __export(push_exports, {
21721
23766
  shouldWarnIgnoredPreviewLabel: () => shouldWarnIgnoredPreviewLabel,
21722
23767
  syncAfterPush: () => syncAfterPush
21723
23768
  });
21724
- import * as fs19 from "fs";
21725
- import * as path26 from "path";
21726
- import * as yaml13 from "js-yaml";
23769
+ import * as fs20 from "fs";
23770
+ import * as path28 from "path";
23771
+ import * as yaml14 from "js-yaml";
21727
23772
  function parseArgs6(args2) {
21728
23773
  let autoConfirm = false;
21729
23774
  let label;
@@ -21828,14 +23873,14 @@ function printLocalFileChanges(delta) {
21828
23873
  }
21829
23874
  async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, remoteConfig) {
21830
23875
  requireRealHubFolder(hubFolder, false);
21831
- const agentsDir = path26.join(hubFolder, "agents");
23876
+ const agentsDir = path28.join(hubFolder, "agents");
21832
23877
  let agentsWithIds = [];
21833
- if (fs19.existsSync(agentsDir)) {
21834
- const yamlFiles = fs19.readdirSync(agentsDir).filter((f) => f.endsWith(".yaml"));
23878
+ if (fs20.existsSync(agentsDir)) {
23879
+ const yamlFiles = fs20.readdirSync(agentsDir).filter((f) => f.endsWith(".yaml"));
21835
23880
  for (const file of yamlFiles) {
21836
- const content = readRealFileOrThrow(hubFolder, path26.join(agentsDir, file)) ?? "";
23881
+ const content = readRealFileOrThrow(hubFolder, path28.join(agentsDir, file)) ?? "";
21837
23882
  try {
21838
- const agent = yaml13.load(content);
23883
+ const agent = yaml14.load(content);
21839
23884
  if (agent?.id && agent.name) {
21840
23885
  agentsWithIds.push({ id: agent.id, name: agent.name });
21841
23886
  }
@@ -21848,7 +23893,7 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21848
23893
  const yamlPath = resolveHubYamlPath(hubFolder);
21849
23894
  if (!yamlPath) return;
21850
23895
  const yamlContent = readRealFileOrThrow(hubFolder, yamlPath) ?? "";
21851
- const config = yaml13.load(yamlContent);
23896
+ const config = yaml14.load(yamlContent);
21852
23897
  agentsWithIds = (config.agents || []).filter((a) => !!a.id && !!a.name);
21853
23898
  }
21854
23899
  if (agentsWithIds.length === 0) return;
@@ -21879,18 +23924,18 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21879
23924
  }
21880
23925
  }
21881
23926
  if (renames.length === 0) return;
21882
- if (!fs19.existsSync(agentsDir)) return;
21883
- for (const file of fs19.readdirSync(agentsDir)) {
23927
+ if (!fs20.existsSync(agentsDir)) return;
23928
+ for (const file of fs20.readdirSync(agentsDir)) {
21884
23929
  if (file.startsWith("__rename_temp_") && (file.endsWith(".md") || file.endsWith(".yaml"))) {
21885
23930
  console.warn(` Warning: removing orphaned temp file agents/${file}`);
21886
- fs19.unlinkSync(path26.join(agentsDir, file));
23931
+ fs20.unlinkSync(path28.join(agentsDir, file));
21887
23932
  }
21888
23933
  }
21889
23934
  const renameFileIfExists = (dir, oldName, newName) => {
21890
- const oldPath = path26.join(dir, oldName);
21891
- const newPath = path26.join(dir, newName);
21892
- if (!fs19.existsSync(oldPath)) return false;
21893
- fs19.renameSync(oldPath, newPath);
23935
+ const oldPath = path28.join(dir, oldName);
23936
+ const newPath = path28.join(dir, newName);
23937
+ if (!fs20.existsSync(oldPath)) return false;
23938
+ fs20.renameSync(oldPath, newPath);
21894
23939
  return true;
21895
23940
  };
21896
23941
  const oldSlugs = new Set(renames.map((r) => r.oldSlug));
@@ -21923,9 +23968,9 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21923
23968
  }
21924
23969
  } else {
21925
23970
  for (const { oldSlug, newSlug } of renames) {
21926
- const hasOldFile = extensions.some((ext) => fs19.existsSync(path26.join(agentsDir, `${oldSlug}${ext}`)));
23971
+ const hasOldFile = extensions.some((ext) => fs20.existsSync(path28.join(agentsDir, `${oldSlug}${ext}`)));
21927
23972
  if (!hasOldFile) continue;
21928
- const hasNewFile = extensions.some((ext) => fs19.existsSync(path26.join(agentsDir, `${newSlug}${ext}`)));
23973
+ const hasNewFile = extensions.some((ext) => fs20.existsSync(path28.join(agentsDir, `${newSlug}${ext}`)));
21929
23974
  if (hasNewFile) {
21930
23975
  console.warn(` Warning: skipping rename agents/${oldSlug}.* \u2192 agents/${newSlug}.* (target already exists)`);
21931
23976
  continue;
@@ -21952,7 +23997,7 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21952
23997
  );
21953
23998
  if (updatedYaml !== mainYamlContent) {
21954
23999
  writeFileNoFollow(hubFolder, mainYamlPath, Buffer.from(updatedYaml, "utf-8"));
21955
- console.log(` Updated instructions paths in ${path26.basename(mainYamlPath)}`);
24000
+ console.log(` Updated instructions paths in ${path28.basename(mainYamlPath)}`);
21956
24001
  }
21957
24002
  }
21958
24003
  }
@@ -22020,7 +24065,7 @@ async function pushSingleHub(client, hubId, hubFolder, autoConfirm, organization
22020
24065
  function selectExistingHub(workspaceDir, allHubs, selector, wsLabel) {
22021
24066
  if (selector) {
22022
24067
  const match = allHubs.find(
22023
- (h) => h.hubId === selector || path26.basename(h.hubFolder) === selector
24068
+ (h) => h.hubId === selector || path28.basename(h.hubFolder) === selector
22024
24069
  );
22025
24070
  if (!match) {
22026
24071
  console.error(`No hub matching --hub ${selector} found in ${wsLabel}/.`);
@@ -22089,7 +24134,7 @@ function reportUnresolvedPushTarget(reason, existingHubs, newHubs, selector, wsL
22089
24134
  if (existingHubs.length > 0) {
22090
24135
  console.error(`No pushable hub found in ${wsLabel}/ \u2014 only read-only production mirror folder(s). Edit the linked preview hub, pull one, or create a new hub with \`wayai create\`:`);
22091
24136
  for (const h of existingHubs) {
22092
- console.error(` ${path26.basename(h.hubFolder)} (${h.hubId}, production mirror)`);
24137
+ console.error(` ${path28.basename(h.hubFolder)} (${h.hubId}, production mirror)`);
22093
24138
  }
22094
24139
  } else {
22095
24140
  console.error(`No hub folders found. Create hub files in ${wsLabel}/<hub>/hub.yaml with \`hub: { name: ... }\` and run \`wayai push\` (or \`wayai create\`).`);
@@ -22103,10 +24148,10 @@ function reportUnresolvedPushTarget(reason, existingHubs, newHubs, selector, wsL
22103
24148
  console.error(`Multiple hub folders found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22104
24149
  }
22105
24150
  for (const h of existingHubs) {
22106
- console.error(` ${path26.basename(h.hubFolder)} (${h.hubId})`);
24151
+ console.error(` ${path28.basename(h.hubFolder)} (${h.hubId})`);
22107
24152
  }
22108
24153
  for (const h of newHubs) {
22109
- console.error(` ${path26.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
24154
+ console.error(` ${path28.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
22110
24155
  }
22111
24156
  if (hint) console.error(hint);
22112
24157
  process.exit(1);
@@ -22133,7 +24178,7 @@ async function pushCommand(args2) {
22133
24178
  const client = new ApiClient({ apiUrl: config.api_url, accessToken });
22134
24179
  const workspaceDir = resolveWorkspaceDir();
22135
24180
  const wsLabel = hubsDirLabel(gitRoot);
22136
- if (!fs19.existsSync(workspaceDir)) {
24181
+ if (!fs20.existsSync(workspaceDir)) {
22137
24182
  console.error(`No ${wsLabel}/ directory found. Run \`wayai pull\` first or create hub files in ${wsLabel}/<hub>/hub.yaml.`);
22138
24183
  const hint = communityFoldersHint(gitRoot, "push");
22139
24184
  if (hint) console.error(hint);
@@ -22155,8 +24200,8 @@ async function pushCommand(args2) {
22155
24200
  if (label) {
22156
24201
  console.warn("Note: --label is only used when creating a new hub. To change an existing hub's label, use `wayai relabel`.");
22157
24202
  }
22158
- console.log(`Target hub: ${path26.basename(existing.hubFolder)} (${existing.hubId})`);
22159
- assertInScope("hubs", existing.hubId, path26.basename(existing.hubFolder));
24203
+ console.log(`Target hub: ${path28.basename(existing.hubFolder)} (${existing.hubId})`);
24204
+ assertInScope("hubs", existing.hubId, path28.basename(existing.hubFolder));
22160
24205
  await pushSingleHub(client, existing.hubId, existing.hubFolder, autoConfirm, organizationId);
22161
24206
  return;
22162
24207
  }
@@ -22194,8 +24239,8 @@ __export(pull_exports, {
22194
24239
  resolveHubTarget: () => resolveHubTarget,
22195
24240
  writeProductionMirror: () => writeProductionMirror2
22196
24241
  });
22197
- import * as fs20 from "fs";
22198
- import * as path27 from "path";
24242
+ import * as fs21 from "fs";
24243
+ import * as path29 from "path";
22199
24244
  function parseArgs7(args2) {
22200
24245
  return { autoConfirm: args2.includes("--yes") || args2.includes("-y") };
22201
24246
  }
@@ -22205,7 +24250,7 @@ function resolveHubTarget(workspaceDir, selector) {
22205
24250
  const allHubs = scanWorkspaceHubs(workspaceDir);
22206
24251
  if (selector) {
22207
24252
  const match = allHubs.find(
22208
- (h) => h.hubId === selector || path27.basename(h.hubFolder) === selector
24253
+ (h) => h.hubId === selector || path29.basename(h.hubFolder) === selector
22209
24254
  );
22210
24255
  if (match) return { hubId: match.hubId, hubFolder: match.hubFolder };
22211
24256
  if (UUID_RE3.test(selector)) return { hubId: selector, hubFolder: null };
@@ -22224,7 +24269,7 @@ function resolveHubTarget(workspaceDir, selector) {
22224
24269
  }
22225
24270
  console.error(`Multiple hubs found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22226
24271
  for (const h of previewHubs) {
22227
- console.error(` ${path27.basename(h.hubFolder)} (${h.hubId})`);
24272
+ console.error(` ${path29.basename(h.hubFolder)} (${h.hubId})`);
22228
24273
  }
22229
24274
  process.exit(1);
22230
24275
  }
@@ -22273,7 +24318,7 @@ async function pullCommand(args2) {
22273
24318
  );
22274
24319
  let wroteFiles = false;
22275
24320
  if (!existingFolder && !resolveHubYamlPath(hubFolder)) {
22276
- fs20.mkdirSync(path27.dirname(hubFolder), { recursive: true });
24321
+ fs21.mkdirSync(path29.dirname(hubFolder), { recursive: true });
22277
24322
  console.log("Writing hub configuration...");
22278
24323
  await materializeHubFolder(hubFolder, payload);
22279
24324
  const finalFolder = autoRenameHubFolder(hubFolder, payload.hub.name, payload.hub_environment, payload.hub_id, payload.preview_label, payload.branch_name);
@@ -22358,7 +24403,7 @@ ${why}`);
22358
24403
  }
22359
24404
  async function writeProductionMirror2(workspaceDir, prodPayload) {
22360
24405
  const folder = resolveHubFolder(workspaceDir, prodPayload.hub_id, prodPayload.hub.name, "production", null, null);
22361
- fs20.mkdirSync(path27.dirname(folder), { recursive: true });
24406
+ fs21.mkdirSync(path29.dirname(folder), { recursive: true });
22362
24407
  await materializeHubFolder(folder, prodPayload, { seedAgentContext: false });
22363
24408
  const finalFolder = autoRenameHubFolder(folder, prodPayload.hub.name, "production", prodPayload.hub_id, null, null);
22364
24409
  prependMirrorMarker(finalFolder, prodPayload.hub_id);
@@ -22374,13 +24419,13 @@ async function mirrorLinkedProduction2(client, workspaceDir, productionHubId, or
22374
24419
  }
22375
24420
  }
22376
24421
  function prependMirrorMarker(hubFolder, productionHubId) {
22377
- const hubYaml = path27.join(hubFolder, "hub.yaml");
24422
+ const hubYaml = path29.join(hubFolder, "hub.yaml");
22378
24423
  try {
22379
- const content = fs20.readFileSync(hubYaml, "utf-8");
24424
+ const content = fs21.readFileSync(hubYaml, "utf-8");
22380
24425
  if (content.startsWith(MIRROR_MARKER_PREFIX2)) return;
22381
24426
  const marker = `${MIRROR_MARKER_PREFIX2} ${productionHubId}. Edits are ignored; push is blocked. Edit the linked preview hub instead.
22382
24427
  `;
22383
- fs20.writeFileSync(hubYaml, marker + content, "utf-8");
24428
+ fs21.writeFileSync(hubYaml, marker + content, "utf-8");
22384
24429
  } catch {
22385
24430
  }
22386
24431
  }
@@ -22428,8 +24473,8 @@ var create_exports = {};
22428
24473
  __export(create_exports, {
22429
24474
  createCommand: () => createCommand
22430
24475
  });
22431
- import * as path28 from "path";
22432
- import * as fs21 from "fs";
24476
+ import * as path30 from "path";
24477
+ import * as fs22 from "fs";
22433
24478
  function parseArgs8(args2) {
22434
24479
  let autoConfirm = false;
22435
24480
  let folderSelector;
@@ -22456,7 +24501,7 @@ async function createCommand(args2) {
22456
24501
  const gitRoot = findGitRoot();
22457
24502
  const wsLabel = hubsDirLabel(gitRoot);
22458
24503
  if (gitRoot) warnLayoutOnce(gitRoot);
22459
- if (!fs21.existsSync(workspaceDir)) {
24504
+ if (!fs22.existsSync(workspaceDir)) {
22460
24505
  console.error(`No ${wsLabel}/ directory found. Create hub files in ${wsLabel}/<hub>/hub.yaml with \`hub: { name: ... }\` first.`);
22461
24506
  process.exit(1);
22462
24507
  }
@@ -22465,7 +24510,7 @@ async function createCommand(args2) {
22465
24510
  const resolution = resolveNewHubForCreate(workspaceDir, existingHubs, newHubs, folderSelector);
22466
24511
  if (!resolution.ok) {
22467
24512
  if (resolution.reason === "exists") {
22468
- const folder = path28.basename(resolution.existing.hubFolder);
24513
+ const folder = path30.basename(resolution.existing.hubFolder);
22469
24514
  console.error(`Hub "${folder}" already exists (${resolution.existing.hubId}). Use \`wayai push --hub ${folder}\` to update it.`);
22470
24515
  process.exit(1);
22471
24516
  }
@@ -22479,7 +24524,7 @@ async function createCommand(args2) {
22479
24524
  }
22480
24525
  console.error(`Multiple new hub folders found in ${wsLabel}/. Pass the folder name (\`wayai create <folder>\`) or run from inside one:`);
22481
24526
  for (const h of newHubs) {
22482
- console.error(` ${path28.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
24527
+ console.error(` ${path30.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
22483
24528
  }
22484
24529
  process.exit(1);
22485
24530
  }
@@ -22590,8 +24635,8 @@ var replicate_exports = {};
22590
24635
  __export(replicate_exports, {
22591
24636
  replicateCommand: () => replicateCommand
22592
24637
  });
22593
- import * as fs22 from "fs";
22594
- import * as path29 from "path";
24638
+ import * as fs23 from "fs";
24639
+ import * as path31 from "path";
22595
24640
  function parseArgs10(args2) {
22596
24641
  let label;
22597
24642
  let hubSelector;
@@ -22637,8 +24682,8 @@ async function replicateCommand(args2) {
22637
24682
  payload.preview_label,
22638
24683
  payload.branch_name
22639
24684
  );
22640
- const folderPreExisted = fs22.existsSync(hubFolder);
22641
- fs22.mkdirSync(path29.dirname(hubFolder), { recursive: true });
24685
+ const folderPreExisted = fs23.existsSync(hubFolder);
24686
+ fs23.mkdirSync(path31.dirname(hubFolder), { recursive: true });
22642
24687
  const delta = await materializeHubFolder(hubFolder, payload);
22643
24688
  hubFolder = autoRenameHubFolder(
22644
24689
  hubFolder,
@@ -22650,8 +24695,8 @@ async function replicateCommand(args2) {
22650
24695
  );
22651
24696
  seedScopeIfEmpty("hubs", previewHubId);
22652
24697
  if (folderPreExisted) printLocalFileChanges(delta);
22653
- console.log(`Preview written to ${path29.basename(hubFolder)}`);
22654
- console.log(` Switch to it with: wayai use ${path29.basename(hubFolder)}`);
24698
+ console.log(`Preview written to ${path31.basename(hubFolder)}`);
24699
+ console.log(` Switch to it with: wayai use ${path31.basename(hubFolder)}`);
22655
24700
  }
22656
24701
  var init_replicate = __esm({
22657
24702
  "src/commands/replicate.ts"() {
@@ -22674,7 +24719,7 @@ var relabel_exports = {};
22674
24719
  __export(relabel_exports, {
22675
24720
  relabelCommand: () => relabelCommand
22676
24721
  });
22677
- import * as path30 from "path";
24722
+ import * as path32 from "path";
22678
24723
  function parseArgs11(args2) {
22679
24724
  let label;
22680
24725
  let clear = false;
@@ -22718,7 +24763,7 @@ async function relabelCommand(args2) {
22718
24763
  } else {
22719
24764
  console.error(`Multiple hubs found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22720
24765
  for (const h of previewHubs) {
22721
- console.error(` ${path30.basename(h.hubFolder)} (${h.hubId})`);
24766
+ console.error(` ${path32.basename(h.hubFolder)} (${h.hubId})`);
22722
24767
  }
22723
24768
  }
22724
24769
  process.exit(1);
@@ -22727,7 +24772,7 @@ async function relabelCommand(args2) {
22727
24772
  console.log("This is a read-only production mirror \u2014 production hubs have no preview label. Relabel the linked preview hub instead.");
22728
24773
  return;
22729
24774
  }
22730
- assertInScope("hubs", target.hubId, path30.basename(target.hubFolder));
24775
+ assertInScope("hubs", target.hubId, path32.basename(target.hubFolder));
22731
24776
  console.log(normalizedLabel ? `Setting preview label to "${normalizedLabel}"...` : "Clearing preview label...");
22732
24777
  const { data } = await client.relabelPreview(target.hubId, normalizedLabel);
22733
24778
  const row = data[0];
@@ -22743,7 +24788,7 @@ async function relabelCommand(args2) {
22743
24788
  );
22744
24789
  console.log(serverLabel ? `Preview label set to "${serverLabel}".` : "Preview label cleared.");
22745
24790
  if (finalFolder !== target.hubFolder) {
22746
- console.log(`Hub folder is now ${path30.basename(finalFolder)}`);
24791
+ console.log(`Hub folder is now ${path32.basename(finalFolder)}`);
22747
24792
  }
22748
24793
  }
22749
24794
  var init_relabel = __esm({
@@ -22769,7 +24814,7 @@ __export(publish_exports, {
22769
24814
  renderHubDiff: () => renderHubDiff,
22770
24815
  runPublish: () => runPublish
22771
24816
  });
22772
- import * as path31 from "path";
24817
+ import * as path33 from "path";
22773
24818
  function parseArgs12(args2) {
22774
24819
  let autoConfirm = false;
22775
24820
  let hubSelector;
@@ -22878,7 +24923,7 @@ async function publishCommand(args2) {
22878
24923
  return;
22879
24924
  }
22880
24925
  }
22881
- assertInScope("hubs", hubId, hubFolder ? path31.basename(hubFolder) : void 0);
24926
+ assertInScope("hubs", hubId, hubFolder ? path33.basename(hubFolder) : void 0);
22882
24927
  await runPublish(client, { hubId, localConfig, organizationId, autoConfirm });
22883
24928
  }
22884
24929
  var STATUS_STYLE, SECTION_TITLES;
@@ -22926,7 +24971,7 @@ var init_publish = __esm({
22926
24971
  });
22927
24972
 
22928
24973
  // src/lib/scope-selector.ts
22929
- import * as path32 from "path";
24974
+ import * as path34 from "path";
22930
24975
  function findAddFlag(args2) {
22931
24976
  return args2.find((arg) => arg === "--add" || arg.startsWith("--add="));
22932
24977
  }
@@ -22983,7 +25028,7 @@ function resolveHubSelectorToId(gitRoot, selector) {
22983
25028
  const match = findHubByFolderName(workspaceDir, selector);
22984
25029
  if (match) return match.hubId;
22985
25030
  const newHubs = scanNewHubs(workspaceDir);
22986
- const pending = newHubs.find((h) => path32.basename(h.hubFolder) === selector);
25031
+ const pending = newHubs.find((h) => path34.basename(h.hubFolder) === selector);
22987
25032
  if (pending) {
22988
25033
  throw expected(
22989
25034
  `"${selector}" is a new hub that hasn't been created on the platform yet, so it has no id to bind to.
@@ -22994,9 +25039,9 @@ Create it first \u2014 the worktree scope picks it up automatically afterward:
22994
25039
  throw expected(
22995
25040
  [
22996
25041
  `No hub matching "${selector}" found in ${hubsDirLabel(gitRoot)}/. Pass a UUID or a folder name from:`,
22997
- ...scanWorkspaceHubs(workspaceDir).map((h) => ` ${path32.basename(h.hubFolder)} (${h.hubId})`),
25042
+ ...scanWorkspaceHubs(workspaceDir).map((h) => ` ${path34.basename(h.hubFolder)} (${h.hubId})`),
22998
25043
  ...newHubs.map(
22999
- (h) => ` ${path32.basename(h.hubFolder)} (new \u2014 "${h.hubName}", run \`wayai create ${path32.basename(h.hubFolder)}\`)`
25044
+ (h) => ` ${path34.basename(h.hubFolder)} (new \u2014 "${h.hubName}", run \`wayai create ${path34.basename(h.hubFolder)}\`)`
23000
25045
  )
23001
25046
  ].join("\n")
23002
25047
  );
@@ -23086,11 +25131,11 @@ __export(migrate_exports, {
23086
25131
  migrateCommand: () => migrateCommand
23087
25132
  });
23088
25133
  import { execFileSync as execFileSync3 } from "child_process";
23089
- import * as fs23 from "fs";
23090
- import * as path33 from "path";
25134
+ import * as fs24 from "fs";
25135
+ import * as path35 from "path";
23091
25136
  function isTracked(gitRoot, p) {
23092
25137
  try {
23093
- execFileSync3("git", ["ls-files", "--error-unmatch", "--", path33.relative(gitRoot, p)], {
25138
+ execFileSync3("git", ["ls-files", "--error-unmatch", "--", path35.relative(gitRoot, p)], {
23094
25139
  cwd: gitRoot,
23095
25140
  stdio: ["pipe", "pipe", "pipe"]
23096
25141
  });
@@ -23100,10 +25145,10 @@ function isTracked(gitRoot, p) {
23100
25145
  }
23101
25146
  }
23102
25147
  function moveDir(gitRoot, from, to) {
23103
- fs23.mkdirSync(path33.dirname(to), { recursive: true });
25148
+ fs24.mkdirSync(path35.dirname(to), { recursive: true });
23104
25149
  if (isTracked(gitRoot, from)) {
23105
25150
  try {
23106
- execFileSync3("git", ["mv", path33.relative(gitRoot, from), path33.relative(gitRoot, to)], {
25151
+ execFileSync3("git", ["mv", path35.relative(gitRoot, from), path35.relative(gitRoot, to)], {
23107
25152
  cwd: gitRoot,
23108
25153
  stdio: ["pipe", "pipe", "pipe"]
23109
25154
  });
@@ -23111,7 +25156,7 @@ function moveDir(gitRoot, from, to) {
23111
25156
  } catch {
23112
25157
  }
23113
25158
  }
23114
- fs23.renameSync(from, to);
25159
+ fs24.renameSync(from, to);
23115
25160
  return "fs";
23116
25161
  }
23117
25162
  async function migrateCommand(_args) {
@@ -23120,12 +25165,12 @@ async function migrateCommand(_args) {
23120
25165
  console.error("Not inside a git repository.");
23121
25166
  process.exit(1);
23122
25167
  }
23123
- const rel = (p) => path33.relative(gitRoot, p);
23124
- const newWs = path33.join(gitRoot, WAYAI_LAYOUT.wsDir);
23125
- const legacyWs = path33.join(gitRoot, WAYAI_LAYOUT.legacy.wsDir);
23126
- const legacyOrg = path33.join(gitRoot, WAYAI_LAYOUT.legacy.orgAtRoot);
23127
- const newHubs = path33.join(newWs, WAYAI_LAYOUT.hubsSubdir);
23128
- const newOrg = path33.join(newWs, WAYAI_LAYOUT.orgSubdir);
25168
+ const rel = (p) => path35.relative(gitRoot, p);
25169
+ const newWs = path35.join(gitRoot, WAYAI_LAYOUT.wsDir);
25170
+ const legacyWs = path35.join(gitRoot, WAYAI_LAYOUT.legacy.wsDir);
25171
+ const legacyOrg = path35.join(gitRoot, WAYAI_LAYOUT.legacy.orgAtRoot);
25172
+ const newHubs = path35.join(newWs, WAYAI_LAYOUT.hubsSubdir);
25173
+ const newOrg = path35.join(newWs, WAYAI_LAYOUT.orgSubdir);
23129
25174
  requireRealSubdirNoSymlink(gitRoot, newHubs, false);
23130
25175
  requireRealSubdirNoSymlink(gitRoot, newOrg, false);
23131
25176
  readRealFileOrThrow(gitRoot, workspaceManifestPath(gitRoot));
@@ -23278,12 +25323,12 @@ var send_message_exports = {};
23278
25323
  __export(send_message_exports, {
23279
25324
  sendMessageCommand: () => sendMessageCommand
23280
25325
  });
23281
- import * as fs24 from "fs";
23282
- import * as path34 from "path";
25326
+ import * as fs25 from "fs";
25327
+ import * as path36 from "path";
23283
25328
  function statAttachment(filePath) {
23284
25329
  let stat2;
23285
25330
  try {
23286
- stat2 = fs24.statSync(filePath);
25331
+ stat2 = fs25.statSync(filePath);
23287
25332
  } catch {
23288
25333
  console.error(`Error: file not found: ${filePath}`);
23289
25334
  process.exit(1);
@@ -23295,11 +25340,11 @@ function statAttachment(filePath) {
23295
25340
  return { filePath, size: stat2.size };
23296
25341
  }
23297
25342
  function readAttachment(filePath, size) {
23298
- const fileName = path34.basename(filePath);
23299
- const ext = path34.extname(fileName).replace(/^\./, "");
25343
+ const fileName = path36.basename(filePath);
25344
+ const ext = path36.extname(fileName).replace(/^\./, "");
23300
25345
  return {
23301
25346
  file_name: fileName,
23302
- file_binary: fs24.readFileSync(filePath).toString("base64"),
25347
+ file_binary: fs25.readFileSync(filePath).toString("base64"),
23303
25348
  file_size: size,
23304
25349
  ...ext && { file_extension: ext }
23305
25350
  };
@@ -23346,7 +25391,7 @@ async function sendMessageCommand(args2) {
23346
25391
  }
23347
25392
  const stats = filePaths.map(statAttachment);
23348
25393
  const filesTotal = stats.reduce((sum, s) => sum + base64Length(s.size), 0);
23349
- const fileDetail = () => stats.map((s) => `${path34.basename(s.filePath)} ${asMb(s.size)} MB`).join(", ");
25394
+ const fileDetail = () => stats.map((s) => `${path36.basename(s.filePath)} ${asMb(s.size)} MB`).join(", ");
23350
25395
  if (filesTotal > MAX_MESSAGE_BODY_BYTES) {
23351
25396
  console.error(
23352
25397
  `Error: ${fileDetail()} encode to ~${asMb(filesTotal)} MB, over the ${asMb(MAX_MESSAGE_BODY_BYTES)} MB per-request limit.`
@@ -23354,10 +25399,10 @@ async function sendMessageCommand(args2) {
23354
25399
  console.error("Base64 inflates a file by about a third, so ~7 MB of files is the practical ceiling.");
23355
25400
  process.exit(1);
23356
25401
  }
23357
- const audioStats = stats.filter((s) => isAudioAttachmentFileName(path34.basename(s.filePath)));
25402
+ const audioStats = stats.filter((s) => isAudioAttachmentFileName(path36.basename(s.filePath)));
23358
25403
  if (audioStats.length > 1) {
23359
25404
  console.error(
23360
- `Error: at most one audio file per message (got ${audioStats.length}: ${audioStats.map((s) => path34.basename(s.filePath)).join(", ")}).`
25405
+ `Error: at most one audio file per message (got ${audioStats.length}: ${audioStats.map((s) => path36.basename(s.filePath)).join(", ")}).`
23361
25406
  );
23362
25407
  console.error("Audio is transcribed via the hub's STT connection, and only one file per message is transcribed.");
23363
25408
  process.exit(1);
@@ -23365,7 +25410,7 @@ async function sendMessageCommand(args2) {
23365
25410
  const oversizeAudio = audioStats.find((s) => base64Length(s.size) > MAX_AUDIO_FILE_BASE64_BYTES);
23366
25411
  if (oversizeAudio) {
23367
25412
  console.error(
23368
- `Error: ${path34.basename(oversizeAudio.filePath)} (${asMb(oversizeAudio.size)} MB) is over the ~${asMb(MAX_AUDIO_FILE_BASE64_BYTES * 3 / 4)} MB audio limit for transcription.`
25413
+ `Error: ${path36.basename(oversizeAudio.filePath)} (${asMb(oversizeAudio.size)} MB) is over the ~${asMb(MAX_AUDIO_FILE_BASE64_BYTES * 3 / 4)} MB audio limit for transcription.`
23369
25414
  );
23370
25415
  process.exit(1);
23371
25416
  }
@@ -24841,133 +26886,6 @@ var init_evals = __esm({
24841
26886
  }
24842
26887
  });
24843
26888
 
24844
- // src/lib/eval-session-control.ts
24845
- function formatEvalSessionStopCommand(sessionId, hubId) {
24846
- return `wayai eval session stop ${sessionId} --hub ${hubId}`;
24847
- }
24848
- function formatEvalResultsCommand(sessionId, hubId) {
24849
- return `wayai eval-results --session ${sessionId} --hub ${hubId}`;
24850
- }
24851
- function describeSeedRelease(status) {
24852
- if (status === "pending") {
24853
- return "A turn that started before the stop is still finishing \u2014 its fixture stays reserved until that turn settles.";
24854
- }
24855
- if (status === "releasing") return "Its fixture is being released.";
24856
- return "A turn already in progress may still be finishing \u2014 wait for it before starting another run.";
24857
- }
24858
- function classifyStopOutcome(response) {
24859
- return response.data.stopped ? { kind: "cancelled", seedRelease: response.data.seed_release_status } : { kind: "already_finished", status: response.data.session.session_status };
24860
- }
24861
- function isTerminalStopFailure(err) {
24862
- return err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 408 && err.status !== 429;
24863
- }
24864
- async function stopEvalSessionWithRetries(stop, opts = {}) {
24865
- const attempts = opts.attempts ?? STOP_ATTEMPTS;
24866
- const attemptTimeoutMs = opts.attemptTimeoutMs ?? STOP_ATTEMPT_TIMEOUT_MS;
24867
- const backoffMs = opts.backoffMs ?? STOP_BACKOFF_MS;
24868
- const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
24869
- let lastError = new Error("Stop was never attempted");
24870
- for (let attempt = 1; attempt <= attempts; attempt++) {
24871
- let timeout;
24872
- try {
24873
- return await Promise.race([
24874
- stop(),
24875
- new Promise((_resolve, reject) => {
24876
- timeout = setTimeout(
24877
- () => reject(new Error(`Stop attempt ${attempt} timed out after ${attemptTimeoutMs}ms`)),
24878
- attemptTimeoutMs
24879
- );
24880
- })
24881
- ]);
24882
- } catch (err) {
24883
- lastError = err;
24884
- if (isTerminalStopFailure(err)) break;
24885
- } finally {
24886
- if (timeout !== void 0) clearTimeout(timeout);
24887
- }
24888
- const delay2 = backoffMs[attempt - 1];
24889
- if (attempt < attempts && delay2) await sleep3(delay2);
24890
- }
24891
- throw lastError;
24892
- }
24893
- function installEvalSignalHandlers(input) {
24894
- const exit = input.exit ?? ((code) => process.exit(code));
24895
- const manualStopCommand = formatEvalSessionStopCommand(input.sessionId, input.hubId);
24896
- let signalCount = 0;
24897
- let disposed = false;
24898
- let settle;
24899
- const settled = new Promise((resolve12) => {
24900
- settle = resolve12;
24901
- });
24902
- const listeners = /* @__PURE__ */ new Map();
24903
- const dispose = () => {
24904
- if (disposed) return;
24905
- disposed = true;
24906
- for (const [signal, listener] of listeners) process.off(signal, listener);
24907
- };
24908
- const finish = (exitCode) => {
24909
- dispose();
24910
- settle();
24911
- exit(exitCode);
24912
- };
24913
- const handle = (signal) => {
24914
- signalCount += 1;
24915
- const signalExitCode = SIGNAL_EXIT_CODE[signal];
24916
- if (signalCount > 1) {
24917
- console.error("\nSecond signal received; exiting immediately. The remote eval session may still be running.");
24918
- console.error(`Run: ${manualStopCommand}`);
24919
- finish(signalExitCode);
24920
- return;
24921
- }
24922
- console.error(`
24923
- ${signal} received; stopping remote eval session ${input.sessionId}...`);
24924
- void stopEvalSessionWithRetries(input.stop).then((response) => {
24925
- if (signalCount > 1) return;
24926
- const outcome = classifyStopOutcome(response);
24927
- if (outcome.kind === "already_finished") {
24928
- console.error(`Session had already finished (${outcome.status}) \u2014 nothing was cancelled.`);
24929
- console.error(`See its results: ${formatEvalResultsCommand(input.sessionId, input.hubId)}`);
24930
- } else {
24931
- console.error("Remote session cancellation requested.");
24932
- console.error(describeSeedRelease(outcome.seedRelease));
24933
- }
24934
- finish(signalExitCode);
24935
- }).catch((err) => {
24936
- if (signalCount > 1) return;
24937
- console.error(`Failed to confirm remote cancellation: ${extractApiMessage(err)}`);
24938
- console.error(`The session may still be running. Run: ${manualStopCommand}`);
24939
- finish(1);
24940
- });
24941
- };
24942
- for (const signal of HANDLED_SIGNALS) {
24943
- const listener = () => handle(signal);
24944
- listeners.set(signal, listener);
24945
- process.on(signal, listener);
24946
- }
24947
- return {
24948
- manualStopCommand,
24949
- async interrupted() {
24950
- if (signalCount === 0) return false;
24951
- await settled;
24952
- return true;
24953
- },
24954
- dispose
24955
- };
24956
- }
24957
- var SIGNAL_EXIT_CODE, HANDLED_SIGNALS, STOP_ATTEMPTS, STOP_ATTEMPT_TIMEOUT_MS, STOP_BACKOFF_MS;
24958
- var init_eval_session_control = __esm({
24959
- "src/lib/eval-session-control.ts"() {
24960
- "use strict";
24961
- init_api_client();
24962
- init_errors2();
24963
- SIGNAL_EXIT_CODE = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 };
24964
- HANDLED_SIGNALS = Object.keys(SIGNAL_EXIT_CODE);
24965
- STOP_ATTEMPTS = 3;
24966
- STOP_ATTEMPT_TIMEOUT_MS = 1e4;
24967
- STOP_BACKOFF_MS = [500, 1e3];
24968
- }
24969
- });
24970
-
24971
26889
  // src/lib/eval-fixture-queue.ts
24972
26890
  function isQueueableLaunchError(err) {
24973
26891
  const code = extractApiErrorCode(err);
@@ -25520,7 +27438,7 @@ var init_eval_results = __esm({
25520
27438
  import { createHash as createHash4 } from "crypto";
25521
27439
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync17, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
25522
27440
  import { homedir as homedir5 } from "os";
25523
- import { dirname as dirname13, isAbsolute as isAbsolute2, join as join30, resolve as resolve10 } from "path";
27441
+ import { dirname as dirname12, isAbsolute as isAbsolute2, join as join31, resolve as resolve10 } from "path";
25524
27442
  function parseWav(buf, name = "clip") {
25525
27443
  if (buf.length < 12 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
25526
27444
  throw new Error(`${name} is not a WAV file.`);
@@ -25559,7 +27477,7 @@ function parseWav(buf, name = "clip") {
25559
27477
  throw new Error(`${name} holds no audio data.`);
25560
27478
  }
25561
27479
  function defaultClipCacheDir() {
25562
- return join30(homedir5(), ".wayai", "call-clips");
27480
+ return join31(homedir5(), ".wayai", "call-clips");
25563
27481
  }
25564
27482
  function pcmClip(buf) {
25565
27483
  const samples = new Int16Array(Math.floor(buf.length / 2));
@@ -25608,15 +27526,15 @@ var init_caller_audio = __esm({
25608
27526
  }
25609
27527
  describe;
25610
27528
  async clipFor(line) {
25611
- const path39 = line.clip ? isAbsolute2(line.clip) ? line.clip : resolve10(this.planDir, line.clip) : this.dir ? join30(this.dir, `${line.n}.wav`) : null;
25612
- if (!path39) throw new Error(`Line ${line.n} has no clip: give --clips <dir>, or a "clip" for it in the plan.`);
27529
+ const path41 = line.clip ? isAbsolute2(line.clip) ? line.clip : resolve10(this.planDir, line.clip) : this.dir ? join31(this.dir, `${line.n}.wav`) : null;
27530
+ if (!path41) throw new Error(`Line ${line.n} has no clip: give --clips <dir>, or a "clip" for it in the plan.`);
25613
27531
  let buf;
25614
27532
  try {
25615
- buf = readFileSync17(path39);
27533
+ buf = readFileSync17(path41);
25616
27534
  } catch {
25617
- throw new Error(`Line ${line.n}'s clip ${path39} cannot be read.`);
27535
+ throw new Error(`Line ${line.n}'s clip ${path41} cannot be read.`);
25618
27536
  }
25619
- return parseWav(buf, path39);
27537
+ return parseWav(buf, path41);
25620
27538
  }
25621
27539
  };
25622
27540
  OPENAI_PCM_SAMPLE_RATE = 24e3;
@@ -25643,20 +27561,20 @@ var init_caller_audio = __esm({
25643
27561
  const key = createHash4("sha256").update(`${this.model}
25644
27562
  ${this.voice}
25645
27563
  ${text}`).digest("hex");
25646
- return join30(this.cacheDir, `${key}.pcm`);
27564
+ return join31(this.cacheDir, `${key}.pcm`);
25647
27565
  }
25648
27566
  clipFor(line) {
25649
- const path39 = this.cachePath(line.say);
25650
- let pending = this.inFlight.get(path39);
27567
+ const path41 = this.cachePath(line.say);
27568
+ let pending = this.inFlight.get(path41);
25651
27569
  if (!pending) {
25652
- pending = this.readOrSynthesize(line, path39);
25653
- this.inFlight.set(path39, pending);
27570
+ pending = this.readOrSynthesize(line, path41);
27571
+ this.inFlight.set(path41, pending);
25654
27572
  }
25655
27573
  return pending;
25656
27574
  }
25657
- async readOrSynthesize(line, path39) {
27575
+ async readOrSynthesize(line, path41) {
25658
27576
  try {
25659
- return pcmClip(readFileSync17(path39));
27577
+ return pcmClip(readFileSync17(path41));
25660
27578
  } catch {
25661
27579
  }
25662
27580
  const res = await this.doFetch("https://api.openai.com/v1/audio/speech", {
@@ -25670,10 +27588,10 @@ ${text}`).digest("hex");
25670
27588
  }
25671
27589
  const buf = Buffer.from(await res.arrayBuffer());
25672
27590
  if (buf.length < 2) throw new Error(`OpenAI speech synthesis returned no audio for line ${line.n}.`);
25673
- mkdirSync12(dirname13(path39), { recursive: true });
25674
- const tmp = `${path39}.${process.pid}.tmp`;
27591
+ mkdirSync12(dirname12(path41), { recursive: true });
27592
+ const tmp = `${path41}.${process.pid}.tmp`;
25675
27593
  writeFileSync7(tmp, buf);
25676
- renameSync5(tmp, path39);
27594
+ renameSync5(tmp, path41);
25677
27595
  return pcmClip(buf);
25678
27596
  }
25679
27597
  };
@@ -27163,7 +29081,7 @@ __export(eval_call_exports, {
27163
29081
  suiteExitCode: () => suiteExitCode
27164
29082
  });
27165
29083
  import { accessSync, constants as fsConstants, existsSync as existsSync16, readFileSync as readFileSync18, statSync as statSync8, writeFileSync as writeFileSync8 } from "fs";
27166
- import { dirname as dirname14, resolve as resolve11 } from "path";
29084
+ import { dirname as dirname13, resolve as resolve11 } from "path";
27167
29085
  function fail(message) {
27168
29086
  throw new Error(message);
27169
29087
  }
@@ -27238,15 +29156,15 @@ function parseEvalCallArgs(args2) {
27238
29156
  if (parsed.jsonPath) checkReportPath(parsed.jsonPath);
27239
29157
  return parsed;
27240
29158
  }
27241
- function checkReportPath(path39) {
27242
- const target = resolve11(path39);
27243
- const dir = dirname14(target);
27244
- if (!existsSync16(dir) || !statSync8(dir).isDirectory()) fail(`--json ${path39}: the folder ${dir} does not exist.`);
27245
- if (existsSync16(target) && statSync8(target).isDirectory()) fail(`--json ${path39} is a folder; name a file.`);
29159
+ function checkReportPath(path41) {
29160
+ const target = resolve11(path41);
29161
+ const dir = dirname13(target);
29162
+ if (!existsSync16(dir) || !statSync8(dir).isDirectory()) fail(`--json ${path41}: the folder ${dir} does not exist.`);
29163
+ if (existsSync16(target) && statSync8(target).isDirectory()) fail(`--json ${path41} is a folder; name a file.`);
27246
29164
  try {
27247
29165
  accessSync(existsSync16(target) ? target : dir, fsConstants.W_OK);
27248
29166
  } catch {
27249
- fail(`--json ${path39} cannot be written here.`);
29167
+ fail(`--json ${path41} cannot be written here.`);
27250
29168
  }
27251
29169
  }
27252
29170
  function suiteExitCode(report2, runs, reportWritten) {
@@ -27257,7 +29175,7 @@ function formatCallPages(runs) {
27257
29175
  return pages.length > 0 ? ["Call pages (the transcript, and the recording once it is ready):", ...pages] : [];
27258
29176
  }
27259
29177
  function chooseAudioSource(options, plan, env = process.env) {
27260
- const planDir = options.planPath ? dirname14(resolve11(options.planPath)) : process.cwd();
29178
+ const planDir = options.planPath ? dirname13(resolve11(options.planPath)) : process.cwd();
27261
29179
  const planHasClips = Object.values(plan.lines).some((line) => line.clip);
27262
29180
  if (options.clipsDir || planHasClips) {
27263
29181
  if (options.clipsDir && !existsSync16(options.clipsDir)) fail(`--clips ${options.clipsDir} does not exist.`);
@@ -27456,9 +29374,9 @@ var eval_capture_exports = {};
27456
29374
  __export(eval_capture_exports, {
27457
29375
  evalCaptureCommand: () => evalCaptureCommand
27458
29376
  });
27459
- import * as fs25 from "fs";
27460
- import * as path35 from "path";
27461
- import * as yaml14 from "js-yaml";
29377
+ import * as fs26 from "fs";
29378
+ import * as path37 from "path";
29379
+ import * as yaml15 from "js-yaml";
27462
29380
  function isValidSetName(name) {
27463
29381
  if (name.length === 0 || name === "." || name === "..") return false;
27464
29382
  if (name.startsWith(".")) return false;
@@ -27523,25 +29441,25 @@ async function evalCaptureCommand(args2) {
27523
29441
  const setFolderName = targetSetName;
27524
29442
  const scenarioName = parsed.evalName ?? `Capture ${parsed.conversationId.slice(0, 8)}`;
27525
29443
  const slug = slugify2(scenarioName);
27526
- const evalsDir = path35.join(hubFolder, "evals");
27527
- const targetDir = path35.join(evalsDir, setFolderName);
27528
- const targetPath = path35.join(targetDir, `${slug}.yaml`);
27529
- if (!targetPath.startsWith(evalsDir + path35.sep)) {
27530
- console.error(`Resolved path "${path35.relative(hubFolder, targetPath)}" escapes evals/. Aborting.`);
29444
+ const evalsDir = path37.join(hubFolder, "evals");
29445
+ const targetDir = path37.join(evalsDir, setFolderName);
29446
+ const targetPath = path37.join(targetDir, `${slug}.yaml`);
29447
+ if (!targetPath.startsWith(evalsDir + path37.sep)) {
29448
+ console.error(`Resolved path "${path37.relative(hubFolder, targetPath)}" escapes evals/. Aborting.`);
27531
29449
  process.exit(1);
27532
29450
  }
27533
29451
  if (!ensureRealSubdirNoSymlink(hubFolder, targetDir, false)) {
27534
- console.error(`${path35.relative(hubFolder, targetDir)} is reached through a symlink. Aborting.`);
29452
+ console.error(`${path37.relative(hubFolder, targetDir)} is reached through a symlink. Aborting.`);
27535
29453
  process.exit(1);
27536
29454
  }
27537
29455
  let targetTaken = true;
27538
29456
  try {
27539
- fs25.lstatSync(targetPath);
29457
+ fs26.lstatSync(targetPath);
27540
29458
  } catch {
27541
29459
  targetTaken = false;
27542
29460
  }
27543
29461
  if (targetTaken) {
27544
- console.error(`File already exists: ${path35.relative(hubFolder, targetPath)}. Use --name to choose a different name.`);
29462
+ console.error(`File already exists: ${path37.relative(hubFolder, targetPath)}. Use --name to choose a different name.`);
27545
29463
  process.exit(1);
27546
29464
  }
27547
29465
  console.log("Resolving scenario set...");
@@ -27573,14 +29491,14 @@ async function evalCaptureCommand(args2) {
27573
29491
  ...captured.evaluator_instructions ? { evaluator_instructions: captured.evaluator_instructions } : {}
27574
29492
  };
27575
29493
  const yamlObj = buildEvalYamlObject(evalEntry, slug);
27576
- const outcome = createFileNoFollow(hubFolder, targetPath, Buffer.from(yaml14.dump(yamlObj, YAML_DUMP_OPTIONS), "utf-8"));
29494
+ const outcome = createFileNoFollow(hubFolder, targetPath, Buffer.from(yaml15.dump(yamlObj, YAML_DUMP_OPTIONS), "utf-8"));
27577
29495
  if (outcome !== "created") {
27578
29496
  console.error(
27579
- `Could not write ${path35.relative(hubFolder, targetPath)} (${outcome === "exists" ? "it appeared meanwhile" : "its folder is reached through a symlink"}). The scenario was created on the platform as ${captured.eval_id} \u2014 run \`wayai pull\` to fetch it.`
29497
+ `Could not write ${path37.relative(hubFolder, targetPath)} (${outcome === "exists" ? "it appeared meanwhile" : "its folder is reached through a symlink"}). The scenario was created on the platform as ${captured.eval_id} \u2014 run \`wayai pull\` to fetch it.`
27580
29498
  );
27581
29499
  process.exit(1);
27582
29500
  }
27583
- const relPath = path35.relative(process.cwd(), targetPath);
29501
+ const relPath = path37.relative(process.cwd(), targetPath);
27584
29502
  console.log(`
27585
29503
  Wrote ${relPath}`);
27586
29504
  console.log("Run `wayai pull` to refresh the agent display name, then commit. The scenario is already on the platform.");
@@ -28960,20 +30878,20 @@ var init_set_connection_credential = __esm({
28960
30878
  });
28961
30879
 
28962
30880
  // src/lib/org-workspace.ts
28963
- import * as fs26 from "fs";
28964
- import * as path36 from "path";
28965
- import * as yaml15 from "js-yaml";
30881
+ import * as fs27 from "fs";
30882
+ import * as path38 from "path";
30883
+ import * as yaml16 from "js-yaml";
28966
30884
  function getOrgDir(gitRoot) {
28967
30885
  return resolveLayout(gitRoot).orgDir;
28968
30886
  }
28969
30887
  function orgManifestExists(orgDir) {
28970
- return fs26.existsSync(path36.join(orgDir, ORG_MANIFEST_NAME));
30888
+ return fs27.existsSync(path38.join(orgDir, ORG_MANIFEST_NAME));
28971
30889
  }
28972
30890
  function parseOrgResources(orgDir) {
28973
- const resourcesDir = path36.join(orgDir, "resources");
30891
+ const resourcesDir = path38.join(orgDir, "resources");
28974
30892
  requireRealSubdirNoSymlink(orgDir, resourcesDir, false);
28975
- const manifestText = readRealFileOrThrow(orgDir, path36.join(orgDir, ORG_MANIFEST_NAME));
28976
- const manifest = (manifestText !== null ? yaml15.load(manifestText) : null) ?? {};
30893
+ const manifestText = readRealFileOrThrow(orgDir, path38.join(orgDir, ORG_MANIFEST_NAME));
30894
+ const manifest = (manifestText !== null ? yaml16.load(manifestText) : null) ?? {};
28977
30895
  const rawResources = Array.isArray(manifest.resources) ? manifest.resources : [];
28978
30896
  const resources = rawResources.map((res) => {
28979
30897
  const resource = { name: res.name };
@@ -28985,9 +30903,9 @@ function parseOrgResources(orgDir) {
28985
30903
  if (res.environment) resource.environment = res.environment;
28986
30904
  if (Array.isArray(res.tags)) resource.tags = res.tags;
28987
30905
  if (Array.isArray(res.folders)) resource.folders = res.folders;
28988
- const resDir = path36.join(resourcesDir, slugify2(resource.name));
30906
+ const resDir = path38.join(resourcesDir, slugify2(resource.name));
28989
30907
  requireRealSubdirNoSymlink(orgDir, resDir, false);
28990
- if (fs26.existsSync(resDir)) {
30908
+ if (fs27.existsSync(resDir)) {
28991
30909
  const files = scanResourceFiles(resDir, "");
28992
30910
  if (files.length > 0) resource.files = files;
28993
30911
  }
@@ -28996,8 +30914,8 @@ function parseOrgResources(orgDir) {
28996
30914
  return { version: 1, resources };
28997
30915
  }
28998
30916
  function writeOrgResources(orgDir, payload) {
28999
- fs26.mkdirSync(orgDir, { recursive: true });
29000
- const resourcesDir = path36.join(orgDir, "resources");
30917
+ fs27.mkdirSync(orgDir, { recursive: true });
30918
+ const resourcesDir = path38.join(orgDir, "resources");
29001
30919
  requireRealSubdirNoSymlink(orgDir, resourcesDir, false);
29002
30920
  const resources = payload.resources ?? [];
29003
30921
  const manifestResources = resources.map((r) => {
@@ -29006,19 +30924,19 @@ function writeOrgResources(orgDir, payload) {
29006
30924
  });
29007
30925
  writeFileNoFollow(
29008
30926
  orgDir,
29009
- path36.join(orgDir, ORG_MANIFEST_NAME),
29010
- Buffer.from(yaml15.dump({ version: 1, resources: manifestResources }, YAML_DUMP_OPTIONS), "utf-8")
30927
+ path38.join(orgDir, ORG_MANIFEST_NAME),
30928
+ Buffer.from(yaml16.dump({ version: 1, resources: manifestResources }, YAML_DUMP_OPTIONS), "utf-8")
29011
30929
  );
29012
30930
  const currentSlugs = /* @__PURE__ */ new Set();
29013
30931
  for (const resource of resources) {
29014
30932
  const resSlug = slugify2(resource.name);
29015
30933
  currentSlugs.add(resSlug);
29016
- writeResourceFileTree(path36.join(resourcesDir, resSlug), resource.files || [], orgDir);
30934
+ writeResourceFileTree(path38.join(resourcesDir, resSlug), resource.files || [], orgDir);
29017
30935
  }
29018
- if (fs26.existsSync(resourcesDir)) {
29019
- for (const entry of fs26.readdirSync(resourcesDir, { withFileTypes: true })) {
30936
+ if (fs27.existsSync(resourcesDir)) {
30937
+ for (const entry of fs27.readdirSync(resourcesDir, { withFileTypes: true })) {
29020
30938
  if (entry.isDirectory() && !currentSlugs.has(entry.name)) {
29021
- fs26.rmSync(path36.join(resourcesDir, entry.name), { recursive: true, force: true });
30939
+ fs27.rmSync(path38.join(resourcesDir, entry.name), { recursive: true, force: true });
29022
30940
  }
29023
30941
  }
29024
30942
  }
@@ -29027,7 +30945,7 @@ async function downloadOrgBinaryFiles(orgDir, payload) {
29027
30945
  let count = 0;
29028
30946
  for (const resource of payload.resources ?? []) {
29029
30947
  if (!resource.files) continue;
29030
- const resDir = path36.join(orgDir, "resources", slugify2(resource.name));
30948
+ const resDir = path38.join(orgDir, "resources", slugify2(resource.name));
29031
30949
  count += await downloadBinaryFiles(resDir, resource.files, orgDir);
29032
30950
  }
29033
30951
  if (count > 0) console.log(`Downloaded ${count} binary resource file(s).`);
@@ -29650,8 +31568,8 @@ var init_report_edit_args = __esm({
29650
31568
  });
29651
31569
 
29652
31570
  // src/lib/file-map.ts
29653
- import * as fs27 from "fs";
29654
- import * as path37 from "path";
31571
+ import * as fs28 from "fs";
31572
+ import * as path39 from "path";
29655
31573
  function isSafeRelPath(rel) {
29656
31574
  if (rel.length === 0 || rel.length > 300) return false;
29657
31575
  if (rel.startsWith("/") || rel.includes("\\")) return false;
@@ -29666,9 +31584,9 @@ function writeFileMap(targetDir, files) {
29666
31584
  if (!isSafeRelPath(rel)) {
29667
31585
  throw new Error(`Refusing to write unsafe path: ${rel}`);
29668
31586
  }
29669
- const abs = path37.join(targetDir, rel);
29670
- fs27.mkdirSync(path37.dirname(abs), { recursive: true });
29671
- fs27.writeFileSync(abs, body, "utf-8");
31587
+ const abs = path39.join(targetDir, rel);
31588
+ fs28.mkdirSync(path39.dirname(abs), { recursive: true });
31589
+ fs28.writeFileSync(abs, body, "utf-8");
29672
31590
  written.push(rel);
29673
31591
  }
29674
31592
  return written;
@@ -29684,8 +31602,8 @@ var admin_exports = {};
29684
31602
  __export(admin_exports, {
29685
31603
  adminCommand: () => adminCommand
29686
31604
  });
29687
- import * as fs28 from "fs";
29688
- import * as path38 from "path";
31605
+ import * as fs29 from "fs";
31606
+ import * as path40 from "path";
29689
31607
  async function adminCommand(args2) {
29690
31608
  const [group, ...afterGroup] = args2;
29691
31609
  if (!group) {
@@ -30124,7 +32042,7 @@ async function runArchiveRead(positional, flagArgs) {
30124
32042
  exitOnApiError(err);
30125
32043
  throw err;
30126
32044
  }
30127
- fs28.writeFileSync(outPath, zip);
32045
+ fs29.writeFileSync(outPath, zip);
30128
32046
  console.log(`Wrote ${zip.byteLength} bytes to ${outPath}`);
30129
32047
  return;
30130
32048
  }
@@ -30266,13 +32184,13 @@ async function runSkillInstall(positional) {
30266
32184
  throw err;
30267
32185
  }
30268
32186
  const root = findGitRoot() ?? process.cwd();
30269
- const present = HARNESS_SKILL_DIRS.filter((dir) => fs28.existsSync(path38.join(root, dir)));
32187
+ const present = HARNESS_SKILL_DIRS.filter((dir) => fs29.existsSync(path40.join(root, dir)));
30270
32188
  const targets = present.length > 0 ? present : HARNESS_SKILL_DIRS;
30271
32189
  const fileCount = Object.keys(res.files).length;
30272
32190
  const relDirs = targets.map((harness) => {
30273
- const targetDir = path38.join(root, harness, "skills", name);
32191
+ const targetDir = path40.join(root, harness, "skills", name);
30274
32192
  writeFileMap(targetDir, res.files);
30275
- return `${path38.relative(root, targetDir)}/`;
32193
+ return `${path40.relative(root, targetDir)}/`;
30276
32194
  });
30277
32195
  console.log(`Installed skill "${name}" (${fileCount} file${fileCount === 1 ? "" : "s"}) \u2192 ${relDirs.join(", ")}`);
30278
32196
  console.log("Reload your agent (e.g. restart Claude Code) to pick up the skill.");
@@ -32865,12 +34783,12 @@ var init_providers = __esm({
32865
34783
  // src/data/commands/report.ts
32866
34784
  import { Command as Command6 } from "commander";
32867
34785
  import { readFileSync as readFileSync20 } from "fs";
32868
- import { dirname as dirname16, join as join35 } from "path";
34786
+ import { dirname as dirname15, join as join36 } from "path";
32869
34787
  import { fileURLToPath as fileURLToPath2 } from "url";
32870
34788
  function resolveCliVersion() {
32871
34789
  for (const candidate of [
32872
- join35(here, "..", "package.json"),
32873
- join35(here, "..", "..", "..", "package.json")
34790
+ join36(here, "..", "package.json"),
34791
+ join36(here, "..", "..", "..", "package.json")
32874
34792
  ]) {
32875
34793
  try {
32876
34794
  const version = JSON.parse(readFileSync20(candidate, "utf-8")).version;
@@ -33069,7 +34987,7 @@ var init_report3 = __esm({
33069
34987
  init_terminal_output();
33070
34988
  init_workspace();
33071
34989
  init_skill_version();
33072
- here = dirname16(fileURLToPath2(import.meta.url));
34990
+ here = dirname15(fileURLToPath2(import.meta.url));
33073
34991
  }
33074
34992
  });
33075
34993
 
@@ -33382,16 +35300,16 @@ MCP URL: ${toolsetMcpUrl(slug)}`);
33382
35300
  ).option("--force", "Revoke even if the token appears to be in live use").action(async function(tokenId, opts) {
33383
35301
  const client = await createDataClient();
33384
35302
  const id = pathSegment(tokenId, "token id");
33385
- const path39 = (force) => `/v1/tokens/${id}${force ? "?force=true" : ""}`;
35303
+ const path41 = (force) => `/v1/tokens/${id}${force ? "?force=true" : ""}`;
33386
35304
  const format = outputFormat(this);
33387
35305
  const revoked = (forced) => printOutput({ token_id: tokenId, revoked: true, forced }, format);
33388
35306
  if (opts.force) {
33389
- await client.request("DELETE", path39(true));
35307
+ await client.request("DELETE", path41(true));
33390
35308
  revoked(true);
33391
35309
  return;
33392
35310
  }
33393
35311
  try {
33394
- await client.request("DELETE", path39(false));
35312
+ await client.request("DELETE", path41(false));
33395
35313
  revoked(false);
33396
35314
  } catch (err) {
33397
35315
  if (!(err instanceof ApiError) || err.status !== 409 || dataErrorDetails(err)?.requires_force !== true) {
@@ -33406,7 +35324,7 @@ MCP URL: ${toolsetMcpUrl(slug)}`);
33406
35324
  console.error("Aborted.");
33407
35325
  return process.exit(1);
33408
35326
  }
33409
- await client.request("DELETE", path39(true));
35327
+ await client.request("DELETE", path41(true));
33410
35328
  revoked(true);
33411
35329
  }
33412
35330
  });
@@ -33554,9 +35472,9 @@ var init_base_tags = __esm({
33554
35472
 
33555
35473
  // src/data/commands/bases.ts
33556
35474
  import { Command as Command10 } from "commander";
33557
- function pageOf(path39, cursor) {
33558
- if (!cursor) return path39;
33559
- return `${path39}${path39.includes("?") ? "&" : "?"}cursor=${encodeURIComponent(cursor)}`;
35475
+ function pageOf(path41, cursor) {
35476
+ if (!cursor) return path41;
35477
+ return `${path41}${path41.includes("?") ? "&" : "?"}cursor=${encodeURIComponent(cursor)}`;
33560
35478
  }
33561
35479
  function parseEnum(flag, value, allowed) {
33562
35480
  if (value === void 0) return void 0;
@@ -33713,9 +35631,9 @@ function buildBasesCommand() {
33713
35631
  });
33714
35632
  bases.command("list-previews <origin-id>").description("List preview bases cloned from a base (production or preview)").action(async function(originId) {
33715
35633
  const client = await createDataClient();
33716
- const path39 = `/v1/${pathSegment(originId, "origin base id")}/previews`;
35634
+ const path41 = `/v1/${pathSegment(originId, "origin base id")}/previews`;
33717
35635
  printOutput(
33718
- await client.collectPages((cursor) => pageOf(path39, cursor)),
35636
+ await client.collectPages((cursor) => pageOf(path41, cursor)),
33719
35637
  outputFormat(this)
33720
35638
  );
33721
35639
  });
@@ -33762,9 +35680,9 @@ function buildBasesCommand() {
33762
35680
  });
33763
35681
  bases.command("promotions <production-id>").description("List promotion history for a production base").action(async function(productionId) {
33764
35682
  const client = await createDataClient();
33765
- const path39 = `/v1/${pathSegment(productionId, "production base id")}/promotions`;
35683
+ const path41 = `/v1/${pathSegment(productionId, "production base id")}/promotions`;
33766
35684
  printOutput(
33767
- await client.collectPages((cursor) => pageOf(path39, cursor)),
35685
+ await client.collectPages((cursor) => pageOf(path41, cursor)),
33768
35686
  outputFormat(this)
33769
35687
  );
33770
35688
  });
@@ -33903,7 +35821,7 @@ var init_file_types = __esm({
33903
35821
  // src/data/commands/files.ts
33904
35822
  import { Command as Command12 } from "commander";
33905
35823
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
33906
- import { basename as basename23 } from "path";
35824
+ import { basename as basename25 } from "path";
33907
35825
  function renderFileDiff(fileType, filePath, from, to, d) {
33908
35826
  console.log(sanitizeTerminalText(`${fileType}/${filePath}: v${from} \u2192 v${to}`));
33909
35827
  const md = d.metadata_delta;
@@ -33935,7 +35853,7 @@ function renderFileDiff(fileType, filePath, from, to, d) {
33935
35853
  }
33936
35854
  function downloadTarget(remotePath, to) {
33937
35855
  if (to) return to;
33938
- const derived = basename23(remotePath);
35856
+ const derived = basename25(remotePath);
33939
35857
  if (derived === "" || derived === "." || derived === "..") {
33940
35858
  throw expected(
33941
35859
  `Cannot derive a local filename from "${remotePath}" \u2014 pass --to <local> to name it.`
@@ -34311,8 +36229,8 @@ function buildRecordsCommand() {
34311
36229
  const body = { data: parseData(opts.data) };
34312
36230
  if (opts.externalId) body.external_id = opts.externalId;
34313
36231
  if (opts.externalSource) body.external_source = opts.externalSource;
34314
- const path39 = opts.id ? `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${pathSegment(opts.id, "--id")}` : `/v1/${base}/records/${pathSegment(recordType2, "record_type")}`;
34315
- printOutput(await client.request("PUT", path39, body), outputFormat(this));
36232
+ const path41 = opts.id ? `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${pathSegment(opts.id, "--id")}` : `/v1/${base}/records/${pathSegment(recordType2, "record_type")}`;
36233
+ printOutput(await client.request("PUT", path41, body), outputFormat(this));
34316
36234
  });
34317
36235
  records.command("query <record_type>").description("List/search records: exact filters + fuzzy `search`, sorting, pagination").option(
34318
36236
  "--external-source <source>",
@@ -34347,9 +36265,9 @@ function buildRecordsCommand() {
34347
36265
  ).action(async function(recordType2, id, opts) {
34348
36266
  const base = pathSegment(requireBase(this), "--base");
34349
36267
  const client = await createDataClient();
34350
- let path39 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
34351
- if (opts.externalSource) path39 += `?external_source=${encodeURIComponent(opts.externalSource)}`;
34352
- printOutput(await client.request("GET", path39), outputFormat(this));
36268
+ let path41 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
36269
+ if (opts.externalSource) path41 += `?external_source=${encodeURIComponent(opts.externalSource)}`;
36270
+ printOutput(await client.request("GET", path41), outputFormat(this));
34353
36271
  });
34354
36272
  records.command("delete <record_type> <id>").description(
34355
36273
  "Delete a record by internal ID, or by external_id (pass --external-source to scope it)"
@@ -34363,11 +36281,11 @@ function buildRecordsCommand() {
34363
36281
  }
34364
36282
  const base = pathSegment(requireBase(this), "--base");
34365
36283
  const client = await createDataClient();
34366
- let path39 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
36284
+ let path41 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
34367
36285
  if (opts.externalSource) {
34368
- path39 += `?external_source=${encodeURIComponent(opts.externalSource)}&external_id=${encodeURIComponent(id)}`;
36286
+ path41 += `?external_source=${encodeURIComponent(opts.externalSource)}&external_id=${encodeURIComponent(id)}`;
34369
36287
  }
34370
- await client.request("DELETE", path39);
36288
+ await client.request("DELETE", path41);
34371
36289
  console.log("Deleted");
34372
36290
  });
34373
36291
  records.command("history <id>").description(
@@ -34532,8 +36450,8 @@ function buildRelationshipsCommand() {
34532
36450
  if (opts.data) body.data = parseData(opts.data);
34533
36451
  const base = pathSegment(requireBase(this), "--base");
34534
36452
  const client = await createDataClient();
34535
- const path39 = opts.id ? `/v1/${base}/relationships/${pathSegment(opts.id, "--id")}` : `/v1/${base}/relationships`;
34536
- printOutput(await client.request("PUT", path39, body), outputFormat(this));
36453
+ const path41 = opts.id ? `/v1/${base}/relationships/${pathSegment(opts.id, "--id")}` : `/v1/${base}/relationships`;
36454
+ printOutput(await client.request("PUT", path41, body), outputFormat(this));
34537
36455
  });
34538
36456
  relationships.command("get <id>").description(
34539
36457
  "Get a relationship by ID (or by its external key: pass the external_id with --rel-type)"
@@ -34766,8 +36684,8 @@ function buildToolsetsCommand() {
34766
36684
  toolsets.command("get <slug>").description("Get a toolset").option("--resolved", "Include resolved record_type schemas").action(async function(slug, opts) {
34767
36685
  const base = pathSegment(requireBase(this), "--base");
34768
36686
  const client = await createDataClient();
34769
- const path39 = opts.resolved ? `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}/resolved` : `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}`;
34770
- printOutput(await client.request("GET", path39), outputFormat(this));
36687
+ const path41 = opts.resolved ? `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}/resolved` : `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}`;
36688
+ printOutput(await client.request("GET", path41), outputFormat(this));
34771
36689
  });
34772
36690
  toolsets.command("list").description("List all toolsets").action(async function() {
34773
36691
  const base = pathSegment(requireBase(this), "--base");
@@ -35002,7 +36920,7 @@ init_utils();
35002
36920
  init_registry();
35003
36921
  import { readFileSync as readFileSync24 } from "fs";
35004
36922
  import { fileURLToPath as fileURLToPath3 } from "url";
35005
- import { dirname as dirname17, join as join36 } from "path";
36923
+ import { dirname as dirname16, join as join37 } from "path";
35006
36924
 
35007
36925
  // src/lib/version-refresh.ts
35008
36926
  init_version_cache();
@@ -35157,8 +37075,8 @@ Run \`wayai admin skill install\` to update.`);
35157
37075
  }
35158
37076
 
35159
37077
  // src/index.ts
35160
- var __dirname = dirname17(fileURLToPath3(import.meta.url));
35161
- var pkg = JSON.parse(readFileSync24(join36(__dirname, "..", "package.json"), "utf-8"));
37078
+ var __dirname = dirname16(fileURLToPath3(import.meta.url));
37079
+ var pkg = JSON.parse(readFileSync24(join37(__dirname, "..", "package.json"), "utf-8"));
35162
37080
  var [, , command, ...args] = process.argv;
35163
37081
  var isBackgroundRefresh = command === REFRESH_COMMAND;
35164
37082
  if (!isBackgroundRefresh) initSentry(command, pkg.version);