@wayai/cli 0.3.203 → 0.3.204

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-5RGL5RXH.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_5RGL5RXH = __esm({
9794
+ "../../packages/core/dist/chunk-5RGL5RXH.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_5RGL5RXH();
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_5RGL5RXH();
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,9 +21577,45 @@ ${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
20993
21584
  };
20994
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);
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;
21618
+ }
20995
21619
  function readRooms(raw, problems2) {
20996
21620
  if (raw === void 0 || raw === null) return [];
20997
21621
  if (!Array.isArray(raw)) {
@@ -21062,31 +21686,30 @@ 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) {
21700
+ function writeCommunityFiles(root, folder, files) {
21075
21701
  const written = [];
21076
21702
  for (const [name, content] of [
21077
21703
  [COMMUNITY_FILE, renderCommunityYaml(files)],
21078
21704
  [RULES_FILE, renderRules(files.rules)]
21079
21705
  ]) {
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");
21085
- }
21086
- written.push(name);
21706
+ if (writeFileIfChangedNoFollow(root, path25.join(folder, name), content)) written.push(name);
21087
21707
  }
21088
21708
  return written;
21089
21709
  }
21710
+ function writeCommunityFolder(root, folder, files) {
21711
+ return [...writeCommunityFiles(root, folder, files), ...files.courses.flatMap((course) => writeCourseFolder(root, folder, course))];
21712
+ }
21090
21713
  var TOP_KEYS, SETTINGS_KEYS, ROOM_KEYS;
21091
21714
  var init_files = __esm({
21092
21715
  "src/communities/files.ts"() {
@@ -21099,20 +21722,1365 @@ var init_files = __esm({
21099
21722
  init_community_workspace();
21100
21723
  init_utils();
21101
21724
  init_model();
21102
- TOP_KEYS = ["community_id", "name", "settings", "join_link_enabled", "join_link_rev", "rooms"];
21725
+ init_course_files();
21726
+ TOP_KEYS = ["community_id", "name", "settings", "join_link_enabled", "join_link_rev", "rooms", "courses", "synced"];
21103
21727
  SETTINGS_KEYS = ["rev", "slow_mode_seconds", "courses_label"];
21104
21728
  ROOM_KEYS = ["key", "id", "rev", "name", "kind", "archived"];
21105
21729
  }
21106
21730
  });
21107
21731
 
21732
+ // src/communities/listing.ts
21733
+ function lineDiff(from, to) {
21734
+ const a = from ? sanitizeTerminalText(from).split("\n") : [];
21735
+ const b = to ? sanitizeTerminalText(to).split("\n") : [];
21736
+ if (a.length * b.length > 25e4) return [...a.map((l) => `- ${l}`), ...b.map((l) => `+ ${l}`)];
21737
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
21738
+ for (let i2 = a.length - 1; i2 >= 0; i2--) {
21739
+ for (let j2 = b.length - 1; j2 >= 0; j2--) {
21740
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
21741
+ }
21742
+ }
21743
+ const out = [];
21744
+ let i = 0;
21745
+ let j = 0;
21746
+ while (i < a.length || j < b.length) {
21747
+ if (i < a.length && j < b.length && a[i] === b[j]) {
21748
+ i++;
21749
+ j++;
21750
+ } else if (j >= b.length || i < a.length && lcs[i + 1][j] >= lcs[i][j + 1]) {
21751
+ out.push(`- ${a[i++]}`);
21752
+ } else {
21753
+ out.push(`+ ${b[j++]}`);
21754
+ }
21755
+ }
21756
+ return out;
21757
+ }
21758
+ function printSection(title, lines) {
21759
+ if (lines.length === 0) return;
21760
+ console.log(`
21761
+ ${title}`);
21762
+ for (const line of lines) console.log(` ${line}`);
21763
+ }
21764
+ function printListing(listing) {
21765
+ printSection("Changes:", listing.changes);
21766
+ printSection("Changed on the server since your last pull \u2014 left as is; pull to take it:", listing.serverChanged);
21767
+ printSection("Kept \u2014 edited in your files, not pushed yet:", listing.kept);
21768
+ printSection("Not in your files \u2014 left as is:", listing.leftAsIs);
21769
+ printSection("Conflicts \u2014 changed in your files and on the server since your last pull:", listing.conflicts);
21770
+ }
21771
+ var show, emptyListing;
21772
+ var init_listing = __esm({
21773
+ "src/communities/listing.ts"() {
21774
+ "use strict";
21775
+ init_terminal_output();
21776
+ show = (text) => JSON.stringify(sanitizeTerminalText(text));
21777
+ emptyListing = () => ({ changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] });
21778
+ }
21779
+ });
21780
+
21781
+ // src/communities/order.ts
21782
+ function idsOf(entries) {
21783
+ return new Set(entries.flatMap((entry) => entry.id === void 0 ? [] : [entry.id]));
21784
+ }
21785
+ function restrict(order, ids) {
21786
+ const keep = new Set(ids);
21787
+ return order.filter((id) => keep.has(id));
21788
+ }
21789
+ function mergeOrder(listed2, server, named) {
21790
+ const listedIds2 = idsOf(listed2);
21791
+ const first = [];
21792
+ const after = /* @__PURE__ */ new Map();
21793
+ let anchor = null;
21794
+ for (const id of server) {
21795
+ if (listedIds2.has(id)) {
21796
+ anchor = id;
21797
+ } else if (!named.has(id)) {
21798
+ if (anchor === null) first.push(id);
21799
+ else after.set(anchor, [...after.get(anchor) ?? [], id]);
21800
+ }
21801
+ }
21802
+ const slots = first.map((id) => ({ untracked: id }));
21803
+ for (const child of listed2) {
21804
+ slots.push({ entry: child.entry });
21805
+ if (child.id !== void 0) for (const id of after.get(child.id) ?? []) slots.push({ untracked: id });
21806
+ }
21807
+ return slots;
21808
+ }
21809
+ function filesOrder(listed2, server, named) {
21810
+ const byEntry = new Map(listed2.map((child) => [child.entry, child.id]));
21811
+ return mergeOrder(listed2, server, named).flatMap((slot) => {
21812
+ const id = "untracked" in slot ? slot.untracked : byEntry.get(slot.entry);
21813
+ return id === void 0 ? [] : [id];
21814
+ });
21815
+ }
21816
+ function placeAt(list, id, position) {
21817
+ const at = list.indexOf(id);
21818
+ if (at >= 0) list.splice(at, 1);
21819
+ list.splice(Math.min(position, list.length), 0, id);
21820
+ }
21821
+ function sameOrder(a, b) {
21822
+ return a.length === b.length && a.every((id, i) => id === b[i]);
21823
+ }
21824
+ function reorderMoves(current, target) {
21825
+ const list = [...current];
21826
+ const present = target.filter((id) => list.includes(id));
21827
+ const stay = longestRising(present, present.map((id) => list.indexOf(id)));
21828
+ const moves = [];
21829
+ target.forEach((id, i) => {
21830
+ if (stay.has(id)) return;
21831
+ const others = list.filter((other) => other !== id);
21832
+ const position = i === 0 ? 0 : others.indexOf(target[i - 1]) + 1;
21833
+ moves.push({ id, position });
21834
+ placeAt(list, id, position);
21835
+ });
21836
+ const placed = list.filter((id) => target.includes(id));
21837
+ if (!sameOrder(placed, target)) {
21838
+ throw new Error(`reorderMoves: ${JSON.stringify(current)} \u2192 ${JSON.stringify(target)} left ${JSON.stringify(placed)}`);
21839
+ }
21840
+ return moves;
21841
+ }
21842
+ function longestRising(ids, positions) {
21843
+ const length = positions.map(() => 1);
21844
+ const previous = positions.map(() => -1);
21845
+ let best = -1;
21846
+ positions.forEach((position, i) => {
21847
+ for (let j = 0; j < i; j++) {
21848
+ if (positions[j] < position && length[j] + 1 > length[i]) {
21849
+ length[i] = length[j] + 1;
21850
+ previous[i] = j;
21851
+ }
21852
+ }
21853
+ if (best === -1 || length[i] > length[best]) best = i;
21854
+ });
21855
+ const stay = /* @__PURE__ */ new Set();
21856
+ for (let i = best; i >= 0; i = previous[i]) stay.add(ids[i]);
21857
+ return stay;
21858
+ }
21859
+ var init_order = __esm({
21860
+ "src/communities/order.ts"() {
21861
+ "use strict";
21862
+ }
21863
+ });
21864
+
21865
+ // src/communities/courses.ts
21866
+ import { isDeepStrictEqual } from "util";
21867
+ import * as path26 from "path";
21868
+ function roomIdOf(rooms, key) {
21869
+ if (key === void 0) return null;
21870
+ const room = rooms.find((entry) => entry.key === key);
21871
+ return room?.id ?? `${PENDING_ROOM}${key}`;
21872
+ }
21873
+ function fileLesson(rooms, lesson) {
21874
+ return lessonContent({
21875
+ title: lesson.title,
21876
+ body: lesson.body,
21877
+ video: lesson.video_url === void 0 ? null : parseCommunityLessonVideoUrl(lesson.video_url),
21878
+ links: lesson.links,
21879
+ room_id: roomIdOf(rooms, lesson.room),
21880
+ requires_paid_plan: lesson.requires_paid_plan,
21881
+ published: lesson.published
21882
+ });
21883
+ }
21884
+ function roomName(rooms, id) {
21885
+ if (id === null) return "none";
21886
+ if (id.startsWith(PENDING_ROOM)) return id.slice(PENDING_ROOM.length);
21887
+ return rooms.find((room) => room.id === id)?.key ?? id;
21888
+ }
21889
+ function roomKeyOf(rooms, roomId) {
21890
+ if (roomId === null) return void 0;
21891
+ const room = rooms.find((entry) => entry.id === roomId);
21892
+ if (!room) throw expected(`room ${roomId} is not one of the rooms ${COMMUNITY_FILE} lists: pull the community first.`);
21893
+ return room.key;
21894
+ }
21895
+ function takeLesson(rooms, lesson, server, fields) {
21896
+ const file = fileLesson(rooms, lesson);
21897
+ const side = lessonContent(server);
21898
+ const take = (field, differs) => differs && (fields === void 0 || fields.has(field));
21899
+ if (take("title", file.title !== side.title)) lesson.title = server.title;
21900
+ if (take("body", file.body !== side.body)) lesson.body = side.body;
21901
+ if (take("video_url", !isDeepStrictEqual(file.video, side.video))) lesson.video_url = side.video ? communityLessonVideoWatchUrl(side.video) : void 0;
21902
+ if (take("links", !isDeepStrictEqual(file.links, side.links))) lesson.links = server.links.map((link) => ({ title: link.title, url: link.url }));
21903
+ if (take("room_id", file.room_id !== side.room_id)) lesson.room = roomKeyOf(rooms, side.room_id);
21904
+ if (take("requires_paid_plan", file.requires_paid_plan !== side.requires_paid_plan)) lesson.requires_paid_plan = side.requires_paid_plan;
21905
+ if (take("published", file.published !== side.published)) lesson.published = side.published;
21906
+ }
21907
+ function takeCourse(folder, server, fields) {
21908
+ const file = courseContent(folder);
21909
+ const side = courseContent(server);
21910
+ const take = (field, differs) => differs && (fields === void 0 || fields.has(field));
21911
+ if (take("title", file.title !== side.title)) folder.title = server.title;
21912
+ if (take("description", file.description !== side.description)) folder.description = server.description;
21913
+ if (take("published", file.published !== side.published)) folder.published = side.published;
21914
+ }
21915
+ function orderVerdict(list, base, server) {
21916
+ const onServer = new Set(server);
21917
+ if (base === void 0) {
21918
+ const shared2 = list.filter((id) => onServer.has(id));
21919
+ return sameOrder(shared2, restrict(server, shared2)) ? "converged" : "conflict";
21920
+ }
21921
+ const inBase = new Set(base);
21922
+ const shared = list.filter((id) => inBase.has(id) && onServer.has(id));
21923
+ const added = list.filter((id) => !inBase.has(id) && onServer.has(id));
21924
+ if (added.length > 0 && !sameGaps(added, shared, list, server)) return "conflict";
21925
+ const was = restrict(base, shared);
21926
+ const now = restrict(server, shared);
21927
+ return threeWay(!sameOrder(shared, was), !sameOrder(now, was), sameOrder(shared, now));
21928
+ }
21929
+ function sameGaps(added, shared, a, b) {
21930
+ const before = (order, id) => new Set(order.slice(0, order.indexOf(id)).filter((other) => shared.includes(other)));
21931
+ const same = (x, y) => x.size === y.size && [...x].every((id) => y.has(id));
21932
+ return sameOrder(restrict(a, added), restrict(b, added)) && added.every((id) => same(before(a, id), before(b, id)));
21933
+ }
21934
+ function placementVerdict(files, base, server) {
21935
+ if (base === void 0) return files === server ? "converged" : "conflict";
21936
+ return threeWay(files !== base, server !== base, files === server);
21937
+ }
21938
+ function planOrder(entries, server, base) {
21939
+ return { state: orderVerdict([...idsOf(entries)], base, server), server: [...server], base: base && [...base] };
21940
+ }
21941
+ function baseModule(synced2, lessonId) {
21942
+ const holding = Object.entries(synced2?.lessons ?? {}).filter(([, ids]) => ids.includes(lessonId));
21943
+ return holding.length === 1 ? holding[0][0] : void 0;
21944
+ }
21945
+ function courseName(files, server, courseId) {
21946
+ const folder = files.courses.find((course) => course.id === courseId);
21947
+ return folder ? `course ${folder.slug}` : `course ${show(server.course.get(courseId)?.course.title ?? courseId)}`;
21948
+ }
21949
+ function planCourses(files, server) {
21950
+ const problems2 = [];
21951
+ const named = idsOf(files.courses);
21952
+ return {
21953
+ server,
21954
+ order: planOrder(files.courses, server.ids, files.synced_courses),
21955
+ courses: files.courses.map((folder) => planCourse(files, folder, server, problems2)),
21956
+ untracked: server.courses.filter((course) => !named.has(course.course.course_id)),
21957
+ problems: problems2
21958
+ };
21959
+ }
21960
+ function planCourse(files, folder, server, problems2) {
21961
+ const found = folder.id === void 0 ? void 0 : server.course.get(folder.id);
21962
+ if (folder.id !== void 0 && !found) problems2.push(`course ${folder.slug}: ${folder.id} is not a course of this community`);
21963
+ const courseId = found?.course.course_id;
21964
+ const plan = {
21965
+ folder,
21966
+ server: null,
21967
+ export: found ?? null,
21968
+ modulesOrder: null,
21969
+ modules: [],
21970
+ lessons: [],
21971
+ untrackedModules: [],
21972
+ untrackedLessons: []
21973
+ };
21974
+ const moduleIds = idsOf(folder.modules);
21975
+ const lessonIds = idsOf(folder.modules.flatMap((module) => module.lessons));
21976
+ if (found) {
21977
+ const side = serverCourse(found.course);
21978
+ plan.server = { state: classify(folder.rev, courseRev(found.course.course_id, courseContent(folder)), side.rev), server: found.course, serverRev: side.rev };
21979
+ plan.modulesOrder = planOrder(folder.modules, found.modules.map((m) => m.module_id), folder.synced?.modules);
21980
+ plan.untrackedModules = found.modules.filter((module) => !moduleIds.has(module.module_id));
21981
+ plan.untrackedLessons = found.modules.flatMap((module) => module.lessons).filter((lesson) => !lessonIds.has(lesson.lesson_id));
21982
+ }
21983
+ const elsewhere = (what, at, id, owner) => {
21984
+ if (owner === void 0) problems2.push(`${what} ${at}: ${id} is not a ${what} of this community`);
21985
+ 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`);
21986
+ };
21987
+ for (const entry of folder.modules) {
21988
+ const at = `${folder.slug}/${entry.key}`;
21989
+ const module = entry.id === void 0 ? void 0 : server.module.get(entry.id);
21990
+ if (entry.id !== void 0 && module?.courseId !== courseId) elsewhere("module", at, entry.id, module?.courseId);
21991
+ const mine = module && module.courseId === courseId ? module.module : void 0;
21992
+ const modulePlan = { course: folder, entry, server: null, lessonsOrder: null };
21993
+ if (mine) {
21994
+ const side = serverModule(mine);
21995
+ modulePlan.server = { state: classify(entry.rev, moduleRev(mine.module_id, moduleContent(entry)), side.rev), server: mine, serverRev: side.rev };
21996
+ modulePlan.lessonsOrder = planOrder(entry.lessons, mine.lessons.map((l) => l.lesson_id), folder.synced?.lessons[mine.module_id]);
21997
+ }
21998
+ plan.modules.push(modulePlan);
21999
+ for (const lesson of entry.lessons) {
22000
+ const where = server.lesson.get(lesson.id ?? "");
22001
+ if (lesson.id !== void 0 && where?.courseId !== courseId) elsewhere("lesson", `${folder.slug}/${lesson.slug}`, lesson.id, where?.courseId);
22002
+ const lessonPlan = { course: folder, module: entry, lesson, server: null, placement: null };
22003
+ if (where && where.courseId === courseId) {
22004
+ const side = serverLesson(where.lesson);
22005
+ lessonPlan.server = { state: classify(lesson.rev, lessonRev(where.lesson.lesson_id, fileLesson(files.rooms, lesson)), side.rev), server: where.lesson, serverRev: side.rev };
22006
+ const filesModule = plannedIds.module(folder, entry);
22007
+ const base = baseModule(folder.synced, where.lesson.lesson_id);
22008
+ lessonPlan.placement = { state: placementVerdict(filesModule, base, where.moduleId), base, files: filesModule, server: where.moduleId };
22009
+ }
22010
+ plan.lessons.push(lessonPlan);
22011
+ }
22012
+ }
22013
+ return plan;
22014
+ }
22015
+ function courseCapProblems(files, plan) {
22016
+ const problems2 = [];
22017
+ const title = (entity) => toLf(entity.title).trim();
22018
+ const courseCreates = plan.courses.filter((course) => course.folder.id === void 0).map((course) => title(course.folder));
22019
+ const courses = plan.server.courses.length;
22020
+ if (courses + certainlyNew(courseCreates, plan.untracked.map((course) => title(course.course))) > COMMUNITY_COURSES_MAX) {
22021
+ problems2.push(`courses: a community holds at most ${COMMUNITY_COURSES_MAX} courses, drafts included; it has ${courses}, and the files create ${courseCreates.length}`);
22022
+ }
22023
+ const named = idsOf(files.courses.flatMap((course) => course.modules.flatMap((module) => module.lessons)));
22024
+ const lessonCreates = [];
22025
+ for (const course of plan.courses) {
22026
+ const moduleCreates = course.modules.filter((module) => module.entry.id === void 0).map((module) => title(module.entry));
22027
+ const creates = course.lessons.filter((lesson) => lesson.lesson.id === void 0).map((lesson) => title(lesson.lesson));
22028
+ lessonCreates.push(...creates);
22029
+ const modules = course.export?.modules.length ?? 0;
22030
+ const lessons2 = course.export?.modules.reduce((n, module) => n + module.lessons.length, 0) ?? 0;
22031
+ if (modules + certainlyNew(moduleCreates, course.untrackedModules.map(title)) > COMMUNITY_COURSE_MODULES_MAX) {
22032
+ 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}`);
22033
+ }
22034
+ if (lessons2 + certainlyNew(creates, course.untrackedLessons.map(title)) > COMMUNITY_COURSE_LESSONS_MAX) {
22035
+ 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}`);
22036
+ }
22037
+ }
22038
+ const lessons = plan.server.lesson.size;
22039
+ const untracked = [...plan.server.lesson.values()].filter(({ lesson }) => !named.has(lesson.lesson_id)).map(({ lesson }) => title(lesson));
22040
+ if (lessons + certainlyNew(lessonCreates, untracked) > COMMUNITY_LESSONS_MAX) {
22041
+ problems2.push(`lessons: a community holds at most ${COMMUNITY_LESSONS_MAX} lessons, drafts included; it has ${lessons}, and the files create ${lessonCreates.length}`);
22042
+ }
22043
+ return problems2;
22044
+ }
22045
+ function certainlyNew(creates, untracked) {
22046
+ const left = [...untracked];
22047
+ return creates.filter((title) => {
22048
+ const at = left.indexOf(title);
22049
+ if (at >= 0) left.splice(at, 1);
22050
+ return at < 0;
22051
+ }).length;
22052
+ }
22053
+ function append(order, id) {
22054
+ order.server.push(id);
22055
+ order.pinned?.push(id);
22056
+ order.touched = true;
22057
+ }
22058
+ function insertAfter(list, item, order) {
22059
+ let at = 0;
22060
+ for (let i = order.indexOf(item) - 1; i >= 0; i--) {
22061
+ const k = list.indexOf(order[i]);
22062
+ if (k >= 0) {
22063
+ at = k + 1;
22064
+ break;
22065
+ }
22066
+ }
22067
+ list.splice(at, 0, item);
22068
+ }
22069
+ function shift(order, id, position) {
22070
+ placeAt(order.server, id, position);
22071
+ order.touched = true;
22072
+ }
22073
+ function leave(order, id) {
22074
+ for (const list of [order.server, order.pinned ?? []]) {
22075
+ const at = list.indexOf(id);
22076
+ if (at >= 0) list.splice(at, 1);
22077
+ }
22078
+ order.touched = true;
22079
+ }
22080
+ function parents(files, sim, ids) {
22081
+ const out = [];
22082
+ const listedOf = (entries, id) => [...idsOf(listedIds(entries, id))];
22083
+ out.push({ what: "courses order", kind: "courses", id: files.community_id, order: sim.courses, courseId: null, listed: listedOf(files.courses, ids.course) });
22084
+ for (const course of files.courses) {
22085
+ const courseId = ids.course(course);
22086
+ const order = courseId === void 0 ? void 0 : sim.modules.get(courseId);
22087
+ if (courseId === void 0 || !order) continue;
22088
+ out.push({
22089
+ what: `modules order of course ${course.slug}`,
22090
+ kind: "modules",
22091
+ id: courseId,
22092
+ order,
22093
+ courseId,
22094
+ listed: listedOf(course.modules, (module) => ids.module(course, module))
22095
+ });
22096
+ }
22097
+ for (const course of files.courses) {
22098
+ const courseId = ids.course(course);
22099
+ if (courseId === void 0 || !sim.modules.has(courseId)) continue;
22100
+ for (const module of course.modules) {
22101
+ const moduleId = ids.module(course, module);
22102
+ const order = moduleId === void 0 ? void 0 : sim.lessons.get(moduleId);
22103
+ if (moduleId === void 0 || !order) continue;
22104
+ out.push({
22105
+ what: `lessons order of module ${course.slug}/${module.key}`,
22106
+ kind: "lessons",
22107
+ id: moduleId,
22108
+ order,
22109
+ courseId,
22110
+ listed: listedOf(module.lessons, (lesson) => ids.lesson(course, lesson))
22111
+ });
22112
+ }
22113
+ }
22114
+ return out;
22115
+ }
22116
+ function listedIds(entries, id) {
22117
+ return entries.map((entry) => ({ entry, id: id(entry) }));
22118
+ }
22119
+ function computePlaces(files, sim, ids, placements) {
22120
+ const out = { moves: [], conflicts: [], reorders: [] };
22121
+ const placementOf = (id) => placements.get(id);
22122
+ for (const parent of parents(files, sim, ids)) {
22123
+ const { order } = parent;
22124
+ if (order.conflict) continue;
22125
+ const lessons = parent.kind === "lessons";
22126
+ const wanted = lessons ? parent.listed.filter((id) => placementOf(id)?.state !== "remote") : parent.listed;
22127
+ const leaving = lessons ? order.server.filter((id) => placementOf(id)?.state === "local" && !parent.listed.includes(id)) : [];
22128
+ const arriving = wanted.filter((id) => !order.server.includes(id));
22129
+ if (order.pinned) {
22130
+ const kept = order.pinned;
22131
+ const shared = wanted.filter((id) => kept.includes(id));
22132
+ if (arriving.length > 0 || !sameOrder(shared, restrict(kept, shared))) out.conflicts.push(parent.what);
22133
+ continue;
22134
+ }
22135
+ if (arriving.length === 0 && sameOrder(restrict(order.server, wanted), wanted)) continue;
22136
+ const target = filesOrder(wanted.map((id) => ({ entry: id, id })), order.server, /* @__PURE__ */ new Set([...wanted, ...leaving]));
22137
+ const from = [...order.server];
22138
+ for (const move of reorderMoves(order.server, target)) {
22139
+ if (lessons) {
22140
+ const left = sim.at.get(move.id);
22141
+ if (left === void 0) throw new Error(`computePlaces: lesson ${move.id} is in no module`);
22142
+ if (left !== parent.id) leave(sim.lessons.get(left), move.id);
22143
+ sim.at.set(move.id, parent.id);
22144
+ }
22145
+ shift(order, move.id, move.position);
22146
+ const { id, position } = move;
22147
+ out.moves.push(
22148
+ 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 }
22149
+ );
22150
+ }
22151
+ out.reorders.push({ what: parent.what, from, to: target });
22152
+ }
22153
+ return out;
22154
+ }
22155
+ function outcomeUnknown(err) {
22156
+ return err?.isExpected !== true;
22157
+ }
22158
+ function coursePatch(folder, now, publish) {
22159
+ const file = courseContent(folder);
22160
+ const server = courseContent(now);
22161
+ const body = {
22162
+ ...file.title !== server.title ? { title: folder.title } : {},
22163
+ ...file.description !== server.description ? { description: folder.description } : {},
22164
+ ...file.published !== server.published && (publish || !file.published) ? { published: file.published } : {}
22165
+ };
22166
+ return Object.keys(body).length > 0 ? body : null;
22167
+ }
22168
+ function lessonPatch(rooms, lesson, now, waitForRooms) {
22169
+ const file = fileLesson(rooms, lesson);
22170
+ const server = lessonContent(now);
22171
+ const pending = file.room_id?.startsWith(PENDING_ROOM) ?? false;
22172
+ if (pending && !waitForRooms) throw new Error(`lessonPatch: room ${lesson.room} has no id yet`);
22173
+ const body = {
22174
+ ...file.title !== server.title ? { title: lesson.title } : {},
22175
+ ...file.body !== server.body ? { body: lesson.body } : {},
22176
+ ...!isDeepStrictEqual(file.video, server.video) ? { video_url: lesson.video_url ?? null } : {},
22177
+ ...!isDeepStrictEqual(file.links, server.links) ? { links: lesson.links } : {},
22178
+ ...file.room_id !== server.room_id && !pending ? { room_id: file.room_id } : {},
22179
+ ...file.requires_paid_plan !== server.requires_paid_plan ? { requires_paid_plan: lesson.requires_paid_plan } : {},
22180
+ ...file.published !== server.published ? { published: lesson.published } : {}
22181
+ };
22182
+ return Object.keys(body).length > 0 ? body : null;
22183
+ }
22184
+ function courseChanges(from, to) {
22185
+ return [
22186
+ ...from.title !== to.title ? [`title ${show(from.title)} \u2192 ${show(to.title)}`] : [],
22187
+ ...from.description !== to.description ? ["description"] : [],
22188
+ ...from.published !== to.published ? [to.published ? "published" : "unpublished"] : []
22189
+ ];
22190
+ }
22191
+ function lessonChanges(from, to, room) {
22192
+ return [
22193
+ ...from.title !== to.title ? [`title ${show(from.title)} \u2192 ${show(to.title)}`] : [],
22194
+ ...from.body !== to.body ? ["text"] : [],
22195
+ ...!isDeepStrictEqual(from.video, to.video) ? [to.video ? from.video ? "video" : "video added" : "video removed"] : [],
22196
+ ...!isDeepStrictEqual(from.links, to.links) ? ["links"] : [],
22197
+ ...from.room_id !== to.room_id ? [`room ${room(from.room_id)} \u2192 ${room(to.room_id)}`] : [],
22198
+ ...from.requires_paid_plan !== to.requires_paid_plan ? [to.requires_paid_plan ? "needs a paid plan" : "no longer needs a paid plan"] : [],
22199
+ ...from.published !== to.published ? [to.published ? "published" : "unpublished"] : []
22200
+ ];
22201
+ }
22202
+ async function lessonWrite(course, lesson, write) {
22203
+ try {
22204
+ await write();
22205
+ } catch (err) {
22206
+ const reason = extractApiErrorReason(err);
22207
+ if (reason === null || !LESSON_TEXT_REASONS.includes(reason)) throw err;
22208
+ throw new Error(`${extractApiMessage(err)} \u2014 change the lesson's text in ${lessonPath(course, lesson)}`);
22209
+ }
22210
+ }
22211
+ function lessonFromServer(rooms, lesson, slug) {
22212
+ const room = roomKeyOf(rooms, lesson.room_id);
22213
+ return {
22214
+ slug,
22215
+ id: lesson.lesson_id,
22216
+ rev: serverLesson(lesson).rev,
22217
+ title: lesson.title,
22218
+ published: lesson.published,
22219
+ ...room === void 0 ? {} : { room },
22220
+ ...lesson.video ? { video_url: communityLessonVideoWatchUrl(lesson.video) } : {},
22221
+ links: lesson.links.map((link) => ({ title: link.title, url: link.url })),
22222
+ requires_paid_plan: lesson.requires_paid_plan,
22223
+ body: toLf(lesson.body)
22224
+ };
22225
+ }
22226
+ function moduleFromServer(module, key) {
22227
+ return { key, id: module.module_id, rev: serverModule(module).rev, title: module.title, lessons: [] };
22228
+ }
22229
+ function syncedFromServer(course) {
22230
+ return {
22231
+ modules: course.modules.map((module) => module.module_id),
22232
+ lessons: Object.fromEntries(course.modules.map((module) => [module.module_id, module.lessons.map((lesson) => lesson.lesson_id)]))
22233
+ };
22234
+ }
22235
+ function courseFromServer(rooms, course, slug) {
22236
+ const keys = /* @__PURE__ */ new Set();
22237
+ const slugs = /* @__PURE__ */ new Set();
22238
+ return {
22239
+ slug,
22240
+ id: course.course.course_id,
22241
+ rev: serverCourse(course.course).rev,
22242
+ title: course.course.title,
22243
+ description: course.course.description,
22244
+ published: course.course.published,
22245
+ modules: course.modules.map((module) => {
22246
+ const key = localName(module.title, module.module_id, keys, "module");
22247
+ keys.add(key);
22248
+ const entry = moduleFromServer(module, key);
22249
+ entry.lessons = module.lessons.map((lesson) => {
22250
+ const lessonSlug = localName(lesson.title, lesson.lesson_id, slugs, "lesson");
22251
+ slugs.add(lessonSlug);
22252
+ return lessonFromServer(rooms, lesson, lessonSlug);
22253
+ });
22254
+ return entry;
22255
+ }),
22256
+ synced: syncedFromServer(course),
22257
+ unlisted: []
22258
+ };
22259
+ }
22260
+ function coursesFromServer(files, server) {
22261
+ const slugs = /* @__PURE__ */ new Set();
22262
+ files.courses = server.courses.map((course) => {
22263
+ const slug = localName(course.course.title, course.course.course_id, slugs, "course");
22264
+ slugs.add(slug);
22265
+ return courseFromServer(files.rooms, course, slug);
22266
+ });
22267
+ files.synced_courses = server.ids;
22268
+ }
22269
+ function takeOrder(entries, placed) {
22270
+ const out = [...placed];
22271
+ for (const entry of entries) if (!out.includes(entry)) insertAfter(out, entry, entries);
22272
+ return out;
22273
+ }
22274
+ function mergeArrivals(entries, server, arrivals) {
22275
+ const named = new Set(server.filter((id) => !arrivals.has(id)));
22276
+ return mergeOrder(listed(entries), server, named).flatMap((slot) => "entry" in slot ? [slot.entry] : arrivals.get(slot.untracked) ?? []);
22277
+ }
22278
+ function pullCourses(files, plan, takes, note, listing) {
22279
+ for (const course of plan.courses) pullContent(files, course, takes, note, listing);
22280
+ const tracked = new Map(files.courses.flatMap((course) => course.id === void 0 ? [] : [[course.id, course]]));
22281
+ const held = new Set(files.unlistedCourses.flatMap((folder) => folder.id === null ? [] : [folder.id]));
22282
+ const slugs = new Set([...files.courses.map((course) => course.slug), ...files.unlistedCourses.map((folder) => folder.name)].map((name) => name.toLowerCase()));
22283
+ const courseFor = (id) => {
22284
+ const known = tracked.get(id);
22285
+ if (known) return known;
22286
+ const course = plan.server.course.get(id);
22287
+ if (held.has(id)) {
22288
+ listing.kept.push(`course ${show(course.course.title)} (${id}): its folder is not listed in ${COMMUNITY_FILE} courses \u2014 list it to pull it`);
22289
+ return null;
22290
+ }
22291
+ const slug = localName(course.course.title, id, slugs, "course");
22292
+ slugs.add(slug);
22293
+ listing.changes.push(`+ course ${slug}: ${show(course.course.title)}`);
22294
+ return courseFromServer(files.rooms, course, slug);
22295
+ };
22296
+ const order = plan.order;
22297
+ if (takes(order.state)) {
22298
+ files.courses = takeOrder(files.courses, order.server.flatMap((id) => courseFor(id) ?? []));
22299
+ listing.changes.push("~ courses order");
22300
+ } else {
22301
+ const arrivals = new Map(order.server.filter((id) => !tracked.has(id)).flatMap((id) => {
22302
+ const course = courseFor(id);
22303
+ return course ? [[id, course]] : [];
22304
+ }));
22305
+ files.courses = mergeArrivals(files.courses, order.server, arrivals);
22306
+ note(order.state, "courses order");
22307
+ }
22308
+ if (synced(order.state, takes)) files.synced_courses = [...order.server];
22309
+ for (const course of plan.courses) if (course.export) pullTree(files, plan.server, course, takes, note, listing);
22310
+ }
22311
+ function pullContent(files, course, takes, note, listing) {
22312
+ const { folder } = course;
22313
+ if (course.server) {
22314
+ const { state, server, serverRev } = course.server;
22315
+ if (takes(state)) {
22316
+ listing.changes.push(`~ course ${folder.slug}: ${courseChanges(courseContent(folder), courseContent(server)).join("; ")}`);
22317
+ takeCourse(folder, server);
22318
+ }
22319
+ if (takes(state) || state === "converged") folder.rev = serverRev;
22320
+ note(state, `course ${folder.slug}`);
22321
+ } else if (folder.id === void 0) {
22322
+ listing.kept.push(`course ${folder.slug} (to create)`);
22323
+ }
22324
+ for (const module of course.modules) {
22325
+ const at = `module ${folder.slug}/${module.entry.key}`;
22326
+ if (!module.server) {
22327
+ if (module.entry.id === void 0) listing.kept.push(`${at} (to create)`);
22328
+ continue;
22329
+ }
22330
+ const { state, server, serverRev } = module.server;
22331
+ if (takes(state)) {
22332
+ listing.changes.push(`~ ${at}: title ${show(module.entry.title)} \u2192 ${show(server.title)}`);
22333
+ if (moduleContent(module.entry).title !== moduleContent(server).title) module.entry.title = server.title;
22334
+ }
22335
+ if (takes(state) || state === "converged") module.entry.rev = serverRev;
22336
+ note(state, at);
22337
+ }
22338
+ for (const lesson of course.lessons) {
22339
+ const at = `lesson ${folder.slug}/${lesson.lesson.slug}`;
22340
+ if (!lesson.server) {
22341
+ if (lesson.lesson.id === void 0) listing.kept.push(`${at} (to create)`);
22342
+ continue;
22343
+ }
22344
+ const { state, server, serverRev } = lesson.server;
22345
+ if (takes(state)) {
22346
+ const changes = lessonChanges(fileLesson(files.rooms, lesson.lesson), lessonContent(server), (id) => roomName(files.rooms, id));
22347
+ listing.changes.push(`~ ${at}: ${changes.join("; ")}`);
22348
+ takeLesson(files.rooms, lesson.lesson, server);
22349
+ }
22350
+ if (takes(state) || state === "converged") lesson.lesson.rev = serverRev;
22351
+ note(state, at);
22352
+ }
22353
+ }
22354
+ function pullTree(files, index, course, takes, note, listing) {
22355
+ const { folder } = course;
22356
+ const server = course.export;
22357
+ const keys = new Set(folder.modules.map((module) => module.key));
22358
+ const unlisted = folder.unlisted.map((file) => ({ slug: path26.basename(file.name, LESSON_FILE_EXT), id: file.id }));
22359
+ const slugs = new Set([...folder.modules.flatMap((module) => module.lessons.map((lesson) => lesson.slug)), ...unlisted.map((file) => file.slug)].map((name) => name.toLowerCase()));
22360
+ const held = new Set(unlisted.flatMap((file) => file.id === null ? [] : [file.id]));
22361
+ const fresh = /* @__PURE__ */ new Set();
22362
+ const moduleFor = (id) => {
22363
+ const known = folder.modules.find((module2) => module2.id === id);
22364
+ if (known) return known;
22365
+ const { module } = index.module.get(id);
22366
+ const key = localName(module.title, id, keys, "module");
22367
+ keys.add(key);
22368
+ listing.changes.push(`+ module ${folder.slug}/${key}: ${show(module.title)}`);
22369
+ const entry = moduleFromServer(module, key);
22370
+ fresh.add(entry);
22371
+ return entry;
22372
+ };
22373
+ const modulesOrder = course.modulesOrder;
22374
+ const what = `modules order of course ${folder.slug}`;
22375
+ if (takes(modulesOrder.state)) {
22376
+ folder.modules = takeOrder(folder.modules, modulesOrder.server.flatMap((id) => moduleFor(id) ?? []));
22377
+ listing.changes.push(`~ ${what}`);
22378
+ } else {
22379
+ const arrivals = new Map(modulesOrder.server.filter((id) => !folder.modules.some((m) => m.id === id)).flatMap((id) => {
22380
+ const entry = moduleFor(id);
22381
+ return entry ? [[id, entry]] : [];
22382
+ }));
22383
+ folder.modules = mergeArrivals(folder.modules, modulesOrder.server, arrivals);
22384
+ note(modulesOrder.state, what);
22385
+ }
22386
+ const entryOf = (moduleId) => folder.modules.find((module) => module.id === moduleId);
22387
+ const home = /* @__PURE__ */ new Map();
22388
+ const keptApart = /* @__PURE__ */ new Set();
22389
+ for (const lesson of course.lessons) {
22390
+ const { placement } = lesson;
22391
+ if (!placement) {
22392
+ home.set(lesson.lesson, lesson.module);
22393
+ continue;
22394
+ }
22395
+ const at = `lesson ${folder.slug}/${lesson.lesson.slug}`;
22396
+ if (placement.state === "conflict") note("conflict", `${at}: moved in the files and on the server`);
22397
+ if (placement.state === "local") listing.kept.push(`${at}: moved to module ${lesson.module.key}`);
22398
+ const taken = takes(placement.state) && placement.state !== "converged";
22399
+ const to = taken ? entryOf(placement.server) : void 0;
22400
+ if (to && to !== lesson.module) listing.changes.push(`~ ${at}: moved to module ${to.key}`);
22401
+ if (placement.state === "conflict" && !taken) keptApart.add(lesson);
22402
+ home.set(lesson.lesson, to ?? lesson.module);
22403
+ }
22404
+ const named = idsOf(course.lessons.map((lesson) => lesson.lesson));
22405
+ for (const module of server.modules) {
22406
+ const entry = entryOf(module.module_id);
22407
+ if (!entry) continue;
22408
+ for (const lesson of module.lessons) {
22409
+ if (named.has(lesson.lesson_id)) continue;
22410
+ if (held.has(lesson.lesson_id)) {
22411
+ 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`);
22412
+ continue;
22413
+ }
22414
+ const slug = localName(lesson.title, lesson.lesson_id, slugs, "lesson");
22415
+ slugs.add(slug);
22416
+ listing.changes.push(`+ lesson ${folder.slug}/${slug}: ${show(lesson.title)}`);
22417
+ home.set(lessonFromServer(files.rooms, lesson, slug), entry);
22418
+ }
22419
+ }
22420
+ for (const entry of folder.modules) {
22421
+ const members = [...home].filter(([, at]) => at === entry).map(([lesson]) => lesson);
22422
+ const own = entry.lessons.filter((lesson) => lesson.id === void 0 || members.includes(lesson));
22423
+ const serverIds = entry.id === void 0 ? [] : index.module.get(entry.id)?.module.lessons.map((lesson) => lesson.lesson_id) ?? [];
22424
+ const plan = course.modules.find((m) => m.entry === entry);
22425
+ const take = fresh.has(entry) || plan?.lessonsOrder !== null && plan?.lessonsOrder !== void 0 && takes(plan.lessonsOrder.state);
22426
+ if (take) {
22427
+ const byId = new Map(members.flatMap((lesson) => lesson.id === void 0 ? [] : [[lesson.id, lesson]]));
22428
+ entry.lessons = takeOrder(own, serverIds.flatMap((id) => byId.get(id) ?? []));
22429
+ if (!fresh.has(entry)) listing.changes.push(`~ lessons order of module ${folder.slug}/${entry.key}`);
22430
+ } else {
22431
+ const arrivals = new Map(members.filter((lesson) => !own.includes(lesson)).flatMap((lesson) => lesson.id === void 0 ? [] : [[lesson.id, lesson]]));
22432
+ entry.lessons = mergeArrivals(own, serverIds, arrivals);
22433
+ if (plan?.lessonsOrder) note(plan.lessonsOrder.state, `lessons order of module ${folder.slug}/${entry.key}`);
22434
+ }
22435
+ }
22436
+ const was = folder.synced;
22437
+ const lessons = {};
22438
+ for (const module of server.modules) {
22439
+ const verdict = course.modules.find((m) => m.entry.id === module.module_id)?.lessonsOrder;
22440
+ if (verdict && !synced(verdict.state, takes) && was?.lessons[module.module_id]) {
22441
+ lessons[module.module_id] = was.lessons[module.module_id];
22442
+ continue;
22443
+ }
22444
+ const kept = [...keptApart].filter((lesson) => lesson.placement.server === module.module_id).map((lesson) => lesson.lesson.id);
22445
+ const based = [...keptApart].filter((lesson) => lesson.placement.base === module.module_id).map((lesson) => lesson.lesson.id);
22446
+ const ids = module.lessons.map((lesson) => lesson.lesson_id).filter((id) => !kept.includes(id));
22447
+ for (const id of based) if (!ids.includes(id)) insertAfter(ids, id, was?.lessons[module.module_id] ?? []);
22448
+ lessons[module.module_id] = ids;
22449
+ }
22450
+ folder.synced = {
22451
+ modules: synced(modulesOrder.state, takes) || !was?.modules ? server.modules.map((module) => module.module_id) : was.modules,
22452
+ lessons
22453
+ };
22454
+ }
22455
+ function coursesChangedSince(before, after) {
22456
+ const states = (server) => {
22457
+ const out = /* @__PURE__ */ new Map();
22458
+ out.set("courses", { label: "courses order", rev: server.ids.join() });
22459
+ for (const course of server.courses) {
22460
+ const courseId = course.course.course_id;
22461
+ const label = `course ${show(course.course.title)}`;
22462
+ out.set(courseId, { label, rev: serverCourse(course.course).rev });
22463
+ out.set(`modules:${courseId}`, { label, rev: course.modules.map((m) => m.module_id).join() });
22464
+ for (const module of course.modules) {
22465
+ const moduleLabel = `module ${show(module.title)}`;
22466
+ out.set(module.module_id, { label: moduleLabel, rev: serverModule(module).rev });
22467
+ out.set(`lessons:${module.module_id}`, { label: moduleLabel, rev: module.lessons.map((l) => l.lesson_id).join() });
22468
+ for (const lesson of module.lessons) out.set(lesson.lesson_id, { label: `lesson ${show(lesson.title)}`, rev: serverLesson(lesson).rev });
22469
+ }
22470
+ }
22471
+ return out;
22472
+ };
22473
+ const was = states(before);
22474
+ const changed = /* @__PURE__ */ new Set();
22475
+ for (const [key, now] of states(after)) if (was.get(key)?.rev !== now.rev) changed.add(now.label);
22476
+ return [...changed];
22477
+ }
22478
+ var ServerCourses, PENDING_ROOM, listed, Sim, freshOrder, realIds, plannedIds, LESSON_TEXT_REASONS, CoursePush, synced;
22479
+ var init_courses = __esm({
22480
+ "src/communities/courses.ts"() {
22481
+ "use strict";
22482
+ init_contracts();
22483
+ init_community_workspace();
22484
+ init_errors2();
22485
+ init_expected();
22486
+ init_course_files();
22487
+ init_listing();
22488
+ init_model();
22489
+ init_order();
22490
+ ServerCourses = class {
22491
+ constructor(courses) {
22492
+ this.courses = courses;
22493
+ for (const course of courses) {
22494
+ const courseId = course.course.course_id;
22495
+ this.course.set(courseId, course);
22496
+ for (const module of course.modules) {
22497
+ this.module.set(module.module_id, { courseId, module });
22498
+ for (const lesson of module.lessons) this.lesson.set(lesson.lesson_id, { courseId, moduleId: module.module_id, lesson });
22499
+ }
22500
+ }
22501
+ }
22502
+ course = /* @__PURE__ */ new Map();
22503
+ module = /* @__PURE__ */ new Map();
22504
+ lesson = /* @__PURE__ */ new Map();
22505
+ get ids() {
22506
+ return this.courses.map((course) => course.course.course_id);
22507
+ }
22508
+ };
22509
+ PENDING_ROOM = "pending:";
22510
+ listed = (entries) => listedIds(entries, (entry) => entry.id);
22511
+ Sim = class _Sim {
22512
+ constructor(courses, modules = /* @__PURE__ */ new Map(), lessons = /* @__PURE__ */ new Map(), at = /* @__PURE__ */ new Map()) {
22513
+ this.courses = courses;
22514
+ this.modules = modules;
22515
+ this.lessons = lessons;
22516
+ this.at = at;
22517
+ }
22518
+ static from(plan) {
22519
+ const order = (ids, verdict) => ({
22520
+ server: [...ids],
22521
+ pinned: verdict?.state === "remote" ? [...verdict.base ?? []] : null,
22522
+ conflict: verdict?.state === "conflict",
22523
+ touched: false
22524
+ });
22525
+ const sim = new _Sim(order(plan.order.server, plan.order));
22526
+ for (const course of plan.server.courses) {
22527
+ const courseId = course.course.course_id;
22528
+ const tracked = plan.courses.find((c) => c.export?.course.course_id === courseId);
22529
+ sim.modules.set(courseId, order(course.modules.map((m) => m.module_id), tracked?.modulesOrder));
22530
+ for (const module of course.modules) {
22531
+ const entry = tracked?.modules.find((m) => m.server?.server.module_id === module.module_id);
22532
+ sim.lessons.set(module.module_id, order(module.lessons.map((l) => l.lesson_id), entry?.lessonsOrder));
22533
+ for (const lesson of module.lessons) sim.at.set(lesson.lesson_id, module.module_id);
22534
+ }
22535
+ }
22536
+ return sim;
22537
+ }
22538
+ clone() {
22539
+ const copy = (order) => ({ ...order, server: [...order.server], pinned: order.pinned && [...order.pinned] });
22540
+ const map = (orders) => new Map([...orders].map(([id, order]) => [id, copy(order)]));
22541
+ return new _Sim(copy(this.courses), map(this.modules), map(this.lessons), new Map(this.at));
22542
+ }
22543
+ };
22544
+ freshOrder = () => ({ server: [], pinned: null, conflict: false, touched: true });
22545
+ realIds = { course: (c) => c.id, module: (_c, m) => m.id, lesson: (_c, l) => l.id };
22546
+ plannedIds = {
22547
+ course: (c) => c.id ?? `new:course:${c.slug}`,
22548
+ module: (c, m) => m.id ?? `new:module:${c.slug}/${m.key}`,
22549
+ lesson: (c, l) => l.id ?? `new:lesson:${c.slug}/${l.slug}`
22550
+ };
22551
+ LESSON_TEXT_REASONS = [
22552
+ COMMUNITY_LESSON_LINE_PREFIX_TOO_WIDE_REASON,
22553
+ COMMUNITY_LESSON_TREE_TOO_DEEP_REASON,
22554
+ COMMUNITY_LESSON_TREE_TOO_LARGE_REASON
22555
+ ];
22556
+ CoursePush = class {
22557
+ constructor(files, plan, remote, save) {
22558
+ this.files = files;
22559
+ this.plan = plan;
22560
+ this.remote = remote;
22561
+ this.save = save;
22562
+ this.sim = Sim.from(plan);
22563
+ for (const lesson of plan.courses.flatMap((course) => course.lessons)) {
22564
+ const { placement } = lesson;
22565
+ if (!placement || lesson.lesson.id === void 0) continue;
22566
+ this.placements.set(lesson.lesson.id, placement);
22567
+ if (placement.state !== "remote") continue;
22568
+ this.movedIn.set(placement.server, /* @__PURE__ */ new Set([...this.movedIn.get(placement.server) ?? [], lesson.lesson.id]));
22569
+ if (placement.base !== void 0) this.movedOut.set(placement.base, [...this.movedOut.get(placement.base) ?? [], lesson.lesson.id]);
22570
+ }
22571
+ for (const course of plan.server.courses) {
22572
+ this.courseNow.set(course.course.course_id, course.course);
22573
+ this.names.set(course.course.course_id, show(course.course.title));
22574
+ for (const module of course.modules) {
22575
+ this.moduleNow.set(module.module_id, module);
22576
+ this.names.set(module.module_id, show(module.title));
22577
+ for (const lesson of module.lessons) {
22578
+ this.lessonNow.set(lesson.lesson_id, lesson);
22579
+ this.names.set(lesson.lesson_id, show(lesson.title));
22580
+ }
22581
+ }
22582
+ }
22583
+ for (const course of files.courses) {
22584
+ this.names.set(plannedIds.course(course), course.slug);
22585
+ for (const module of course.modules) {
22586
+ this.names.set(plannedIds.module(course, module), module.key);
22587
+ for (const lesson of module.lessons) this.names.set(plannedIds.lesson(course, lesson), lesson.slug);
22588
+ }
22589
+ }
22590
+ this.predicted = this.predict();
22591
+ }
22592
+ sim;
22593
+ courseNow = /* @__PURE__ */ new Map();
22594
+ moduleNow = /* @__PURE__ */ new Map();
22595
+ lessonNow = /* @__PURE__ */ new Map();
22596
+ names = /* @__PURE__ */ new Map();
22597
+ /** Files whose synced lists changed since they were last saved. */
22598
+ unsaved = /* @__PURE__ */ new Set();
22599
+ /** Each listed lesson's place's verdict, by id. */
22600
+ placements = /* @__PURE__ */ new Map();
22601
+ /** The lessons the server moved since, which push leaves where they are: by module, those it moved in and out. */
22602
+ movedIn = /* @__PURE__ */ new Map();
22603
+ movedOut = /* @__PURE__ */ new Map();
22604
+ /** The places phase as the plan predicts it, every create landing as a new entity. */
22605
+ predicted;
22606
+ get communityId() {
22607
+ return this.files.community_id;
22608
+ }
22609
+ lessons() {
22610
+ return this.plan.courses.flatMap((course) => course.lessons);
22611
+ }
22612
+ /** The places phase on a copy of the server's orders where every create has landed, last. */
22613
+ predict() {
22614
+ const sim = this.sim.clone();
22615
+ for (const course of this.files.courses) {
22616
+ const courseId = plannedIds.course(course);
22617
+ if (course.id === void 0) {
22618
+ append(sim.courses, courseId);
22619
+ sim.modules.set(courseId, freshOrder());
22620
+ }
22621
+ for (const module of course.modules) {
22622
+ const moduleId = plannedIds.module(course, module);
22623
+ if (module.id === void 0) {
22624
+ const modules = sim.modules.get(courseId);
22625
+ if (!modules) continue;
22626
+ append(modules, moduleId);
22627
+ sim.lessons.set(moduleId, freshOrder());
22628
+ }
22629
+ for (const lesson of module.lessons) {
22630
+ const lessons = sim.lessons.get(moduleId);
22631
+ if (lesson.id !== void 0 || !lessons) continue;
22632
+ const lessonId = plannedIds.lesson(course, lesson);
22633
+ append(lessons, lessonId);
22634
+ sim.at.set(lessonId, moduleId);
22635
+ }
22636
+ }
22637
+ }
22638
+ return computePlaces(this.files, sim, plannedIds, this.placements);
22639
+ }
22640
+ // ── The listing ──
22641
+ roomName = (id) => roomName(this.files.rooms, id);
22642
+ /** Push's listing of the courses, server → files. */
22643
+ list(listing) {
22644
+ const { plan, files } = this;
22645
+ for (const course of plan.courses) {
22646
+ const { folder } = course;
22647
+ if (!course.server) {
22648
+ listing.changes.push(`+ course ${folder.slug}: create ${show(folder.title)}, ${folder.published ? "published once its lessons are in place" : "a draft"}`);
22649
+ } else if (course.server.state === "local") {
22650
+ listing.changes.push(`~ course ${folder.slug}: ${courseChanges(courseContent(course.server.server), courseContent(folder)).join("; ")}`);
22651
+ }
22652
+ for (const module of course.modules) {
22653
+ const at = `${folder.slug}/${module.entry.key}`;
22654
+ if (!module.server) listing.changes.push(`+ module ${at}: create ${show(module.entry.title)}`);
22655
+ else if (module.server.state === "local") listing.changes.push(`~ module ${at}: title ${show(module.server.server.title)} \u2192 ${show(module.entry.title)}`);
22656
+ }
22657
+ for (const lesson of course.lessons) {
22658
+ const at = `${folder.slug}/${lesson.lesson.slug}`;
22659
+ const file = fileLesson(files.rooms, lesson.lesson);
22660
+ if (!lesson.server) {
22661
+ const flags = [...file.published ? ["published"] : [], ...file.room_id ? [`room ${this.roomName(file.room_id)}`] : []];
22662
+ listing.changes.push(`+ lesson ${at}: create ${show(lesson.lesson.title)} in module ${lesson.module.key}${flags.length > 0 ? `, ${flags.join(", ")}` : ""}`);
22663
+ } else if (lesson.server.state === "local") {
22664
+ const server = lessonContent(lesson.server.server);
22665
+ listing.changes.push(`~ lesson ${at}: ${lessonChanges(server, file, this.roomName).join("; ")}`);
22666
+ if (server.body !== file.body) listing.changes.push(...lineDiff(server.body, file.body).map((line) => ` ${line}`));
22667
+ }
22668
+ }
22669
+ }
22670
+ for (const reorder of this.predicted.reorders) {
22671
+ listing.changes.push(`~ ${reorder.what}: ${reorder.from.map(this.name).join(", ")} \u2192 ${reorder.to.map(this.name).join(", ")}`);
22672
+ }
22673
+ const verdicts = (state) => [
22674
+ ...plan.order.state === state ? ["courses order"] : [],
22675
+ ...plan.courses.flatMap((course) => [
22676
+ ...course.server?.state === state ? [`course ${course.folder.slug}`] : [],
22677
+ ...course.modulesOrder?.state === state ? [`modules order of course ${course.folder.slug}`] : [],
22678
+ ...course.modules.flatMap((module) => [
22679
+ ...module.server?.state === state ? [`module ${course.folder.slug}/${module.entry.key}`] : [],
22680
+ ...module.lessonsOrder?.state === state ? [`lessons order of module ${course.folder.slug}/${module.entry.key}`] : []
22681
+ ]),
22682
+ ...course.lessons.flatMap((lesson) => [
22683
+ ...lesson.server?.state === state ? [`lesson ${course.folder.slug}/${lesson.lesson.slug}`] : [],
22684
+ ...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)}`}`] : []
22685
+ ])
22686
+ ])
22687
+ ];
22688
+ listing.serverChanged.push(...verdicts("remote"));
22689
+ listing.conflicts.push(.../* @__PURE__ */ new Set([...verdicts("conflict"), ...this.predicted.conflicts]));
22690
+ listing.leftAsIs.push(
22691
+ ...plan.untracked.map((course) => `course ${show(course.course.title)} (${course.course.course_id})`),
22692
+ ...plan.courses.flatMap((course) => [
22693
+ ...course.untrackedModules.map((module) => `module ${show(module.title)} (${module.module_id}) of course ${course.folder.slug}`),
22694
+ ...course.untrackedLessons.map((lesson) => `lesson ${show(lesson.title)} (${lesson.lesson_id}) of course ${course.folder.slug}`)
22695
+ ])
22696
+ );
22697
+ }
22698
+ name = (id) => this.names.get(id) ?? id;
22699
+ /** Revs to refresh without a write: both sides agree, or the files carried no base. Saves the files they live in. */
22700
+ refreshConverged() {
22701
+ const { plan, files } = this;
22702
+ let refreshed = 0;
22703
+ const refresh = (converged, owner, set) => {
22704
+ if (!converged) return;
22705
+ set();
22706
+ this.unsaved.add(owner);
22707
+ refreshed++;
22708
+ };
22709
+ const rebase = (converged, order) => {
22710
+ if (!converged || !order) return;
22711
+ order.touched = true;
22712
+ refreshed++;
22713
+ };
22714
+ rebase(plan.order.state === "converged", this.sim.courses);
22715
+ for (const course of plan.courses) {
22716
+ const { folder } = course;
22717
+ refresh(course.server?.state === "converged", folder, () => {
22718
+ folder.rev = course.server.serverRev;
22719
+ });
22720
+ if (folder.id !== void 0) rebase(course.modulesOrder?.state === "converged", this.sim.modules.get(folder.id));
22721
+ for (const module of course.modules) {
22722
+ refresh(module.server?.state === "converged", folder, () => {
22723
+ module.entry.rev = module.server.serverRev;
22724
+ });
22725
+ if (module.entry.id !== void 0) rebase(module.lessonsOrder?.state === "converged", this.sim.lessons.get(module.entry.id));
22726
+ }
22727
+ for (const lesson of course.lessons) {
22728
+ const placement = lesson.placement;
22729
+ if (placement?.state !== "converged") continue;
22730
+ const was = placement.base === void 0 ? void 0 : this.sim.lessons.get(placement.base);
22731
+ if (was) {
22732
+ const at = was.pinned?.indexOf(lesson.lesson.id) ?? -1;
22733
+ if (at >= 0) was.pinned.splice(at, 1);
22734
+ was.touched = true;
22735
+ }
22736
+ const now = this.sim.lessons.get(placement.server);
22737
+ if (now) {
22738
+ if (now.pinned && !now.pinned.includes(lesson.lesson.id)) insertAfter(now.pinned, lesson.lesson.id, lesson.module.lessons.map((l) => l.id ?? ""));
22739
+ now.touched = true;
22740
+ }
22741
+ refreshed++;
22742
+ }
22743
+ for (const lesson of course.lessons) {
22744
+ if (lesson.server?.state === "converged") {
22745
+ lesson.lesson.rev = lesson.server.serverRev;
22746
+ this.save.lesson(folder, lesson.lesson);
22747
+ refreshed++;
22748
+ }
22749
+ }
22750
+ }
22751
+ this.settle();
22752
+ return refreshed;
22753
+ }
22754
+ /** Save the files whose synced lists changed. */
22755
+ flush() {
22756
+ for (const owner of this.unsaved) {
22757
+ if (owner === null) this.save.community();
22758
+ else this.save.course(owner);
22759
+ }
22760
+ this.unsaved.clear();
22761
+ }
22762
+ // ── The writes, phase by phase ──
22763
+ /** Courses, then lessons, that the files take off the community: first, before anything else changes. */
22764
+ unpublishWrites() {
22765
+ const writes = [];
22766
+ for (const course of this.plan.courses) {
22767
+ const { server, folder } = course;
22768
+ if (server?.state === "local" && server.server.published && !folder.published) {
22769
+ writes.push({ label: `course ${folder.slug} (unpublish)`, requests: 1, apply: () => this.updateCourse(folder, false) });
22770
+ }
22771
+ }
22772
+ for (const lesson of this.lessons()) {
22773
+ const { server, lesson: file, course } = lesson;
22774
+ if (server?.state === "local" && server.server.published && !file.published) {
22775
+ writes.push({ label: `lesson ${course.slug}/${file.slug} (unpublish)`, requests: 1, apply: () => lessonWrite(course, file, () => this.updateLesson(course, file, true)) });
22776
+ }
22777
+ }
22778
+ return writes;
22779
+ }
22780
+ /** New courses, as drafts; and the courses whose content changed, but for their publishing. */
22781
+ courseWrites() {
22782
+ return this.plan.courses.flatMap((course) => {
22783
+ const { server, folder } = course;
22784
+ if (!server) return [{ label: `course ${folder.slug} (create)`, requests: 1, apply: () => this.createCourse(folder) }];
22785
+ if (server.state !== "local" || server.server.published !== folder.published) return [];
22786
+ return [{ label: `course ${folder.slug}`, requests: 1, apply: () => this.updateCourse(folder, false) }];
22787
+ });
22788
+ }
22789
+ moduleWrites() {
22790
+ return this.plan.courses.flatMap((course) => course.modules.flatMap((module) => {
22791
+ const at = `${course.folder.slug}/${module.entry.key}`;
22792
+ if (!module.server) return [{ label: `module ${at} (create)`, requests: 1, apply: () => this.createModule(course.folder, module.entry) }];
22793
+ if (module.server.state !== "local") return [];
22794
+ return [{ label: `module ${at}`, requests: 1, apply: () => this.updateModule(course.folder, module.entry) }];
22795
+ }));
22796
+ }
22797
+ lessonWrites() {
22798
+ return this.lessons().flatMap((lesson) => {
22799
+ const { server, course, module, lesson: file } = lesson;
22800
+ const at = `${course.slug}/${file.slug}`;
22801
+ if (!server) return [{ label: `lesson ${at} (create)`, requests: 1, apply: () => lessonWrite(course, file, () => this.createLesson(course, module, file)) }];
22802
+ if (server.state !== "local") return [];
22803
+ const unpublished = server.server.published && !file.published;
22804
+ if (unpublished && !fileLesson(this.files.rooms, file).room_id?.startsWith(PENDING_ROOM)) return [];
22805
+ return [{ label: `lesson ${at}`, requests: 1, apply: () => lessonWrite(course, file, () => this.updateLesson(course, file, false)) }];
22806
+ });
22807
+ }
22808
+ /** How many position writes the plan predicts, every create landing as a new entity. */
22809
+ get plannedPlaces() {
22810
+ return this.predicted.moves.length;
22811
+ }
22812
+ /** The position writes, from the server's orders as the creates left them: when the phase is reached. */
22813
+ placeWrites() {
22814
+ return computePlaces(this.files, this.sim.clone(), realIds, this.placements).moves.map((move) => ({
22815
+ 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)}`,
22816
+ requests: 1,
22817
+ apply: () => this.place(move)
22818
+ }));
22819
+ }
22820
+ /** Courses the files publish: last, once their lessons are in place. */
22821
+ publishWrites() {
22822
+ return this.plan.courses.flatMap((course) => {
22823
+ const { folder, server } = course;
22824
+ const now = folder.id === void 0 ? void 0 : this.courseNow.get(folder.id);
22825
+ if (!folder.published || now?.published || server !== null && server.state !== "local") return [];
22826
+ return [{ label: `course ${folder.slug} (publish)`, requests: 1, apply: () => this.updateCourse(folder, true) }];
22827
+ });
22828
+ }
22829
+ pathOf(id) {
22830
+ for (const course of this.files.courses) {
22831
+ if (course.id === id) return course.slug;
22832
+ for (const module of course.modules) {
22833
+ if (module.id === id) return `${course.slug}/${module.key}`;
22834
+ for (const lesson of module.lessons) if (lesson.id === id) return `${course.slug}/${lesson.slug}`;
22835
+ }
22836
+ }
22837
+ return this.name(id);
22838
+ }
22839
+ // ── Each write ──
22840
+ async createCourse(folder) {
22841
+ const made = await this.remote.createCourse({
22842
+ title: folder.title,
22843
+ description: folder.description,
22844
+ client_id: await createClientId(this.communityId, "course", folder.slug)
22845
+ });
22846
+ const twin = this.files.courses.find((other) => other !== folder && other.id === made.course_id);
22847
+ if (twin) {
22848
+ throw expected(
22849
+ `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.`
22850
+ );
22851
+ }
22852
+ const existed = this.sim.modules.get(made.course_id);
22853
+ folder.id = made.course_id;
22854
+ folder.rev = serverCourse(made).rev;
22855
+ this.courseNow.set(made.course_id, made);
22856
+ if (!existed) {
22857
+ append(this.sim.courses, made.course_id);
22858
+ this.sim.modules.set(made.course_id, freshOrder());
22859
+ }
22860
+ this.unsaved.add(folder);
22861
+ this.settle();
22862
+ await this.updateCourse(folder, false);
22863
+ }
22864
+ async updateCourse(folder, publish) {
22865
+ const now = this.courseNow.get(folder.id);
22866
+ const body = coursePatch(folder, now, publish);
22867
+ if (!body) return;
22868
+ const answer = await this.remote.updateCourse(folder.id, body);
22869
+ takeCourse(folder, answer, new Set(Object.keys(body)));
22870
+ folder.rev = serverCourse(answer).rev;
22871
+ this.courseNow.set(answer.course_id, answer);
22872
+ this.save.course(folder);
22873
+ }
22874
+ async createModule(course, entry) {
22875
+ const courseId = course.id;
22876
+ const made = await this.remote.createModule(courseId, {
22877
+ title: entry.title,
22878
+ client_id: await createClientId(this.communityId, "module", `${course.slug}/${entry.key}`)
22879
+ });
22880
+ if (made.course_id !== courseId) {
22881
+ throw expected(
22882
+ `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.`
22883
+ );
22884
+ }
22885
+ const twin = this.files.courses.flatMap((c) => c.modules.map((m) => ({ c, m }))).find(({ m }) => m !== entry && m.id === made.module_id);
22886
+ if (twin) {
22887
+ 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.`);
22888
+ }
22889
+ const existed = this.sim.lessons.get(made.module_id);
22890
+ entry.id = made.module_id;
22891
+ entry.rev = serverModule(made).rev;
22892
+ this.moduleNow.set(made.module_id, made);
22893
+ if (!existed) {
22894
+ append(this.sim.modules.get(courseId), made.module_id);
22895
+ this.sim.lessons.set(made.module_id, freshOrder());
22896
+ }
22897
+ this.unsaved.add(course);
22898
+ this.settle();
22899
+ await this.updateModule(course, entry);
22900
+ }
22901
+ async updateModule(course, entry) {
22902
+ const now = this.moduleNow.get(entry.id);
22903
+ if (moduleContent(entry).title === moduleContent(now).title) return;
22904
+ const answer = await this.remote.updateModule(course.id, entry.id, { title: entry.title });
22905
+ if (moduleContent(answer).title !== moduleContent(entry).title) entry.title = answer.title;
22906
+ entry.rev = serverModule(answer).rev;
22907
+ this.moduleNow.set(answer.module_id, answer);
22908
+ this.save.course(course);
22909
+ }
22910
+ async createLesson(course, module, lesson) {
22911
+ const courseId = course.id;
22912
+ const roomId = fileLesson(this.files.rooms, lesson).room_id;
22913
+ if (roomId?.startsWith(PENDING_ROOM)) throw new Error(`createLesson: room ${lesson.room} has no id yet`);
22914
+ const made = await this.remote.createLesson(courseId, {
22915
+ module_id: module.id,
22916
+ title: lesson.title,
22917
+ ...lesson.video_url === void 0 ? {} : { video_url: lesson.video_url },
22918
+ body: lesson.body,
22919
+ links: lesson.links,
22920
+ ...roomId === null ? {} : { room_id: roomId },
22921
+ requires_paid_plan: lesson.requires_paid_plan,
22922
+ published: lesson.published,
22923
+ client_id: await createClientId(this.communityId, "lesson", `${course.slug}/${lesson.slug}`)
22924
+ });
22925
+ if (made.course_id !== courseId) {
22926
+ throw expected(
22927
+ `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.`
22928
+ );
22929
+ }
22930
+ const twin = this.lessons().find((other) => other.lesson !== lesson && other.lesson.id === made.lesson_id);
22931
+ if (twin) {
22932
+ 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.`);
22933
+ }
22934
+ const existed = this.sim.at.has(made.lesson_id);
22935
+ lesson.id = made.lesson_id;
22936
+ lesson.rev = serverLesson(made).rev;
22937
+ this.lessonNow.set(made.lesson_id, made);
22938
+ if (!existed) {
22939
+ append(this.sim.lessons.get(made.module_id), made.lesson_id);
22940
+ this.sim.at.set(made.lesson_id, made.module_id);
22941
+ }
22942
+ this.save.lesson(course, lesson);
22943
+ this.settle();
22944
+ await this.updateLesson(course, lesson, false);
22945
+ }
22946
+ async updateLesson(course, lesson, waitForRooms) {
22947
+ const now = this.lessonNow.get(lesson.id);
22948
+ const body = lessonPatch(this.files.rooms, lesson, now, waitForRooms);
22949
+ if (!body) return;
22950
+ const answer = await this.remote.updateLesson(course.id, lesson.id, body);
22951
+ takeLesson(this.files.rooms, lesson, answer, new Set(Object.keys(body)));
22952
+ lesson.rev = serverLesson(answer).rev;
22953
+ this.lessonNow.set(answer.lesson_id, answer);
22954
+ this.save.lesson(course, lesson);
22955
+ }
22956
+ /**
22957
+ * A position write. One whose answer is lost may have moved the child or not: the server is read
22958
+ * again, and when the child stands where the write put it, the write is recorded as if answered —
22959
+ * the orders it changed, and no other — before the failure stands. A refusal is not read back.
22960
+ */
22961
+ async place(move) {
22962
+ try {
22963
+ await this.sendPlace(move);
22964
+ } catch (err) {
22965
+ if (outcomeUnknown(err)) await this.recordIfLanded(move).catch(() => void 0);
22966
+ throw err;
22967
+ }
22968
+ }
22969
+ async sendPlace(move) {
22970
+ if (move.kind === "course") {
22971
+ const answer = await this.remote.updateCourse(move.id, { position: move.position });
22972
+ this.placedCourse(answer, answer.position);
22973
+ } else if (move.kind === "module") {
22974
+ const answer = await this.remote.updateModule(move.courseId, move.id, { position: move.position });
22975
+ this.placedModule(move.courseId, answer, answer.position);
22976
+ } else {
22977
+ const from = this.sim.at.get(move.id);
22978
+ const body = from === move.moduleId ? { position: move.position } : { module_id: move.moduleId, position: move.position };
22979
+ const answer = await this.remote.updateLesson(move.courseId, move.id, body);
22980
+ this.placedLesson(answer, answer.module_id, answer.position);
22981
+ }
22982
+ }
22983
+ /** A position write whose answer was lost, recorded when a read finds its child where it put it. */
22984
+ async recordIfLanded(move) {
22985
+ const wanted = (order) => {
22986
+ const ids = [...order.server];
22987
+ placeAt(ids, move.id, move.position);
22988
+ return ids.indexOf(move.id);
22989
+ };
22990
+ if (move.kind === "course") {
22991
+ const courses = await this.remote.listCourses();
22992
+ const at2 = courses.findIndex((course2) => course2.course_id === move.id);
22993
+ if (at2 >= 0 && at2 === wanted(this.sim.courses)) this.placedCourse(courses[at2], at2);
22994
+ return;
22995
+ }
22996
+ const course = await this.remote.exportCourse(move.courseId);
22997
+ if (move.kind === "module") {
22998
+ const at2 = course.modules.findIndex((module) => module.module_id === move.id);
22999
+ if (at2 >= 0 && at2 === wanted(this.sim.modules.get(move.courseId))) this.placedModule(move.courseId, course.modules[at2], at2);
23000
+ return;
23001
+ }
23002
+ const lessons = course.modules.find((module) => module.module_id === move.moduleId)?.lessons ?? [];
23003
+ const at = lessons.findIndex((lesson) => lesson.lesson_id === move.id);
23004
+ if (at >= 0 && at === wanted(this.sim.lessons.get(move.moduleId))) this.placedLesson(lessons[at], move.moduleId, at);
23005
+ }
23006
+ placedCourse(now, position) {
23007
+ shift(this.sim.courses, now.course_id, position);
23008
+ this.courseNow.set(now.course_id, now);
23009
+ this.settle();
23010
+ }
23011
+ placedModule(courseId, now, position) {
23012
+ shift(this.sim.modules.get(courseId), now.module_id, position);
23013
+ this.moduleNow.set(now.module_id, now);
23014
+ this.settle();
23015
+ }
23016
+ placedLesson(now, moduleId, position) {
23017
+ const from = this.sim.at.get(now.lesson_id);
23018
+ if (moduleId !== from) leave(this.sim.lessons.get(from), now.lesson_id);
23019
+ shift(this.sim.lessons.get(moduleId), now.lesson_id, position);
23020
+ this.sim.at.set(now.lesson_id, moduleId);
23021
+ this.lessonNow.set(now.lesson_id, now);
23022
+ this.settle();
23023
+ }
23024
+ /**
23025
+ * After a write that changed a parent's order (or a refresh), each such parent's synced list is
23026
+ * recorded and the files keeping a changed one are saved. A synced list is the server's order as
23027
+ * its own placement leaves it — less a lesson the server moved in since, and with one it moved
23028
+ * out, which the files have not taken: their base stays the module the files last synced them in
23029
+ * — or, for a parent whose order changed on the server only, the files' synced list as push left
23030
+ * it (`OrderSim.pinned`).
23031
+ */
23032
+ settle() {
23033
+ const record = (order, current, base, set, owner) => {
23034
+ if (!order?.touched || order.conflict) return;
23035
+ order.touched = false;
23036
+ const ids = order.pinned ?? base(order, current ?? []);
23037
+ if (current && sameOrder(current, ids)) return;
23038
+ set([...ids]);
23039
+ this.unsaved.add(owner);
23040
+ };
23041
+ const asIs = (order) => order.server;
23042
+ record(this.sim.courses, this.files.synced_courses, asIs, (ids) => {
23043
+ this.files.synced_courses = ids;
23044
+ }, null);
23045
+ for (const course of this.files.courses) {
23046
+ const modules = course.id === void 0 ? void 0 : this.sim.modules.get(course.id);
23047
+ if (!modules) continue;
23048
+ const synced2 = () => course.synced ??= { lessons: {} };
23049
+ record(modules, course.synced?.modules, asIs, (ids) => {
23050
+ synced2().modules = ids;
23051
+ }, course);
23052
+ for (const moduleId of modules.server) {
23053
+ record(this.sim.lessons.get(moduleId), course.synced?.lessons[moduleId], (order, current) => this.lessonsBase(moduleId, order.server, current), (ids) => {
23054
+ synced2().lessons[moduleId] = ids;
23055
+ }, course);
23056
+ }
23057
+ }
23058
+ this.flush();
23059
+ }
23060
+ /**
23061
+ * A module's synced lessons: the server's, less those it moved in and with those it moved out, the
23062
+ * files not having taken either — each one moved out back in its place in the synced list as it
23063
+ * stood (`current`).
23064
+ */
23065
+ lessonsBase(moduleId, server, current) {
23066
+ const movedIn = this.movedIn.get(moduleId);
23067
+ const ids = server.filter((id) => !movedIn?.has(id));
23068
+ for (const id of this.movedOut.get(moduleId) ?? []) if (!ids.includes(id)) insertAfter(ids, id, current);
23069
+ return ids;
23070
+ }
23071
+ };
23072
+ synced = (state, takes) => state !== "conflict" || takes(state);
23073
+ }
23074
+ });
23075
+
21108
23076
  // src/communities/sync.ts
21109
23077
  var sync_exports2 = {};
21110
23078
  __export(sync_exports2, {
21111
23079
  pullCommunity: () => pullCommunity,
21112
23080
  pushCommunity: () => pushCommunity
21113
23081
  });
21114
- import * as fs18 from "fs";
21115
- import * as path25 from "path";
23082
+ import * as fs19 from "fs";
23083
+ import * as path27 from "path";
21116
23084
  function parseArgs5(args2) {
21117
23085
  return {
21118
23086
  autoConfirm: args2.includes("--yes") || args2.includes("-y"),
@@ -21173,11 +23141,22 @@ function planSync(files, server) {
21173
23141
  },
21174
23142
  rooms,
21175
23143
  untracked: server.rooms.filter((room) => !tracked.has(room.room_id)),
21176
- unknownIds
23144
+ unknownIds,
23145
+ courses: planCourses(files, server.courses)
21177
23146
  };
21178
23147
  }
21179
23148
  function unknownIdProblems(plan) {
21180
- return plan.unknownIds.map((entry) => `room ${entry.key}: ${entry.id} is not a room of this community`);
23149
+ return [...plan.unknownIds.map((entry) => `room ${entry.key}: ${entry.id} is not a room of this community`), ...plan.courses.problems];
23150
+ }
23151
+ function warnUnlisted(files) {
23152
+ for (const folder of files.unlistedCourses) {
23153
+ console.warn(`Skipped: ${unlistedPath(folder.name)} \u2014 ${COMMUNITY_FILE}'s courses does not list it.`);
23154
+ }
23155
+ for (const course of files.courses) {
23156
+ for (const file of course.unlisted) {
23157
+ console.warn(`Skipped: ${unlistedPath(file.name, course)} \u2014 no module of the course lists it.`);
23158
+ }
23159
+ }
21181
23160
  }
21182
23161
  function settingsChanges(from, to) {
21183
23162
  const out = [];
@@ -21186,31 +23165,6 @@ function settingsChanges(from, to) {
21186
23165
  if (from.rules !== to.rules) out.push("rules (rules.md)");
21187
23166
  return out;
21188
23167
  }
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
23168
  function roomChanges(from, to) {
21215
23169
  const out = [];
21216
23170
  if (from.name !== to.name) out.push(`name ${show(from.name)} \u2192 ${show(to.name)}`);
@@ -21220,19 +23174,6 @@ function roomChanges(from, to) {
21220
23174
  function joinLinkChange(from, to) {
21221
23175
  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
23176
  }
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
23177
  function pushProblems(files, server, plan) {
21237
23178
  const problems2 = unknownIdProblems(plan);
21238
23179
  if (files.name !== server.community.name) {
@@ -21244,12 +23185,13 @@ function pushProblems(files, server, plan) {
21244
23185
  }
21245
23186
  }
21246
23187
  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) {
23188
+ const certainlyNew2 = Math.max(0, creates - plan.untracked.length);
23189
+ if (server.rooms.length + certainlyNew2 > COMMUNITY_ROOMS_MAX) {
21249
23190
  problems2.push(
21250
23191
  `rooms: a community holds at most ${COMMUNITY_ROOMS_MAX} rooms, archived ones included; it has ${server.rooms.length}, and the files create ${creates}`
21251
23192
  );
21252
23193
  }
23194
+ problems2.push(...courseCapProblems(files, plan.courses));
21253
23195
  return problems2;
21254
23196
  }
21255
23197
  function settingsPatch(files, server) {
@@ -21289,7 +23231,18 @@ function takeJoinLink(files, enabled) {
21289
23231
  files.join_link_enabled = enabled;
21290
23232
  files.join_link_rev = joinLinkRev(files.community_id, { enabled });
21291
23233
  }
21292
- function pushWrites(files, plan, remote, save) {
23234
+ function pushPhases(files, plan, remote, save, courses) {
23235
+ return [
23236
+ courses.unpublishWrites(),
23237
+ communityWrites(files, plan, remote, save),
23238
+ courses.courseWrites(),
23239
+ courses.moduleWrites(),
23240
+ courses.lessonWrites(),
23241
+ { planned: courses.plannedPlaces, later: () => courses.placeWrites() },
23242
+ { planned: courses.publishWrites().length, later: () => courses.publishWrites() }
23243
+ ];
23244
+ }
23245
+ function communityWrites(files, plan, remote, save) {
21293
23246
  const writes = [];
21294
23247
  if (plan.settings.state === "local") {
21295
23248
  const body = settingsPatch(files, plan.settings.server);
@@ -21379,8 +23332,8 @@ function refreshConverged(files, plan) {
21379
23332
  }
21380
23333
  return refreshed;
21381
23334
  }
21382
- function pushListing(plan) {
21383
- const listing = { changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] };
23335
+ function pushListing(plan, courses) {
23336
+ const listing = emptyListing();
21384
23337
  const { settings, joinLink } = plan;
21385
23338
  if (settings.state === "local") {
21386
23339
  listing.changes.push(`~ settings: ${settingsChanges(settings.server, settings.file).join("; ")}`);
@@ -21405,6 +23358,7 @@ function pushListing(plan) {
21405
23358
  listing.serverChanged = named("remote");
21406
23359
  listing.conflicts = named("conflict");
21407
23360
  listing.leftAsIs = plan.untracked.map((room) => `room ${show(room.name)} (${room.room_id})`);
23361
+ courses.list(listing);
21408
23362
  return listing;
21409
23363
  }
21410
23364
  async function pushCommunity(gitRootOrNull, selector, args2) {
@@ -21423,37 +23377,39 @@ async function pushCommunity(gitRootOrNull, selector, args2) {
21423
23377
  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
23378
  }
21425
23379
  const { organization_id: orgId } = requireRepoConfig(gitRoot);
21426
- const label = path25.basename(folder);
23380
+ const label = path27.basename(folder);
21427
23381
  assertInScope("communities", files.community_id, label);
23382
+ warnUnlisted(files);
21428
23383
  const remote = await openRemote(files.community_id);
21429
23384
  const server = await remote.read(orgId);
21430
23385
  const plan = planSync(files, server);
21431
- console.log(`Community ${show(server.community.name)} (${server.community.community_id}) \u2014 ${path25.relative(gitRoot, folder)}/`);
23386
+ console.log(`Community ${show(server.community.name)} (${server.community.community_id}) \u2014 ${path27.relative(gitRoot, folder)}/`);
21432
23387
  const problems2 = pushProblems(files, server, plan);
21433
23388
  if (problems2.length > 0) {
21434
23389
  throw expected(`Nothing was pushed:
21435
23390
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
21436
23391
  }
21437
- const listing = pushListing(plan);
23392
+ const { communitiesDir } = resolveLayout(gitRoot);
23393
+ const save = () => {
23394
+ writeCommunityFiles(communitiesDir, folder, files);
23395
+ };
23396
+ const courses = new CoursePush(files, plan.courses, remote, courseSaver(communitiesDir, folder, save));
23397
+ const listing = pushListing(plan, courses);
21438
23398
  printListing(listing);
21439
23399
  if (listing.conflicts.length > 0) {
21440
23400
  throw expected(`
21441
23401
  Nothing was pushed: ${listing.conflicts.length} conflict(s). ${resolveConflicts(folder)}`);
21442
23402
  }
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);
23403
+ const phases = pushPhases(files, plan, remote, save, courses);
23404
+ const requests = phases.reduce((sum, phase) => sum + plannedRequests(phase), 0);
23405
+ if (requests === 0) {
23406
+ const refreshed = dryRun ? 0 : refreshConverged(files, plan) + courses.refreshConverged();
21450
23407
  if (refreshed > 0) save();
21451
23408
  console.log(`
21452
- No changes to push${refreshed > 0 ? ` (${refreshed} rev(s) refreshed in ${COMMUNITY_FILE})` : ""}.`);
23409
+ No changes to push${refreshed > 0 ? ` (${refreshed} rev(s) refreshed)` : ""}.`);
21453
23410
  if (!dryRun) seedScopeIfEmpty("communities", files.community_id);
21454
23411
  return;
21455
23412
  }
21456
- const requests = writes.reduce((sum, write) => sum + write.requests, 0);
21457
23413
  console.log(
21458
23414
  `
21459
23415
  This push makes ${requests} request(s) to the live community \u2014 ${paceDuration(requests)} at ${COMMUNITY_REQUESTS_PER_MINUTE} a minute.`
@@ -21475,24 +23431,67 @@ This push makes ${requests} request(s) to the live community \u2014 ${paceDurati
21475
23431
  }
21476
23432
  }
21477
23433
  refreshConverged(files, plan);
23434
+ courses.refreshConverged();
21478
23435
  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
- );
23436
+ let total = phases.reduce((sum, phase) => sum + plannedCount(phase), 0);
23437
+ let done = 0;
23438
+ const stop = stopAfterWrite();
23439
+ try {
23440
+ for (const phase of phases) {
23441
+ const writes = Array.isArray(phase) ? phase : phase.later();
23442
+ total += writes.length - plannedCount(phase);
23443
+ for (const write of writes) {
23444
+ if (stop.requested()) {
23445
+ throw expected(
23446
+ `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.`
23447
+ );
23448
+ }
23449
+ try {
23450
+ await write.apply();
23451
+ } catch (err) {
23452
+ throw expected(
23453
+ `Push stopped at ${write.label}: ${stopReason(err)}
23454
+ What it applied before stopping is on the community, and the files record it. Fix the cause and push again.`
23455
+ );
23456
+ }
23457
+ console.log(` [${++done}/${total}] ${write.label}`);
23458
+ }
21487
23459
  }
21488
- console.log(` [${index + 1}/${writes.length}] ${write.label}`);
23460
+ } finally {
23461
+ stop.release();
21489
23462
  }
21490
23463
  console.log(
21491
23464
  `
21492
- Pushed ${writes.length} change(s). Commit ${path25.relative(gitRoot, folder)}/: ${COMMUNITY_FILE} now records the ids and revs this push wrote.`
23465
+ Pushed ${done} change(s). Commit ${path27.relative(gitRoot, folder)}/: the files now record the ids and revs this push wrote.`
21493
23466
  );
21494
23467
  seedScopeIfEmpty("communities", files.community_id);
21495
23468
  }
23469
+ function stopAfterWrite() {
23470
+ let asked = false;
23471
+ const onSignal = (signal) => {
23472
+ if (asked) process.exit(SIGNAL_EXIT_CODE[signal]);
23473
+ asked = true;
23474
+ console.error("\n Stopping after the write in progress, once the files record it. Press Ctrl-C again to leave at once.");
23475
+ };
23476
+ for (const signal of HANDLED_SIGNALS) process.on(signal, onSignal);
23477
+ return {
23478
+ requested: () => asked,
23479
+ release: () => {
23480
+ for (const signal of HANDLED_SIGNALS) process.off(signal, onSignal);
23481
+ }
23482
+ };
23483
+ }
23484
+ function courseSaver(root, folder, community) {
23485
+ return {
23486
+ community,
23487
+ course: (course) => {
23488
+ writeCourseYaml(root, folder, course);
23489
+ },
23490
+ lesson: (course, lesson) => {
23491
+ writeLessonFile(root, folder, course, lesson);
23492
+ }
23493
+ };
23494
+ }
21496
23495
  function changedSince(before, after) {
21497
23496
  const id = before.community.community_id;
21498
23497
  const changed = [];
@@ -21504,6 +23503,7 @@ function changedSince(before, after) {
21504
23503
  for (const room of after.rooms) {
21505
23504
  if (revs.get(room.room_id) !== serverRoom(room).rev) changed.push(`room ${show(room.name)}`);
21506
23505
  }
23506
+ changed.push(...coursesChangedSince(before.courses, after.courses));
21507
23507
  return changed;
21508
23508
  }
21509
23509
  function stopReason(err) {
@@ -21527,15 +23527,18 @@ function filesFromServer(server) {
21527
23527
  settings: { slow_mode_seconds: 0, courses_label: "" },
21528
23528
  rules: "",
21529
23529
  join_link_enabled: false,
21530
- rooms: []
23530
+ rooms: [],
23531
+ courses: [],
23532
+ unlistedCourses: []
21531
23533
  };
21532
23534
  takeSettings(files, server.settings);
21533
23535
  takeJoinLink(files, server.joinLink.enabled);
21534
23536
  addUntracked(files, server.rooms);
23537
+ coursesFromServer(files, server.courses);
21535
23538
  return files;
21536
23539
  }
21537
23540
  function pullMerge(files, server, plan, theirs) {
21538
- const listing = { changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] };
23541
+ const listing = emptyListing();
21539
23542
  const takes = (state) => state === "remote" || state === "conflict" && theirs;
21540
23543
  const note = (state, what) => {
21541
23544
  if (state === "local") listing.kept.push(what);
@@ -21573,6 +23576,7 @@ function pullMerge(files, server, plan, theirs) {
21573
23576
  }
21574
23577
  refreshConverged(files, plan);
21575
23578
  listing.changes.push(...addUntracked(files, plan.untracked));
23579
+ pullCourses(files, plan.courses, takes, note, listing);
21576
23580
  return listing;
21577
23581
  }
21578
23582
  async function pullCommunity(gitRootOrNull, selector, args2) {
@@ -21588,26 +23592,28 @@ async function pullCommunity(gitRootOrNull, selector, args2) {
21588
23592
  communityId = existing.files.community_id;
21589
23593
  }
21590
23594
  const { organization_id: orgId } = requireRepoConfig(gitRoot);
21591
- assertInScope("communities", communityId, existing ? path25.basename(existing.folder) : void 0);
23595
+ assertInScope("communities", communityId, existing ? path27.basename(existing.folder) : void 0);
21592
23596
  const { communitiesDir } = resolveLayout(gitRoot);
21593
23597
  const remote = await openRemote(communityId);
21594
23598
  const server = await remote.read(orgId);
21595
23599
  if (!existing) {
21596
23600
  const taken = new Set(safeReaddir2(communitiesDir));
21597
- const folder2 = path25.join(communitiesDir, localName(server.community.name, communityId, taken, "community"));
23601
+ const folder2 = path27.join(communitiesDir, localName(server.community.name, communityId, taken, "community"));
21598
23602
  const files = filesFromServer(server);
21599
- const where = `${path25.relative(gitRoot, folder2)}/`;
23603
+ const where = `${path27.relative(gitRoot, folder2)}/`;
23604
+ const counts = `${files.rooms.length} room(s), ${files.courses.length} course(s)`;
21600
23605
  if (dryRun) {
21601
- console.log(`Dry run \u2014 would write community ${show(server.community.name)} to ${where} with ${files.rooms.length} room(s).`);
23606
+ console.log(`Dry run \u2014 would write community ${show(server.community.name)} to ${where} with ${counts}.`);
21602
23607
  return;
21603
23608
  }
21604
23609
  requireRealSubdirNoSymlink(gitRoot, communitiesDir, true);
21605
23610
  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.`);
23611
+ 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
23612
  seedScopeIfEmpty("communities", communityId);
21608
23613
  return;
21609
23614
  }
21610
23615
  const { folder, files: local } = existing;
23616
+ warnUnlisted(local);
21611
23617
  const plan = planSync(local, server);
21612
23618
  const problems2 = unknownIdProblems(plan);
21613
23619
  if (problems2.length > 0) {
@@ -21615,7 +23621,7 @@ async function pullCommunity(gitRootOrNull, selector, args2) {
21615
23621
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
21616
23622
  }
21617
23623
  const listing = pullMerge(local, server, plan, theirs);
21618
- console.log(`Community ${show(server.community.name)} (${communityId}) \u2014 ${path25.relative(gitRoot, folder)}/`);
23624
+ console.log(`Community ${show(server.community.name)} (${communityId}) \u2014 ${path27.relative(gitRoot, folder)}/`);
21619
23625
  printListing(listing);
21620
23626
  if (dryRun) {
21621
23627
  console.log("\nDry run \u2014 nothing was written.");
@@ -21630,16 +23636,16 @@ Wrote ${written.join(", ")}.` : "\nAlready up to date.");
21630
23636
  }
21631
23637
  }
21632
23638
  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.`;
23639
+ 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
23640
  }
21635
23641
  function safeReaddir2(dir) {
21636
23642
  try {
21637
- return fs18.readdirSync(dir);
23643
+ return fs19.readdirSync(dir);
21638
23644
  } catch {
21639
23645
  return [];
21640
23646
  }
21641
23647
  }
21642
- var show, Remote, onOff;
23648
+ var Remote, onOff, plannedCount, plannedRequests;
21643
23649
  var init_sync2 = __esm({
21644
23650
  "src/communities/sync.ts"() {
21645
23651
  "use strict";
@@ -21655,11 +23661,14 @@ var init_sync2 = __esm({
21655
23661
  init_worktree_scope();
21656
23662
  init_community_workspace();
21657
23663
  init_helpers();
23664
+ init_eval_session_control();
21658
23665
  init_api2();
21659
23666
  init_pacer();
21660
23667
  init_files();
23668
+ init_course_files();
23669
+ init_courses();
23670
+ init_listing();
21661
23671
  init_model();
21662
- show = (text) => JSON.stringify(sanitizeTerminalText(text));
21663
23672
  Remote = class {
21664
23673
  constructor(api, pacer, communityId) {
21665
23674
  this.api = api;
@@ -21670,8 +23679,9 @@ var init_sync2 = __esm({
21670
23679
  return (await callCommunities(this.pacer.run(request), notFound)).data;
21671
23680
  }
21672
23681
  /**
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.
23682
+ * gh #5003 Decision 8's bounded read: the org's list, settings, rooms, the join link and the
23683
+ * courses, then each course whole — one request per course. The org's list both binds the
23684
+ * community to the workspace's organization and names it.
21675
23685
  */
21676
23686
  async read(orgId) {
21677
23687
  const communities = await this.send(() => this.api.listOrgCommunities(orgId), orgNotFound(orgId));
@@ -21681,12 +23691,14 @@ var init_sync2 = __esm({
21681
23691
  `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
23692
  );
21683
23693
  }
21684
- const [settings, rooms, joinLink] = await Promise.all([
23694
+ const [settings, rooms, joinLink, courses] = await Promise.all([
21685
23695
  this.send(() => this.api.getCommunitySettings(this.communityId)),
21686
23696
  this.send(() => this.api.listCommunityRooms(this.communityId)),
21687
- this.send(() => this.api.getCommunityJoinLink(this.communityId))
23697
+ this.send(() => this.api.getCommunityJoinLink(this.communityId)),
23698
+ // Each course whole, as soon as the list names it.
23699
+ this.listCourses().then((list) => Promise.all(list.map((course) => this.exportCourse(course.course_id))))
21688
23700
  ]);
21689
- return { community, settings, rooms, joinLink };
23701
+ return { community, settings, rooms, joinLink, courses: new ServerCourses(courses) };
21690
23702
  }
21691
23703
  updateSettings(body) {
21692
23704
  return this.send(() => this.api.updateCommunitySettings(this.communityId, body));
@@ -21701,8 +23713,34 @@ var init_sync2 = __esm({
21701
23713
  updateRoom(roomId, body) {
21702
23714
  return this.send(() => this.api.updateCommunityRoom(this.communityId, roomId, body));
21703
23715
  }
23716
+ createCourse(body) {
23717
+ return this.send(() => this.api.createCommunityCourse(this.communityId, body));
23718
+ }
23719
+ updateCourse(courseId, body) {
23720
+ return this.send(() => this.api.updateCommunityCourse(this.communityId, courseId, body));
23721
+ }
23722
+ createModule(courseId, body) {
23723
+ return this.send(() => this.api.createCommunityModule(this.communityId, courseId, body));
23724
+ }
23725
+ updateModule(courseId, moduleId, body) {
23726
+ return this.send(() => this.api.updateCommunityModule(this.communityId, courseId, moduleId, body));
23727
+ }
23728
+ createLesson(courseId, body) {
23729
+ return this.send(() => this.api.createCommunityLesson(this.communityId, courseId, body));
23730
+ }
23731
+ updateLesson(courseId, lessonId, body) {
23732
+ return this.send(() => this.api.updateCommunityLesson(this.communityId, courseId, lessonId, body));
23733
+ }
23734
+ listCourses() {
23735
+ return this.send(() => this.api.listCommunityCourseDrafts(this.communityId));
23736
+ }
23737
+ exportCourse(courseId) {
23738
+ return this.send(() => this.api.exportCommunityCourse(this.communityId, courseId));
23739
+ }
21704
23740
  };
21705
23741
  onOff = (enabled) => enabled ? "on" : "off";
23742
+ plannedCount = (phase) => Array.isArray(phase) ? phase.length : phase.planned;
23743
+ plannedRequests = (phase) => Array.isArray(phase) ? phase.reduce((n, write) => n + write.requests, 0) : phase.planned;
21706
23744
  }
21707
23745
  });
21708
23746
 
@@ -21721,9 +23759,9 @@ __export(push_exports, {
21721
23759
  shouldWarnIgnoredPreviewLabel: () => shouldWarnIgnoredPreviewLabel,
21722
23760
  syncAfterPush: () => syncAfterPush
21723
23761
  });
21724
- import * as fs19 from "fs";
21725
- import * as path26 from "path";
21726
- import * as yaml13 from "js-yaml";
23762
+ import * as fs20 from "fs";
23763
+ import * as path28 from "path";
23764
+ import * as yaml14 from "js-yaml";
21727
23765
  function parseArgs6(args2) {
21728
23766
  let autoConfirm = false;
21729
23767
  let label;
@@ -21828,14 +23866,14 @@ function printLocalFileChanges(delta) {
21828
23866
  }
21829
23867
  async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, remoteConfig) {
21830
23868
  requireRealHubFolder(hubFolder, false);
21831
- const agentsDir = path26.join(hubFolder, "agents");
23869
+ const agentsDir = path28.join(hubFolder, "agents");
21832
23870
  let agentsWithIds = [];
21833
- if (fs19.existsSync(agentsDir)) {
21834
- const yamlFiles = fs19.readdirSync(agentsDir).filter((f) => f.endsWith(".yaml"));
23871
+ if (fs20.existsSync(agentsDir)) {
23872
+ const yamlFiles = fs20.readdirSync(agentsDir).filter((f) => f.endsWith(".yaml"));
21835
23873
  for (const file of yamlFiles) {
21836
- const content = readRealFileOrThrow(hubFolder, path26.join(agentsDir, file)) ?? "";
23874
+ const content = readRealFileOrThrow(hubFolder, path28.join(agentsDir, file)) ?? "";
21837
23875
  try {
21838
- const agent = yaml13.load(content);
23876
+ const agent = yaml14.load(content);
21839
23877
  if (agent?.id && agent.name) {
21840
23878
  agentsWithIds.push({ id: agent.id, name: agent.name });
21841
23879
  }
@@ -21848,7 +23886,7 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21848
23886
  const yamlPath = resolveHubYamlPath(hubFolder);
21849
23887
  if (!yamlPath) return;
21850
23888
  const yamlContent = readRealFileOrThrow(hubFolder, yamlPath) ?? "";
21851
- const config = yaml13.load(yamlContent);
23889
+ const config = yaml14.load(yamlContent);
21852
23890
  agentsWithIds = (config.agents || []).filter((a) => !!a.id && !!a.name);
21853
23891
  }
21854
23892
  if (agentsWithIds.length === 0) return;
@@ -21879,18 +23917,18 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21879
23917
  }
21880
23918
  }
21881
23919
  if (renames.length === 0) return;
21882
- if (!fs19.existsSync(agentsDir)) return;
21883
- for (const file of fs19.readdirSync(agentsDir)) {
23920
+ if (!fs20.existsSync(agentsDir)) return;
23921
+ for (const file of fs20.readdirSync(agentsDir)) {
21884
23922
  if (file.startsWith("__rename_temp_") && (file.endsWith(".md") || file.endsWith(".yaml"))) {
21885
23923
  console.warn(` Warning: removing orphaned temp file agents/${file}`);
21886
- fs19.unlinkSync(path26.join(agentsDir, file));
23924
+ fs20.unlinkSync(path28.join(agentsDir, file));
21887
23925
  }
21888
23926
  }
21889
23927
  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);
23928
+ const oldPath = path28.join(dir, oldName);
23929
+ const newPath = path28.join(dir, newName);
23930
+ if (!fs20.existsSync(oldPath)) return false;
23931
+ fs20.renameSync(oldPath, newPath);
21894
23932
  return true;
21895
23933
  };
21896
23934
  const oldSlugs = new Set(renames.map((r) => r.oldSlug));
@@ -21923,9 +23961,9 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21923
23961
  }
21924
23962
  } else {
21925
23963
  for (const { oldSlug, newSlug } of renames) {
21926
- const hasOldFile = extensions.some((ext) => fs19.existsSync(path26.join(agentsDir, `${oldSlug}${ext}`)));
23964
+ const hasOldFile = extensions.some((ext) => fs20.existsSync(path28.join(agentsDir, `${oldSlug}${ext}`)));
21927
23965
  if (!hasOldFile) continue;
21928
- const hasNewFile = extensions.some((ext) => fs19.existsSync(path26.join(agentsDir, `${newSlug}${ext}`)));
23966
+ const hasNewFile = extensions.some((ext) => fs20.existsSync(path28.join(agentsDir, `${newSlug}${ext}`)));
21929
23967
  if (hasNewFile) {
21930
23968
  console.warn(` Warning: skipping rename agents/${oldSlug}.* \u2192 agents/${newSlug}.* (target already exists)`);
21931
23969
  continue;
@@ -21952,7 +23990,7 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21952
23990
  );
21953
23991
  if (updatedYaml !== mainYamlContent) {
21954
23992
  writeFileNoFollow(hubFolder, mainYamlPath, Buffer.from(updatedYaml, "utf-8"));
21955
- console.log(` Updated instructions paths in ${path26.basename(mainYamlPath)}`);
23993
+ console.log(` Updated instructions paths in ${path28.basename(mainYamlPath)}`);
21956
23994
  }
21957
23995
  }
21958
23996
  }
@@ -22020,7 +24058,7 @@ async function pushSingleHub(client, hubId, hubFolder, autoConfirm, organization
22020
24058
  function selectExistingHub(workspaceDir, allHubs, selector, wsLabel) {
22021
24059
  if (selector) {
22022
24060
  const match = allHubs.find(
22023
- (h) => h.hubId === selector || path26.basename(h.hubFolder) === selector
24061
+ (h) => h.hubId === selector || path28.basename(h.hubFolder) === selector
22024
24062
  );
22025
24063
  if (!match) {
22026
24064
  console.error(`No hub matching --hub ${selector} found in ${wsLabel}/.`);
@@ -22089,7 +24127,7 @@ function reportUnresolvedPushTarget(reason, existingHubs, newHubs, selector, wsL
22089
24127
  if (existingHubs.length > 0) {
22090
24128
  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
24129
  for (const h of existingHubs) {
22092
- console.error(` ${path26.basename(h.hubFolder)} (${h.hubId}, production mirror)`);
24130
+ console.error(` ${path28.basename(h.hubFolder)} (${h.hubId}, production mirror)`);
22093
24131
  }
22094
24132
  } else {
22095
24133
  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 +24141,10 @@ function reportUnresolvedPushTarget(reason, existingHubs, newHubs, selector, wsL
22103
24141
  console.error(`Multiple hub folders found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22104
24142
  }
22105
24143
  for (const h of existingHubs) {
22106
- console.error(` ${path26.basename(h.hubFolder)} (${h.hubId})`);
24144
+ console.error(` ${path28.basename(h.hubFolder)} (${h.hubId})`);
22107
24145
  }
22108
24146
  for (const h of newHubs) {
22109
- console.error(` ${path26.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
24147
+ console.error(` ${path28.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
22110
24148
  }
22111
24149
  if (hint) console.error(hint);
22112
24150
  process.exit(1);
@@ -22133,7 +24171,7 @@ async function pushCommand(args2) {
22133
24171
  const client = new ApiClient({ apiUrl: config.api_url, accessToken });
22134
24172
  const workspaceDir = resolveWorkspaceDir();
22135
24173
  const wsLabel = hubsDirLabel(gitRoot);
22136
- if (!fs19.existsSync(workspaceDir)) {
24174
+ if (!fs20.existsSync(workspaceDir)) {
22137
24175
  console.error(`No ${wsLabel}/ directory found. Run \`wayai pull\` first or create hub files in ${wsLabel}/<hub>/hub.yaml.`);
22138
24176
  const hint = communityFoldersHint(gitRoot, "push");
22139
24177
  if (hint) console.error(hint);
@@ -22155,8 +24193,8 @@ async function pushCommand(args2) {
22155
24193
  if (label) {
22156
24194
  console.warn("Note: --label is only used when creating a new hub. To change an existing hub's label, use `wayai relabel`.");
22157
24195
  }
22158
- console.log(`Target hub: ${path26.basename(existing.hubFolder)} (${existing.hubId})`);
22159
- assertInScope("hubs", existing.hubId, path26.basename(existing.hubFolder));
24196
+ console.log(`Target hub: ${path28.basename(existing.hubFolder)} (${existing.hubId})`);
24197
+ assertInScope("hubs", existing.hubId, path28.basename(existing.hubFolder));
22160
24198
  await pushSingleHub(client, existing.hubId, existing.hubFolder, autoConfirm, organizationId);
22161
24199
  return;
22162
24200
  }
@@ -22194,8 +24232,8 @@ __export(pull_exports, {
22194
24232
  resolveHubTarget: () => resolveHubTarget,
22195
24233
  writeProductionMirror: () => writeProductionMirror2
22196
24234
  });
22197
- import * as fs20 from "fs";
22198
- import * as path27 from "path";
24235
+ import * as fs21 from "fs";
24236
+ import * as path29 from "path";
22199
24237
  function parseArgs7(args2) {
22200
24238
  return { autoConfirm: args2.includes("--yes") || args2.includes("-y") };
22201
24239
  }
@@ -22205,7 +24243,7 @@ function resolveHubTarget(workspaceDir, selector) {
22205
24243
  const allHubs = scanWorkspaceHubs(workspaceDir);
22206
24244
  if (selector) {
22207
24245
  const match = allHubs.find(
22208
- (h) => h.hubId === selector || path27.basename(h.hubFolder) === selector
24246
+ (h) => h.hubId === selector || path29.basename(h.hubFolder) === selector
22209
24247
  );
22210
24248
  if (match) return { hubId: match.hubId, hubFolder: match.hubFolder };
22211
24249
  if (UUID_RE3.test(selector)) return { hubId: selector, hubFolder: null };
@@ -22224,7 +24262,7 @@ function resolveHubTarget(workspaceDir, selector) {
22224
24262
  }
22225
24263
  console.error(`Multiple hubs found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22226
24264
  for (const h of previewHubs) {
22227
- console.error(` ${path27.basename(h.hubFolder)} (${h.hubId})`);
24265
+ console.error(` ${path29.basename(h.hubFolder)} (${h.hubId})`);
22228
24266
  }
22229
24267
  process.exit(1);
22230
24268
  }
@@ -22273,7 +24311,7 @@ async function pullCommand(args2) {
22273
24311
  );
22274
24312
  let wroteFiles = false;
22275
24313
  if (!existingFolder && !resolveHubYamlPath(hubFolder)) {
22276
- fs20.mkdirSync(path27.dirname(hubFolder), { recursive: true });
24314
+ fs21.mkdirSync(path29.dirname(hubFolder), { recursive: true });
22277
24315
  console.log("Writing hub configuration...");
22278
24316
  await materializeHubFolder(hubFolder, payload);
22279
24317
  const finalFolder = autoRenameHubFolder(hubFolder, payload.hub.name, payload.hub_environment, payload.hub_id, payload.preview_label, payload.branch_name);
@@ -22358,7 +24396,7 @@ ${why}`);
22358
24396
  }
22359
24397
  async function writeProductionMirror2(workspaceDir, prodPayload) {
22360
24398
  const folder = resolveHubFolder(workspaceDir, prodPayload.hub_id, prodPayload.hub.name, "production", null, null);
22361
- fs20.mkdirSync(path27.dirname(folder), { recursive: true });
24399
+ fs21.mkdirSync(path29.dirname(folder), { recursive: true });
22362
24400
  await materializeHubFolder(folder, prodPayload, { seedAgentContext: false });
22363
24401
  const finalFolder = autoRenameHubFolder(folder, prodPayload.hub.name, "production", prodPayload.hub_id, null, null);
22364
24402
  prependMirrorMarker(finalFolder, prodPayload.hub_id);
@@ -22374,13 +24412,13 @@ async function mirrorLinkedProduction2(client, workspaceDir, productionHubId, or
22374
24412
  }
22375
24413
  }
22376
24414
  function prependMirrorMarker(hubFolder, productionHubId) {
22377
- const hubYaml = path27.join(hubFolder, "hub.yaml");
24415
+ const hubYaml = path29.join(hubFolder, "hub.yaml");
22378
24416
  try {
22379
- const content = fs20.readFileSync(hubYaml, "utf-8");
24417
+ const content = fs21.readFileSync(hubYaml, "utf-8");
22380
24418
  if (content.startsWith(MIRROR_MARKER_PREFIX2)) return;
22381
24419
  const marker = `${MIRROR_MARKER_PREFIX2} ${productionHubId}. Edits are ignored; push is blocked. Edit the linked preview hub instead.
22382
24420
  `;
22383
- fs20.writeFileSync(hubYaml, marker + content, "utf-8");
24421
+ fs21.writeFileSync(hubYaml, marker + content, "utf-8");
22384
24422
  } catch {
22385
24423
  }
22386
24424
  }
@@ -22428,8 +24466,8 @@ var create_exports = {};
22428
24466
  __export(create_exports, {
22429
24467
  createCommand: () => createCommand
22430
24468
  });
22431
- import * as path28 from "path";
22432
- import * as fs21 from "fs";
24469
+ import * as path30 from "path";
24470
+ import * as fs22 from "fs";
22433
24471
  function parseArgs8(args2) {
22434
24472
  let autoConfirm = false;
22435
24473
  let folderSelector;
@@ -22456,7 +24494,7 @@ async function createCommand(args2) {
22456
24494
  const gitRoot = findGitRoot();
22457
24495
  const wsLabel = hubsDirLabel(gitRoot);
22458
24496
  if (gitRoot) warnLayoutOnce(gitRoot);
22459
- if (!fs21.existsSync(workspaceDir)) {
24497
+ if (!fs22.existsSync(workspaceDir)) {
22460
24498
  console.error(`No ${wsLabel}/ directory found. Create hub files in ${wsLabel}/<hub>/hub.yaml with \`hub: { name: ... }\` first.`);
22461
24499
  process.exit(1);
22462
24500
  }
@@ -22465,7 +24503,7 @@ async function createCommand(args2) {
22465
24503
  const resolution = resolveNewHubForCreate(workspaceDir, existingHubs, newHubs, folderSelector);
22466
24504
  if (!resolution.ok) {
22467
24505
  if (resolution.reason === "exists") {
22468
- const folder = path28.basename(resolution.existing.hubFolder);
24506
+ const folder = path30.basename(resolution.existing.hubFolder);
22469
24507
  console.error(`Hub "${folder}" already exists (${resolution.existing.hubId}). Use \`wayai push --hub ${folder}\` to update it.`);
22470
24508
  process.exit(1);
22471
24509
  }
@@ -22479,7 +24517,7 @@ async function createCommand(args2) {
22479
24517
  }
22480
24518
  console.error(`Multiple new hub folders found in ${wsLabel}/. Pass the folder name (\`wayai create <folder>\`) or run from inside one:`);
22481
24519
  for (const h of newHubs) {
22482
- console.error(` ${path28.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
24520
+ console.error(` ${path30.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
22483
24521
  }
22484
24522
  process.exit(1);
22485
24523
  }
@@ -22590,8 +24628,8 @@ var replicate_exports = {};
22590
24628
  __export(replicate_exports, {
22591
24629
  replicateCommand: () => replicateCommand
22592
24630
  });
22593
- import * as fs22 from "fs";
22594
- import * as path29 from "path";
24631
+ import * as fs23 from "fs";
24632
+ import * as path31 from "path";
22595
24633
  function parseArgs10(args2) {
22596
24634
  let label;
22597
24635
  let hubSelector;
@@ -22637,8 +24675,8 @@ async function replicateCommand(args2) {
22637
24675
  payload.preview_label,
22638
24676
  payload.branch_name
22639
24677
  );
22640
- const folderPreExisted = fs22.existsSync(hubFolder);
22641
- fs22.mkdirSync(path29.dirname(hubFolder), { recursive: true });
24678
+ const folderPreExisted = fs23.existsSync(hubFolder);
24679
+ fs23.mkdirSync(path31.dirname(hubFolder), { recursive: true });
22642
24680
  const delta = await materializeHubFolder(hubFolder, payload);
22643
24681
  hubFolder = autoRenameHubFolder(
22644
24682
  hubFolder,
@@ -22650,8 +24688,8 @@ async function replicateCommand(args2) {
22650
24688
  );
22651
24689
  seedScopeIfEmpty("hubs", previewHubId);
22652
24690
  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)}`);
24691
+ console.log(`Preview written to ${path31.basename(hubFolder)}`);
24692
+ console.log(` Switch to it with: wayai use ${path31.basename(hubFolder)}`);
22655
24693
  }
22656
24694
  var init_replicate = __esm({
22657
24695
  "src/commands/replicate.ts"() {
@@ -22674,7 +24712,7 @@ var relabel_exports = {};
22674
24712
  __export(relabel_exports, {
22675
24713
  relabelCommand: () => relabelCommand
22676
24714
  });
22677
- import * as path30 from "path";
24715
+ import * as path32 from "path";
22678
24716
  function parseArgs11(args2) {
22679
24717
  let label;
22680
24718
  let clear = false;
@@ -22718,7 +24756,7 @@ async function relabelCommand(args2) {
22718
24756
  } else {
22719
24757
  console.error(`Multiple hubs found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22720
24758
  for (const h of previewHubs) {
22721
- console.error(` ${path30.basename(h.hubFolder)} (${h.hubId})`);
24759
+ console.error(` ${path32.basename(h.hubFolder)} (${h.hubId})`);
22722
24760
  }
22723
24761
  }
22724
24762
  process.exit(1);
@@ -22727,7 +24765,7 @@ async function relabelCommand(args2) {
22727
24765
  console.log("This is a read-only production mirror \u2014 production hubs have no preview label. Relabel the linked preview hub instead.");
22728
24766
  return;
22729
24767
  }
22730
- assertInScope("hubs", target.hubId, path30.basename(target.hubFolder));
24768
+ assertInScope("hubs", target.hubId, path32.basename(target.hubFolder));
22731
24769
  console.log(normalizedLabel ? `Setting preview label to "${normalizedLabel}"...` : "Clearing preview label...");
22732
24770
  const { data } = await client.relabelPreview(target.hubId, normalizedLabel);
22733
24771
  const row = data[0];
@@ -22743,7 +24781,7 @@ async function relabelCommand(args2) {
22743
24781
  );
22744
24782
  console.log(serverLabel ? `Preview label set to "${serverLabel}".` : "Preview label cleared.");
22745
24783
  if (finalFolder !== target.hubFolder) {
22746
- console.log(`Hub folder is now ${path30.basename(finalFolder)}`);
24784
+ console.log(`Hub folder is now ${path32.basename(finalFolder)}`);
22747
24785
  }
22748
24786
  }
22749
24787
  var init_relabel = __esm({
@@ -22769,7 +24807,7 @@ __export(publish_exports, {
22769
24807
  renderHubDiff: () => renderHubDiff,
22770
24808
  runPublish: () => runPublish
22771
24809
  });
22772
- import * as path31 from "path";
24810
+ import * as path33 from "path";
22773
24811
  function parseArgs12(args2) {
22774
24812
  let autoConfirm = false;
22775
24813
  let hubSelector;
@@ -22878,7 +24916,7 @@ async function publishCommand(args2) {
22878
24916
  return;
22879
24917
  }
22880
24918
  }
22881
- assertInScope("hubs", hubId, hubFolder ? path31.basename(hubFolder) : void 0);
24919
+ assertInScope("hubs", hubId, hubFolder ? path33.basename(hubFolder) : void 0);
22882
24920
  await runPublish(client, { hubId, localConfig, organizationId, autoConfirm });
22883
24921
  }
22884
24922
  var STATUS_STYLE, SECTION_TITLES;
@@ -22926,7 +24964,7 @@ var init_publish = __esm({
22926
24964
  });
22927
24965
 
22928
24966
  // src/lib/scope-selector.ts
22929
- import * as path32 from "path";
24967
+ import * as path34 from "path";
22930
24968
  function findAddFlag(args2) {
22931
24969
  return args2.find((arg) => arg === "--add" || arg.startsWith("--add="));
22932
24970
  }
@@ -22983,7 +25021,7 @@ function resolveHubSelectorToId(gitRoot, selector) {
22983
25021
  const match = findHubByFolderName(workspaceDir, selector);
22984
25022
  if (match) return match.hubId;
22985
25023
  const newHubs = scanNewHubs(workspaceDir);
22986
- const pending = newHubs.find((h) => path32.basename(h.hubFolder) === selector);
25024
+ const pending = newHubs.find((h) => path34.basename(h.hubFolder) === selector);
22987
25025
  if (pending) {
22988
25026
  throw expected(
22989
25027
  `"${selector}" is a new hub that hasn't been created on the platform yet, so it has no id to bind to.
@@ -22994,9 +25032,9 @@ Create it first \u2014 the worktree scope picks it up automatically afterward:
22994
25032
  throw expected(
22995
25033
  [
22996
25034
  `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})`),
25035
+ ...scanWorkspaceHubs(workspaceDir).map((h) => ` ${path34.basename(h.hubFolder)} (${h.hubId})`),
22998
25036
  ...newHubs.map(
22999
- (h) => ` ${path32.basename(h.hubFolder)} (new \u2014 "${h.hubName}", run \`wayai create ${path32.basename(h.hubFolder)}\`)`
25037
+ (h) => ` ${path34.basename(h.hubFolder)} (new \u2014 "${h.hubName}", run \`wayai create ${path34.basename(h.hubFolder)}\`)`
23000
25038
  )
23001
25039
  ].join("\n")
23002
25040
  );
@@ -23086,11 +25124,11 @@ __export(migrate_exports, {
23086
25124
  migrateCommand: () => migrateCommand
23087
25125
  });
23088
25126
  import { execFileSync as execFileSync3 } from "child_process";
23089
- import * as fs23 from "fs";
23090
- import * as path33 from "path";
25127
+ import * as fs24 from "fs";
25128
+ import * as path35 from "path";
23091
25129
  function isTracked(gitRoot, p) {
23092
25130
  try {
23093
- execFileSync3("git", ["ls-files", "--error-unmatch", "--", path33.relative(gitRoot, p)], {
25131
+ execFileSync3("git", ["ls-files", "--error-unmatch", "--", path35.relative(gitRoot, p)], {
23094
25132
  cwd: gitRoot,
23095
25133
  stdio: ["pipe", "pipe", "pipe"]
23096
25134
  });
@@ -23100,10 +25138,10 @@ function isTracked(gitRoot, p) {
23100
25138
  }
23101
25139
  }
23102
25140
  function moveDir(gitRoot, from, to) {
23103
- fs23.mkdirSync(path33.dirname(to), { recursive: true });
25141
+ fs24.mkdirSync(path35.dirname(to), { recursive: true });
23104
25142
  if (isTracked(gitRoot, from)) {
23105
25143
  try {
23106
- execFileSync3("git", ["mv", path33.relative(gitRoot, from), path33.relative(gitRoot, to)], {
25144
+ execFileSync3("git", ["mv", path35.relative(gitRoot, from), path35.relative(gitRoot, to)], {
23107
25145
  cwd: gitRoot,
23108
25146
  stdio: ["pipe", "pipe", "pipe"]
23109
25147
  });
@@ -23111,7 +25149,7 @@ function moveDir(gitRoot, from, to) {
23111
25149
  } catch {
23112
25150
  }
23113
25151
  }
23114
- fs23.renameSync(from, to);
25152
+ fs24.renameSync(from, to);
23115
25153
  return "fs";
23116
25154
  }
23117
25155
  async function migrateCommand(_args) {
@@ -23120,12 +25158,12 @@ async function migrateCommand(_args) {
23120
25158
  console.error("Not inside a git repository.");
23121
25159
  process.exit(1);
23122
25160
  }
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);
25161
+ const rel = (p) => path35.relative(gitRoot, p);
25162
+ const newWs = path35.join(gitRoot, WAYAI_LAYOUT.wsDir);
25163
+ const legacyWs = path35.join(gitRoot, WAYAI_LAYOUT.legacy.wsDir);
25164
+ const legacyOrg = path35.join(gitRoot, WAYAI_LAYOUT.legacy.orgAtRoot);
25165
+ const newHubs = path35.join(newWs, WAYAI_LAYOUT.hubsSubdir);
25166
+ const newOrg = path35.join(newWs, WAYAI_LAYOUT.orgSubdir);
23129
25167
  requireRealSubdirNoSymlink(gitRoot, newHubs, false);
23130
25168
  requireRealSubdirNoSymlink(gitRoot, newOrg, false);
23131
25169
  readRealFileOrThrow(gitRoot, workspaceManifestPath(gitRoot));
@@ -23278,12 +25316,12 @@ var send_message_exports = {};
23278
25316
  __export(send_message_exports, {
23279
25317
  sendMessageCommand: () => sendMessageCommand
23280
25318
  });
23281
- import * as fs24 from "fs";
23282
- import * as path34 from "path";
25319
+ import * as fs25 from "fs";
25320
+ import * as path36 from "path";
23283
25321
  function statAttachment(filePath) {
23284
25322
  let stat2;
23285
25323
  try {
23286
- stat2 = fs24.statSync(filePath);
25324
+ stat2 = fs25.statSync(filePath);
23287
25325
  } catch {
23288
25326
  console.error(`Error: file not found: ${filePath}`);
23289
25327
  process.exit(1);
@@ -23295,11 +25333,11 @@ function statAttachment(filePath) {
23295
25333
  return { filePath, size: stat2.size };
23296
25334
  }
23297
25335
  function readAttachment(filePath, size) {
23298
- const fileName = path34.basename(filePath);
23299
- const ext = path34.extname(fileName).replace(/^\./, "");
25336
+ const fileName = path36.basename(filePath);
25337
+ const ext = path36.extname(fileName).replace(/^\./, "");
23300
25338
  return {
23301
25339
  file_name: fileName,
23302
- file_binary: fs24.readFileSync(filePath).toString("base64"),
25340
+ file_binary: fs25.readFileSync(filePath).toString("base64"),
23303
25341
  file_size: size,
23304
25342
  ...ext && { file_extension: ext }
23305
25343
  };
@@ -23346,7 +25384,7 @@ async function sendMessageCommand(args2) {
23346
25384
  }
23347
25385
  const stats = filePaths.map(statAttachment);
23348
25386
  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(", ");
25387
+ const fileDetail = () => stats.map((s) => `${path36.basename(s.filePath)} ${asMb(s.size)} MB`).join(", ");
23350
25388
  if (filesTotal > MAX_MESSAGE_BODY_BYTES) {
23351
25389
  console.error(
23352
25390
  `Error: ${fileDetail()} encode to ~${asMb(filesTotal)} MB, over the ${asMb(MAX_MESSAGE_BODY_BYTES)} MB per-request limit.`
@@ -23354,10 +25392,10 @@ async function sendMessageCommand(args2) {
23354
25392
  console.error("Base64 inflates a file by about a third, so ~7 MB of files is the practical ceiling.");
23355
25393
  process.exit(1);
23356
25394
  }
23357
- const audioStats = stats.filter((s) => isAudioAttachmentFileName(path34.basename(s.filePath)));
25395
+ const audioStats = stats.filter((s) => isAudioAttachmentFileName(path36.basename(s.filePath)));
23358
25396
  if (audioStats.length > 1) {
23359
25397
  console.error(
23360
- `Error: at most one audio file per message (got ${audioStats.length}: ${audioStats.map((s) => path34.basename(s.filePath)).join(", ")}).`
25398
+ `Error: at most one audio file per message (got ${audioStats.length}: ${audioStats.map((s) => path36.basename(s.filePath)).join(", ")}).`
23361
25399
  );
23362
25400
  console.error("Audio is transcribed via the hub's STT connection, and only one file per message is transcribed.");
23363
25401
  process.exit(1);
@@ -23365,7 +25403,7 @@ async function sendMessageCommand(args2) {
23365
25403
  const oversizeAudio = audioStats.find((s) => base64Length(s.size) > MAX_AUDIO_FILE_BASE64_BYTES);
23366
25404
  if (oversizeAudio) {
23367
25405
  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.`
25406
+ `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
25407
  );
23370
25408
  process.exit(1);
23371
25409
  }
@@ -24841,133 +26879,6 @@ var init_evals = __esm({
24841
26879
  }
24842
26880
  });
24843
26881
 
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
26882
  // src/lib/eval-fixture-queue.ts
24972
26883
  function isQueueableLaunchError(err) {
24973
26884
  const code = extractApiErrorCode(err);
@@ -25520,7 +27431,7 @@ var init_eval_results = __esm({
25520
27431
  import { createHash as createHash4 } from "crypto";
25521
27432
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync17, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
25522
27433
  import { homedir as homedir5 } from "os";
25523
- import { dirname as dirname13, isAbsolute as isAbsolute2, join as join30, resolve as resolve10 } from "path";
27434
+ import { dirname as dirname12, isAbsolute as isAbsolute2, join as join31, resolve as resolve10 } from "path";
25524
27435
  function parseWav(buf, name = "clip") {
25525
27436
  if (buf.length < 12 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
25526
27437
  throw new Error(`${name} is not a WAV file.`);
@@ -25559,7 +27470,7 @@ function parseWav(buf, name = "clip") {
25559
27470
  throw new Error(`${name} holds no audio data.`);
25560
27471
  }
25561
27472
  function defaultClipCacheDir() {
25562
- return join30(homedir5(), ".wayai", "call-clips");
27473
+ return join31(homedir5(), ".wayai", "call-clips");
25563
27474
  }
25564
27475
  function pcmClip(buf) {
25565
27476
  const samples = new Int16Array(Math.floor(buf.length / 2));
@@ -25608,15 +27519,15 @@ var init_caller_audio = __esm({
25608
27519
  }
25609
27520
  describe;
25610
27521
  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.`);
27522
+ const path41 = line.clip ? isAbsolute2(line.clip) ? line.clip : resolve10(this.planDir, line.clip) : this.dir ? join31(this.dir, `${line.n}.wav`) : null;
27523
+ if (!path41) throw new Error(`Line ${line.n} has no clip: give --clips <dir>, or a "clip" for it in the plan.`);
25613
27524
  let buf;
25614
27525
  try {
25615
- buf = readFileSync17(path39);
27526
+ buf = readFileSync17(path41);
25616
27527
  } catch {
25617
- throw new Error(`Line ${line.n}'s clip ${path39} cannot be read.`);
27528
+ throw new Error(`Line ${line.n}'s clip ${path41} cannot be read.`);
25618
27529
  }
25619
- return parseWav(buf, path39);
27530
+ return parseWav(buf, path41);
25620
27531
  }
25621
27532
  };
25622
27533
  OPENAI_PCM_SAMPLE_RATE = 24e3;
@@ -25643,20 +27554,20 @@ var init_caller_audio = __esm({
25643
27554
  const key = createHash4("sha256").update(`${this.model}
25644
27555
  ${this.voice}
25645
27556
  ${text}`).digest("hex");
25646
- return join30(this.cacheDir, `${key}.pcm`);
27557
+ return join31(this.cacheDir, `${key}.pcm`);
25647
27558
  }
25648
27559
  clipFor(line) {
25649
- const path39 = this.cachePath(line.say);
25650
- let pending = this.inFlight.get(path39);
27560
+ const path41 = this.cachePath(line.say);
27561
+ let pending = this.inFlight.get(path41);
25651
27562
  if (!pending) {
25652
- pending = this.readOrSynthesize(line, path39);
25653
- this.inFlight.set(path39, pending);
27563
+ pending = this.readOrSynthesize(line, path41);
27564
+ this.inFlight.set(path41, pending);
25654
27565
  }
25655
27566
  return pending;
25656
27567
  }
25657
- async readOrSynthesize(line, path39) {
27568
+ async readOrSynthesize(line, path41) {
25658
27569
  try {
25659
- return pcmClip(readFileSync17(path39));
27570
+ return pcmClip(readFileSync17(path41));
25660
27571
  } catch {
25661
27572
  }
25662
27573
  const res = await this.doFetch("https://api.openai.com/v1/audio/speech", {
@@ -25670,10 +27581,10 @@ ${text}`).digest("hex");
25670
27581
  }
25671
27582
  const buf = Buffer.from(await res.arrayBuffer());
25672
27583
  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`;
27584
+ mkdirSync12(dirname12(path41), { recursive: true });
27585
+ const tmp = `${path41}.${process.pid}.tmp`;
25675
27586
  writeFileSync7(tmp, buf);
25676
- renameSync5(tmp, path39);
27587
+ renameSync5(tmp, path41);
25677
27588
  return pcmClip(buf);
25678
27589
  }
25679
27590
  };
@@ -27163,7 +29074,7 @@ __export(eval_call_exports, {
27163
29074
  suiteExitCode: () => suiteExitCode
27164
29075
  });
27165
29076
  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";
29077
+ import { dirname as dirname13, resolve as resolve11 } from "path";
27167
29078
  function fail(message) {
27168
29079
  throw new Error(message);
27169
29080
  }
@@ -27238,15 +29149,15 @@ function parseEvalCallArgs(args2) {
27238
29149
  if (parsed.jsonPath) checkReportPath(parsed.jsonPath);
27239
29150
  return parsed;
27240
29151
  }
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.`);
29152
+ function checkReportPath(path41) {
29153
+ const target = resolve11(path41);
29154
+ const dir = dirname13(target);
29155
+ if (!existsSync16(dir) || !statSync8(dir).isDirectory()) fail(`--json ${path41}: the folder ${dir} does not exist.`);
29156
+ if (existsSync16(target) && statSync8(target).isDirectory()) fail(`--json ${path41} is a folder; name a file.`);
27246
29157
  try {
27247
29158
  accessSync(existsSync16(target) ? target : dir, fsConstants.W_OK);
27248
29159
  } catch {
27249
- fail(`--json ${path39} cannot be written here.`);
29160
+ fail(`--json ${path41} cannot be written here.`);
27250
29161
  }
27251
29162
  }
27252
29163
  function suiteExitCode(report2, runs, reportWritten) {
@@ -27257,7 +29168,7 @@ function formatCallPages(runs) {
27257
29168
  return pages.length > 0 ? ["Call pages (the transcript, and the recording once it is ready):", ...pages] : [];
27258
29169
  }
27259
29170
  function chooseAudioSource(options, plan, env = process.env) {
27260
- const planDir = options.planPath ? dirname14(resolve11(options.planPath)) : process.cwd();
29171
+ const planDir = options.planPath ? dirname13(resolve11(options.planPath)) : process.cwd();
27261
29172
  const planHasClips = Object.values(plan.lines).some((line) => line.clip);
27262
29173
  if (options.clipsDir || planHasClips) {
27263
29174
  if (options.clipsDir && !existsSync16(options.clipsDir)) fail(`--clips ${options.clipsDir} does not exist.`);
@@ -27456,9 +29367,9 @@ var eval_capture_exports = {};
27456
29367
  __export(eval_capture_exports, {
27457
29368
  evalCaptureCommand: () => evalCaptureCommand
27458
29369
  });
27459
- import * as fs25 from "fs";
27460
- import * as path35 from "path";
27461
- import * as yaml14 from "js-yaml";
29370
+ import * as fs26 from "fs";
29371
+ import * as path37 from "path";
29372
+ import * as yaml15 from "js-yaml";
27462
29373
  function isValidSetName(name) {
27463
29374
  if (name.length === 0 || name === "." || name === "..") return false;
27464
29375
  if (name.startsWith(".")) return false;
@@ -27523,25 +29434,25 @@ async function evalCaptureCommand(args2) {
27523
29434
  const setFolderName = targetSetName;
27524
29435
  const scenarioName = parsed.evalName ?? `Capture ${parsed.conversationId.slice(0, 8)}`;
27525
29436
  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.`);
29437
+ const evalsDir = path37.join(hubFolder, "evals");
29438
+ const targetDir = path37.join(evalsDir, setFolderName);
29439
+ const targetPath = path37.join(targetDir, `${slug}.yaml`);
29440
+ if (!targetPath.startsWith(evalsDir + path37.sep)) {
29441
+ console.error(`Resolved path "${path37.relative(hubFolder, targetPath)}" escapes evals/. Aborting.`);
27531
29442
  process.exit(1);
27532
29443
  }
27533
29444
  if (!ensureRealSubdirNoSymlink(hubFolder, targetDir, false)) {
27534
- console.error(`${path35.relative(hubFolder, targetDir)} is reached through a symlink. Aborting.`);
29445
+ console.error(`${path37.relative(hubFolder, targetDir)} is reached through a symlink. Aborting.`);
27535
29446
  process.exit(1);
27536
29447
  }
27537
29448
  let targetTaken = true;
27538
29449
  try {
27539
- fs25.lstatSync(targetPath);
29450
+ fs26.lstatSync(targetPath);
27540
29451
  } catch {
27541
29452
  targetTaken = false;
27542
29453
  }
27543
29454
  if (targetTaken) {
27544
- console.error(`File already exists: ${path35.relative(hubFolder, targetPath)}. Use --name to choose a different name.`);
29455
+ console.error(`File already exists: ${path37.relative(hubFolder, targetPath)}. Use --name to choose a different name.`);
27545
29456
  process.exit(1);
27546
29457
  }
27547
29458
  console.log("Resolving scenario set...");
@@ -27573,14 +29484,14 @@ async function evalCaptureCommand(args2) {
27573
29484
  ...captured.evaluator_instructions ? { evaluator_instructions: captured.evaluator_instructions } : {}
27574
29485
  };
27575
29486
  const yamlObj = buildEvalYamlObject(evalEntry, slug);
27576
- const outcome = createFileNoFollow(hubFolder, targetPath, Buffer.from(yaml14.dump(yamlObj, YAML_DUMP_OPTIONS), "utf-8"));
29487
+ const outcome = createFileNoFollow(hubFolder, targetPath, Buffer.from(yaml15.dump(yamlObj, YAML_DUMP_OPTIONS), "utf-8"));
27577
29488
  if (outcome !== "created") {
27578
29489
  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.`
29490
+ `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
29491
  );
27581
29492
  process.exit(1);
27582
29493
  }
27583
- const relPath = path35.relative(process.cwd(), targetPath);
29494
+ const relPath = path37.relative(process.cwd(), targetPath);
27584
29495
  console.log(`
27585
29496
  Wrote ${relPath}`);
27586
29497
  console.log("Run `wayai pull` to refresh the agent display name, then commit. The scenario is already on the platform.");
@@ -28960,20 +30871,20 @@ var init_set_connection_credential = __esm({
28960
30871
  });
28961
30872
 
28962
30873
  // src/lib/org-workspace.ts
28963
- import * as fs26 from "fs";
28964
- import * as path36 from "path";
28965
- import * as yaml15 from "js-yaml";
30874
+ import * as fs27 from "fs";
30875
+ import * as path38 from "path";
30876
+ import * as yaml16 from "js-yaml";
28966
30877
  function getOrgDir(gitRoot) {
28967
30878
  return resolveLayout(gitRoot).orgDir;
28968
30879
  }
28969
30880
  function orgManifestExists(orgDir) {
28970
- return fs26.existsSync(path36.join(orgDir, ORG_MANIFEST_NAME));
30881
+ return fs27.existsSync(path38.join(orgDir, ORG_MANIFEST_NAME));
28971
30882
  }
28972
30883
  function parseOrgResources(orgDir) {
28973
- const resourcesDir = path36.join(orgDir, "resources");
30884
+ const resourcesDir = path38.join(orgDir, "resources");
28974
30885
  requireRealSubdirNoSymlink(orgDir, resourcesDir, false);
28975
- const manifestText = readRealFileOrThrow(orgDir, path36.join(orgDir, ORG_MANIFEST_NAME));
28976
- const manifest = (manifestText !== null ? yaml15.load(manifestText) : null) ?? {};
30886
+ const manifestText = readRealFileOrThrow(orgDir, path38.join(orgDir, ORG_MANIFEST_NAME));
30887
+ const manifest = (manifestText !== null ? yaml16.load(manifestText) : null) ?? {};
28977
30888
  const rawResources = Array.isArray(manifest.resources) ? manifest.resources : [];
28978
30889
  const resources = rawResources.map((res) => {
28979
30890
  const resource = { name: res.name };
@@ -28985,9 +30896,9 @@ function parseOrgResources(orgDir) {
28985
30896
  if (res.environment) resource.environment = res.environment;
28986
30897
  if (Array.isArray(res.tags)) resource.tags = res.tags;
28987
30898
  if (Array.isArray(res.folders)) resource.folders = res.folders;
28988
- const resDir = path36.join(resourcesDir, slugify2(resource.name));
30899
+ const resDir = path38.join(resourcesDir, slugify2(resource.name));
28989
30900
  requireRealSubdirNoSymlink(orgDir, resDir, false);
28990
- if (fs26.existsSync(resDir)) {
30901
+ if (fs27.existsSync(resDir)) {
28991
30902
  const files = scanResourceFiles(resDir, "");
28992
30903
  if (files.length > 0) resource.files = files;
28993
30904
  }
@@ -28996,8 +30907,8 @@ function parseOrgResources(orgDir) {
28996
30907
  return { version: 1, resources };
28997
30908
  }
28998
30909
  function writeOrgResources(orgDir, payload) {
28999
- fs26.mkdirSync(orgDir, { recursive: true });
29000
- const resourcesDir = path36.join(orgDir, "resources");
30910
+ fs27.mkdirSync(orgDir, { recursive: true });
30911
+ const resourcesDir = path38.join(orgDir, "resources");
29001
30912
  requireRealSubdirNoSymlink(orgDir, resourcesDir, false);
29002
30913
  const resources = payload.resources ?? [];
29003
30914
  const manifestResources = resources.map((r) => {
@@ -29006,19 +30917,19 @@ function writeOrgResources(orgDir, payload) {
29006
30917
  });
29007
30918
  writeFileNoFollow(
29008
30919
  orgDir,
29009
- path36.join(orgDir, ORG_MANIFEST_NAME),
29010
- Buffer.from(yaml15.dump({ version: 1, resources: manifestResources }, YAML_DUMP_OPTIONS), "utf-8")
30920
+ path38.join(orgDir, ORG_MANIFEST_NAME),
30921
+ Buffer.from(yaml16.dump({ version: 1, resources: manifestResources }, YAML_DUMP_OPTIONS), "utf-8")
29011
30922
  );
29012
30923
  const currentSlugs = /* @__PURE__ */ new Set();
29013
30924
  for (const resource of resources) {
29014
30925
  const resSlug = slugify2(resource.name);
29015
30926
  currentSlugs.add(resSlug);
29016
- writeResourceFileTree(path36.join(resourcesDir, resSlug), resource.files || [], orgDir);
30927
+ writeResourceFileTree(path38.join(resourcesDir, resSlug), resource.files || [], orgDir);
29017
30928
  }
29018
- if (fs26.existsSync(resourcesDir)) {
29019
- for (const entry of fs26.readdirSync(resourcesDir, { withFileTypes: true })) {
30929
+ if (fs27.existsSync(resourcesDir)) {
30930
+ for (const entry of fs27.readdirSync(resourcesDir, { withFileTypes: true })) {
29020
30931
  if (entry.isDirectory() && !currentSlugs.has(entry.name)) {
29021
- fs26.rmSync(path36.join(resourcesDir, entry.name), { recursive: true, force: true });
30932
+ fs27.rmSync(path38.join(resourcesDir, entry.name), { recursive: true, force: true });
29022
30933
  }
29023
30934
  }
29024
30935
  }
@@ -29027,7 +30938,7 @@ async function downloadOrgBinaryFiles(orgDir, payload) {
29027
30938
  let count = 0;
29028
30939
  for (const resource of payload.resources ?? []) {
29029
30940
  if (!resource.files) continue;
29030
- const resDir = path36.join(orgDir, "resources", slugify2(resource.name));
30941
+ const resDir = path38.join(orgDir, "resources", slugify2(resource.name));
29031
30942
  count += await downloadBinaryFiles(resDir, resource.files, orgDir);
29032
30943
  }
29033
30944
  if (count > 0) console.log(`Downloaded ${count} binary resource file(s).`);
@@ -29650,8 +31561,8 @@ var init_report_edit_args = __esm({
29650
31561
  });
29651
31562
 
29652
31563
  // src/lib/file-map.ts
29653
- import * as fs27 from "fs";
29654
- import * as path37 from "path";
31564
+ import * as fs28 from "fs";
31565
+ import * as path39 from "path";
29655
31566
  function isSafeRelPath(rel) {
29656
31567
  if (rel.length === 0 || rel.length > 300) return false;
29657
31568
  if (rel.startsWith("/") || rel.includes("\\")) return false;
@@ -29666,9 +31577,9 @@ function writeFileMap(targetDir, files) {
29666
31577
  if (!isSafeRelPath(rel)) {
29667
31578
  throw new Error(`Refusing to write unsafe path: ${rel}`);
29668
31579
  }
29669
- const abs = path37.join(targetDir, rel);
29670
- fs27.mkdirSync(path37.dirname(abs), { recursive: true });
29671
- fs27.writeFileSync(abs, body, "utf-8");
31580
+ const abs = path39.join(targetDir, rel);
31581
+ fs28.mkdirSync(path39.dirname(abs), { recursive: true });
31582
+ fs28.writeFileSync(abs, body, "utf-8");
29672
31583
  written.push(rel);
29673
31584
  }
29674
31585
  return written;
@@ -29684,8 +31595,8 @@ var admin_exports = {};
29684
31595
  __export(admin_exports, {
29685
31596
  adminCommand: () => adminCommand
29686
31597
  });
29687
- import * as fs28 from "fs";
29688
- import * as path38 from "path";
31598
+ import * as fs29 from "fs";
31599
+ import * as path40 from "path";
29689
31600
  async function adminCommand(args2) {
29690
31601
  const [group, ...afterGroup] = args2;
29691
31602
  if (!group) {
@@ -30124,7 +32035,7 @@ async function runArchiveRead(positional, flagArgs) {
30124
32035
  exitOnApiError(err);
30125
32036
  throw err;
30126
32037
  }
30127
- fs28.writeFileSync(outPath, zip);
32038
+ fs29.writeFileSync(outPath, zip);
30128
32039
  console.log(`Wrote ${zip.byteLength} bytes to ${outPath}`);
30129
32040
  return;
30130
32041
  }
@@ -30266,13 +32177,13 @@ async function runSkillInstall(positional) {
30266
32177
  throw err;
30267
32178
  }
30268
32179
  const root = findGitRoot() ?? process.cwd();
30269
- const present = HARNESS_SKILL_DIRS.filter((dir) => fs28.existsSync(path38.join(root, dir)));
32180
+ const present = HARNESS_SKILL_DIRS.filter((dir) => fs29.existsSync(path40.join(root, dir)));
30270
32181
  const targets = present.length > 0 ? present : HARNESS_SKILL_DIRS;
30271
32182
  const fileCount = Object.keys(res.files).length;
30272
32183
  const relDirs = targets.map((harness) => {
30273
- const targetDir = path38.join(root, harness, "skills", name);
32184
+ const targetDir = path40.join(root, harness, "skills", name);
30274
32185
  writeFileMap(targetDir, res.files);
30275
- return `${path38.relative(root, targetDir)}/`;
32186
+ return `${path40.relative(root, targetDir)}/`;
30276
32187
  });
30277
32188
  console.log(`Installed skill "${name}" (${fileCount} file${fileCount === 1 ? "" : "s"}) \u2192 ${relDirs.join(", ")}`);
30278
32189
  console.log("Reload your agent (e.g. restart Claude Code) to pick up the skill.");
@@ -32865,12 +34776,12 @@ var init_providers = __esm({
32865
34776
  // src/data/commands/report.ts
32866
34777
  import { Command as Command6 } from "commander";
32867
34778
  import { readFileSync as readFileSync20 } from "fs";
32868
- import { dirname as dirname16, join as join35 } from "path";
34779
+ import { dirname as dirname15, join as join36 } from "path";
32869
34780
  import { fileURLToPath as fileURLToPath2 } from "url";
32870
34781
  function resolveCliVersion() {
32871
34782
  for (const candidate of [
32872
- join35(here, "..", "package.json"),
32873
- join35(here, "..", "..", "..", "package.json")
34783
+ join36(here, "..", "package.json"),
34784
+ join36(here, "..", "..", "..", "package.json")
32874
34785
  ]) {
32875
34786
  try {
32876
34787
  const version = JSON.parse(readFileSync20(candidate, "utf-8")).version;
@@ -33069,7 +34980,7 @@ var init_report3 = __esm({
33069
34980
  init_terminal_output();
33070
34981
  init_workspace();
33071
34982
  init_skill_version();
33072
- here = dirname16(fileURLToPath2(import.meta.url));
34983
+ here = dirname15(fileURLToPath2(import.meta.url));
33073
34984
  }
33074
34985
  });
33075
34986
 
@@ -33382,16 +35293,16 @@ MCP URL: ${toolsetMcpUrl(slug)}`);
33382
35293
  ).option("--force", "Revoke even if the token appears to be in live use").action(async function(tokenId, opts) {
33383
35294
  const client = await createDataClient();
33384
35295
  const id = pathSegment(tokenId, "token id");
33385
- const path39 = (force) => `/v1/tokens/${id}${force ? "?force=true" : ""}`;
35296
+ const path41 = (force) => `/v1/tokens/${id}${force ? "?force=true" : ""}`;
33386
35297
  const format = outputFormat(this);
33387
35298
  const revoked = (forced) => printOutput({ token_id: tokenId, revoked: true, forced }, format);
33388
35299
  if (opts.force) {
33389
- await client.request("DELETE", path39(true));
35300
+ await client.request("DELETE", path41(true));
33390
35301
  revoked(true);
33391
35302
  return;
33392
35303
  }
33393
35304
  try {
33394
- await client.request("DELETE", path39(false));
35305
+ await client.request("DELETE", path41(false));
33395
35306
  revoked(false);
33396
35307
  } catch (err) {
33397
35308
  if (!(err instanceof ApiError) || err.status !== 409 || dataErrorDetails(err)?.requires_force !== true) {
@@ -33406,7 +35317,7 @@ MCP URL: ${toolsetMcpUrl(slug)}`);
33406
35317
  console.error("Aborted.");
33407
35318
  return process.exit(1);
33408
35319
  }
33409
- await client.request("DELETE", path39(true));
35320
+ await client.request("DELETE", path41(true));
33410
35321
  revoked(true);
33411
35322
  }
33412
35323
  });
@@ -33554,9 +35465,9 @@ var init_base_tags = __esm({
33554
35465
 
33555
35466
  // src/data/commands/bases.ts
33556
35467
  import { Command as Command10 } from "commander";
33557
- function pageOf(path39, cursor) {
33558
- if (!cursor) return path39;
33559
- return `${path39}${path39.includes("?") ? "&" : "?"}cursor=${encodeURIComponent(cursor)}`;
35468
+ function pageOf(path41, cursor) {
35469
+ if (!cursor) return path41;
35470
+ return `${path41}${path41.includes("?") ? "&" : "?"}cursor=${encodeURIComponent(cursor)}`;
33560
35471
  }
33561
35472
  function parseEnum(flag, value, allowed) {
33562
35473
  if (value === void 0) return void 0;
@@ -33713,9 +35624,9 @@ function buildBasesCommand() {
33713
35624
  });
33714
35625
  bases.command("list-previews <origin-id>").description("List preview bases cloned from a base (production or preview)").action(async function(originId) {
33715
35626
  const client = await createDataClient();
33716
- const path39 = `/v1/${pathSegment(originId, "origin base id")}/previews`;
35627
+ const path41 = `/v1/${pathSegment(originId, "origin base id")}/previews`;
33717
35628
  printOutput(
33718
- await client.collectPages((cursor) => pageOf(path39, cursor)),
35629
+ await client.collectPages((cursor) => pageOf(path41, cursor)),
33719
35630
  outputFormat(this)
33720
35631
  );
33721
35632
  });
@@ -33762,9 +35673,9 @@ function buildBasesCommand() {
33762
35673
  });
33763
35674
  bases.command("promotions <production-id>").description("List promotion history for a production base").action(async function(productionId) {
33764
35675
  const client = await createDataClient();
33765
- const path39 = `/v1/${pathSegment(productionId, "production base id")}/promotions`;
35676
+ const path41 = `/v1/${pathSegment(productionId, "production base id")}/promotions`;
33766
35677
  printOutput(
33767
- await client.collectPages((cursor) => pageOf(path39, cursor)),
35678
+ await client.collectPages((cursor) => pageOf(path41, cursor)),
33768
35679
  outputFormat(this)
33769
35680
  );
33770
35681
  });
@@ -33903,7 +35814,7 @@ var init_file_types = __esm({
33903
35814
  // src/data/commands/files.ts
33904
35815
  import { Command as Command12 } from "commander";
33905
35816
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
33906
- import { basename as basename23 } from "path";
35817
+ import { basename as basename25 } from "path";
33907
35818
  function renderFileDiff(fileType, filePath, from, to, d) {
33908
35819
  console.log(sanitizeTerminalText(`${fileType}/${filePath}: v${from} \u2192 v${to}`));
33909
35820
  const md = d.metadata_delta;
@@ -33935,7 +35846,7 @@ function renderFileDiff(fileType, filePath, from, to, d) {
33935
35846
  }
33936
35847
  function downloadTarget(remotePath, to) {
33937
35848
  if (to) return to;
33938
- const derived = basename23(remotePath);
35849
+ const derived = basename25(remotePath);
33939
35850
  if (derived === "" || derived === "." || derived === "..") {
33940
35851
  throw expected(
33941
35852
  `Cannot derive a local filename from "${remotePath}" \u2014 pass --to <local> to name it.`
@@ -34311,8 +36222,8 @@ function buildRecordsCommand() {
34311
36222
  const body = { data: parseData(opts.data) };
34312
36223
  if (opts.externalId) body.external_id = opts.externalId;
34313
36224
  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));
36225
+ const path41 = opts.id ? `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${pathSegment(opts.id, "--id")}` : `/v1/${base}/records/${pathSegment(recordType2, "record_type")}`;
36226
+ printOutput(await client.request("PUT", path41, body), outputFormat(this));
34316
36227
  });
34317
36228
  records.command("query <record_type>").description("List/search records: exact filters + fuzzy `search`, sorting, pagination").option(
34318
36229
  "--external-source <source>",
@@ -34347,9 +36258,9 @@ function buildRecordsCommand() {
34347
36258
  ).action(async function(recordType2, id, opts) {
34348
36259
  const base = pathSegment(requireBase(this), "--base");
34349
36260
  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));
36261
+ let path41 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
36262
+ if (opts.externalSource) path41 += `?external_source=${encodeURIComponent(opts.externalSource)}`;
36263
+ printOutput(await client.request("GET", path41), outputFormat(this));
34353
36264
  });
34354
36265
  records.command("delete <record_type> <id>").description(
34355
36266
  "Delete a record by internal ID, or by external_id (pass --external-source to scope it)"
@@ -34363,11 +36274,11 @@ function buildRecordsCommand() {
34363
36274
  }
34364
36275
  const base = pathSegment(requireBase(this), "--base");
34365
36276
  const client = await createDataClient();
34366
- let path39 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
36277
+ let path41 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
34367
36278
  if (opts.externalSource) {
34368
- path39 += `?external_source=${encodeURIComponent(opts.externalSource)}&external_id=${encodeURIComponent(id)}`;
36279
+ path41 += `?external_source=${encodeURIComponent(opts.externalSource)}&external_id=${encodeURIComponent(id)}`;
34369
36280
  }
34370
- await client.request("DELETE", path39);
36281
+ await client.request("DELETE", path41);
34371
36282
  console.log("Deleted");
34372
36283
  });
34373
36284
  records.command("history <id>").description(
@@ -34532,8 +36443,8 @@ function buildRelationshipsCommand() {
34532
36443
  if (opts.data) body.data = parseData(opts.data);
34533
36444
  const base = pathSegment(requireBase(this), "--base");
34534
36445
  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));
36446
+ const path41 = opts.id ? `/v1/${base}/relationships/${pathSegment(opts.id, "--id")}` : `/v1/${base}/relationships`;
36447
+ printOutput(await client.request("PUT", path41, body), outputFormat(this));
34537
36448
  });
34538
36449
  relationships.command("get <id>").description(
34539
36450
  "Get a relationship by ID (or by its external key: pass the external_id with --rel-type)"
@@ -34766,8 +36677,8 @@ function buildToolsetsCommand() {
34766
36677
  toolsets.command("get <slug>").description("Get a toolset").option("--resolved", "Include resolved record_type schemas").action(async function(slug, opts) {
34767
36678
  const base = pathSegment(requireBase(this), "--base");
34768
36679
  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));
36680
+ const path41 = opts.resolved ? `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}/resolved` : `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}`;
36681
+ printOutput(await client.request("GET", path41), outputFormat(this));
34771
36682
  });
34772
36683
  toolsets.command("list").description("List all toolsets").action(async function() {
34773
36684
  const base = pathSegment(requireBase(this), "--base");
@@ -35002,7 +36913,7 @@ init_utils();
35002
36913
  init_registry();
35003
36914
  import { readFileSync as readFileSync24 } from "fs";
35004
36915
  import { fileURLToPath as fileURLToPath3 } from "url";
35005
- import { dirname as dirname17, join as join36 } from "path";
36916
+ import { dirname as dirname16, join as join37 } from "path";
35006
36917
 
35007
36918
  // src/lib/version-refresh.ts
35008
36919
  init_version_cache();
@@ -35157,8 +37068,8 @@ Run \`wayai admin skill install\` to update.`);
35157
37068
  }
35158
37069
 
35159
37070
  // src/index.ts
35160
- var __dirname = dirname17(fileURLToPath3(import.meta.url));
35161
- var pkg = JSON.parse(readFileSync24(join36(__dirname, "..", "package.json"), "utf-8"));
37071
+ var __dirname = dirname16(fileURLToPath3(import.meta.url));
37072
+ var pkg = JSON.parse(readFileSync24(join37(__dirname, "..", "package.json"), "utf-8"));
35162
37073
  var [, , command, ...args] = process.argv;
35163
37074
  var isBackgroundRefresh = command === REFRESH_COMMAND;
35164
37075
  if (!isBackgroundRefresh) initSentry(command, pkg.version);