@wayai/cli 0.3.202 → 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() {
@@ -6920,7 +6920,7 @@ var init_chunk_T7KQKDET = __esm({
6920
6920
  }
6921
6921
  });
6922
6922
 
6923
- // ../../packages/core/dist/chunk-T2RKGBYS.js
6923
+ // ../../packages/core/dist/chunk-46WU2TD4.js
6924
6924
  function containsKanbanPlaceholders(value) {
6925
6925
  if (value == null) return false;
6926
6926
  const serialized = typeof value === "string" ? value : JSON.stringify(value);
@@ -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
  }
@@ -8230,8 +8230,8 @@ function refineHubAsCodeMonitorRulesOutput(config, ctx) {
8230
8230
  });
8231
8231
  }
8232
8232
  var PLACEHOLDER_TOKENS, ALLOWED_TYPES, SUBSCHEMA_OBJECT_KEYWORDS, SUBSCHEMA_LIST_KEYWORDS, SUBSCHEMA_MAP_KEYWORDS, MAX_SCHEMA_DEPTH, uuidSchema, USER_ID_REGEX, timestampSchema, idParamSchema, clientOptionalString, clientOptionalBoolean, clientOptionalNumber, messageAttachment, MAX_MESSAGE_BODY_BYTES, MAX_ATTACHMENTS_PER_MESSAGE, MESSAGE_ENVELOPE_HEADROOM_BYTES, MAX_AUDIO_FILE_BASE64_BYTES, MAX_KANBAN_STATUSES, MAX_FOLLOWUPS_PER_STATUS, MAX_LANES, MAX_ADDITIONAL_CONTEXT_SCHEMA_BYTES, RESERVED_TEMPLATE_DUMP_NAME, kanbanStatusSlugSchema, followupTypeSchema, EVENT_RELATIVE_FOLLOWUP_TYPES, followupTimeUnitSchema, followupIdSchema, SELECTABLE_FOLLOWUP_TYPES, MAX_LINKED_FOLLOWUP_TARGETS_PER_STATUS, followupSchema, MAX_OUTCOMES_PER_STATUS, kanbanOutcomeSchema, EXCLUSIVE_FLAG_PAIRS, kanbanStatusSchema, kanbanStatusesArraySchema, laneSchema, lanesArraySchema, toolIdParam, listToolsQuery, toolAgentQuery, deleteToolQuery, toolConnectionsQuery, nativeToolIdSchema, CONTEXT_BOUNDARIES, contextBoundarySchema, addNativeToolBody, addMcpToolBody, createCustomToolBody, updateCustomToolBody, updateToolExecutionConfigBody, primaryRegionSchema, placementSourceSchema, orgIdParam, orgAdminIdParam, createOrganizationBody, updateOrganizationBody, addOrgAdminBody, HUB_DISPLAY_NAME_MAX_CHARS, HUB_WELCOME_MESSAGE_MAX_CHARS, HUB_STARTER_QUESTIONS_MAX, HUB_STARTER_QUESTION_MAX_CHARS, displayName, welcomeMessage, starterQuestions, hubDisplayNamesSchema, hubWelcomeMessagesSchema, hubStarterQuestionsSchema, hubDisplayNamesField, hubWelcomeMessagesField, hubStarterQuestionsField, HUB_PRESENTATION_FIELDS, HUB_PRESENTATION_SCHEMAS, hubIdParam, hubAdminIdParam, hubUserIdParam, hubIdentityIdParam, hubTeamIdParam, hubTeamUserDeleteParam, hubSchemaQuery, HUB_USERS_PAGE_SIZE_MAX, HUB_USERS_SEARCH_MAX, HUB_USERS_CHANNEL_TYPES, getHubUsersQuery, PREVIEW_LABEL_MAX_LENGTH, MAX_ENDED_INDEX_RETENTION_DAYS, MAX_EVAL_RETENTION_DAYS, MAX_END_USER_DAILY_MESSAGE_LIMIT, endUserDailyMessageLimitField, previewLabelField, hubTagsField, createHubBody, updateHubBody, HUB_TAG_CHANGE_REFUSED, hubTagChangeRefusalSchema, addHubAdminBody, addHubIdentityAuthorizationBody, contactDecisionBody, approveContactBody, listContactAccessQuery, createHubTeamBody, updateHubTeamBody, addHubTeamUserBody, reassignHubUserBody, replicatePreviewBody, HUB_DIFF_SECTION_SET, HUB_DIFF_SECTIONS, updateChannelTestIdentitiesBody, claimCodeChannelParam, claimCodeDeleteParam, PREVIOUS_CONVERSATIONS_MAX, NATIVE_TOOL_NAMES, previousConversationsCountField, summarizationThresholdField, flagConditionSchema, MONITOR_TRIGGERS, monitorTriggerSchema, AUTOMATION_GATE_TRIGGERS, NON_FIRING_TRIGGERS, MONITOR_FIRING_TRIGGERS, MONITOR_DELAY_SECONDS_MIN, MONITOR_IDLE_DELAY_REQUIRED_MESSAGE, MONITOR_HISTORY_MESSAGES_MAX, monitorHistoryMessagesSchema, monitorIncludeToolResultsSchema, MONITOR_INPUT_SHAPING_KEYS, MONITOR_INPUT_SHAPING_SCHEMAS, MONITOR_INPUT_SHAPING_IDLE_MESSAGE, monitorArgumentSourceSchema, MONITOR_NOTE_TEMPLATE_MAX, MONITOR_STEER_NOTE_MAX, MONITOR_GATE_AGENT_NAME_MAX, monitorActionSchema, monitorRuleSchema, MONITOR_RULE_KEYS, MONITOR_RULE_SCHEMAS, MONITOR_RULE_TRIGGERS, MONITOR_RULE_TRIGGER_MESSAGE, CALL_UTTERANCE_SPEAKERS, callUtteranceSpeakerSchema, MONITOR_CALL_KEYS, MONITOR_CALL_SCHEMAS, MONITOR_CALL_KEY_MESSAGE, CALL_STEERING_MIN_CONFIDENCE, CALL_UTTERANCE_RULE_NOT_CONFIDENT_MESSAGE, CALL_UTTERANCE_FALLBACK_MESSAGE, CALL_UTTERANCE_FLAG_CONDITIONS_MESSAGE, CALL_STEER_NOTE_BLANK_MESSAGE, MONITOR_USER_MESSAGE_ACTION_KINDS, MONITOR_ASSISTANT_REPLY_ACTION_KINDS, MONITOR_CALL_UTTERANCE_ACTION_KINDS, MONITOR_AUTOMATION_FIRE_ACTION_KINDS, MONITOR_AUTOMATION_ITEM_ACTION_KINDS, INSERT_NOTE_TOOL_NAME, RUN_MONITOR_TOOL_NAME, MONITOR_RULE_ALLOWED_NATIVE_TOOLS, MONITOR_RULE_REENTRY_TOOLS, MONITOR_RULE_REENTRY_TRACKS, AUTOMATION_FIRE_FLAG_CONDITIONS_MESSAGE, AUTOMATION_FIRE_INPUT_SHAPING_MESSAGE, AUTOMATION_ITEM_FLAG_CONDITIONS_MESSAGE, AUTOMATION_ITEM_INPUT_SHAPING_MESSAGE, AUTOMATION_GATE_KEY_MESSAGES, monitorConfigField, flagConditionsField, agentIdParam, listAgentsQuery, agentConnectionsQuery, createAgentBody, updateAgentBody, AGENT_PARAMETER_TYPES, AGENT_PARAMETER_NAME_REGEX, agentParameterCreateSchema, MAX_AGENT_PARAMETERS_PER_REQUEST, createAgentParametersBody, evalIdParam, sessionIdParam, scenarioSetIdParam, hubIdQuery, evalDateContextSchema, EVAL_FIXTURE_NAME_MAX_LENGTH, evalFixtureOverrideSchema, runSessionQuery, runJourneyQuery, EVAL_INPUT_ROLES, EVAL_INPUT_ROLE_LIST, ROLE_ECHO_MAX, EVAL_INITIAL_STATE_SCOPES, MAX_INITIAL_STATE_ENTRIES, evalInitialStateEntry, evalBody, EVAL_LIST_MAX_LIMIT, evalListLimit, getEvalsQuery, toggleEvalBody, bulkToggleEvalsBody, pacingConfigSchema, EVAL_MAX_SELECTED_SCENARIOS, evalScenarioSelectionEntrySchema, evalScenarioSelectionSchema, sessionConfigSchema, createSessionBody, getSessionsQuery, getSessionResultsQuery, getSessionRunsQuery, evalAnalyticsQuery, getSessionCostQuery, compareSessionsQuery, evalsSqlBody, evalsSqlSchemaQuery, exportResultsQuery, validateEvalBody, hubAgentsQuery, createScenarioSetBody, updateScenarioSetBody, scenarioSetsHubQuery, createScenarioFromConversationBody, createJourneyFromConversationBody, EVAL_ATTACHMENT_HASH_REGEX, evalAttachmentRef, turnMayCarryAttachments, ATTACHMENTS_USER_ONLY_MESSAGE, journeyIdParam, journeyToolCallSchema, journeyTurnSchema, journeyStepOverrideSchema, fixtureField, createJourneyBody, updateJourneyBody, evalSessionConfigResponseSchema, evalSessionResponseSchema, evalSessionListConfigSchema, evalSessionListItemSchema, seedReleaseStatusSchema, EVAL_SESSION_NOT_QUIESCENT, MAX_RESOURCE_FILE_SIZE, CALL_OPENING_LANGUAGES, callOpeningLanguage, CALL_OPENING_MAX_DURATION_MS, CALL_OPENING_MAX_BYTES, CALL_OPENING_CAPTION_MAX_CHARS, CALL_OPENING_CONTENT_TYPES, callOpeningContentType, CALL_OPENING_HASH_REGEX, callOpeningHash, callOpeningCaption, storedCallOpening, MAX_CALL_OPENING_ENCODED_BYTES, MAX_CALL_OPENING_BODY_BYTES, agentCallOpeningParams, putAgentCallOpeningBody, deleteAgentCallOpeningQuery, callOpeningParam, callOpeningQuery, MAX_10MB_BASE64_ENCODED_BYTES, MAX_EVAL_ATTACHMENT_ENCODED_BYTES, MAX_EVAL_ATTACHMENT_CARRIERS, MAX_EVAL_ATTACHMENT_AGGREGATE_ENCODED_BYTES, MAX_RESOURCE_ENCODED_BYTES, MAX_HUB_AS_CODE_RESOURCES, MAX_HUB_AS_CODE_RESOURCE_FILES, MAX_RESOURCE_AGGREGATE_ENCODED_BYTES, MAX_CI_CONFIG_BODY_BYTES, MAX_CALL_OPENING_CARRIERS, MAX_CALL_OPENING_AGGREGATE_ENCODED_BYTES, hubAsCodeStateSchema, hubAsCodeResourceFileSchema, hubAsCodeResourceSchema, uuidPattern, ciUuidSchema, boundedHubAsCodeResourcesSchema, hubAsCodeConfigSchema, ciPullHubIdParam, ciBranchParam, ciPullQuery, ciHubsQuery, ciLookupQuery, ciBranchesQuery, ciSyncBody, ciPushBody, ciDiffBody, ciPublishBody, ciPublishPreviewBody, ciSyncMcpBody, ciOrgResourcesParam, orgResourcesConfigSchema, ciOrgResourcesPushBody, ciOrgResourcesDiffBody, CI_APPLY_ENTITY_KINDS, CI_APPLY_OPERATIONS, ciApplyErrorBaseSchema, ciApplyErrorSchema, ciSyncCountSchema, ciSyncChangesSchema, ciSyncWarningSchema, ciSyncResponseSchema;
8233
- var init_chunk_T2RKGBYS = __esm({
8234
- "../../packages/core/dist/chunk-T2RKGBYS.js"() {
8233
+ var init_chunk_46WU2TD4 = __esm({
8234
+ "../../packages/core/dist/chunk-46WU2TD4.js"() {
8235
8235
  "use strict";
8236
8236
  init_chunk_SNA2DXAC();
8237
8237
  init_chunk_MIWZOSPI();
@@ -8742,6 +8742,7 @@ var init_chunk_T2RKGBYS = __esm({
8742
8742
  organization_id: uuidSchema,
8743
8743
  hub_name: external_exports.string().min(1, "hub_name is required"),
8744
8744
  hub_description: external_exports.string().optional(),
8745
+ hub_type: external_exports.enum(["chat", "task"]).optional(),
8745
8746
  kanban_statuses: kanbanStatusesArraySchema.nullable().optional(),
8746
8747
  lanes: lanesArraySchema.nullable().optional(),
8747
8748
  // Optional preview label for the first preview. Server-owned thereafter
@@ -8752,14 +8753,12 @@ var init_chunk_T2RKGBYS = __esm({
8752
8753
  // Org tag ids. Ids that name none of the org's tags are dropped; a hub created with
8753
8754
  // any is a tag change (`hubTagChangeRefusalSchema`).
8754
8755
  tags: hubTagsField,
8755
- // Localized end-user presentation (`hub-presentation.ts`). Validated here too: the body is
8756
- // `.passthrough()` and `createHub` writes all three, so an unvalidated map would persist.
8756
+ // Localized end-user presentation (`hub-presentation.ts`), as `updateHubBody` takes it.
8757
8757
  display_names: hubDisplayNamesField,
8758
8758
  welcome_messages: hubWelcomeMessagesField,
8759
8759
  starter_questions: hubStarterQuestionsField,
8760
- // Validated here for the same reason: `createHub` writes it.
8761
8760
  end_user_daily_message_limit: endUserDailyMessageLimitField
8762
- }).passthrough().superRefine(refineLaneReferences);
8761
+ }).strict().superRefine(refineLaneReferences);
8763
8762
  updateHubBody = external_exports.object({
8764
8763
  support_model: external_exports.enum(["conversation", "account"]).optional(),
8765
8764
  unassigned_policy: external_exports.enum(["default_team", "auto_assign_random", "auto_assign_round_robin", "pull_queue"]).optional(),
@@ -9724,7 +9723,7 @@ var init_chunk_T2RKGBYS = __esm({
9724
9723
  }
9725
9724
  });
9726
9725
 
9727
- // ../../packages/core/dist/chunk-GTTWALH3.js
9726
+ // ../../packages/core/dist/chunk-5RGL5RXH.js
9728
9727
  function cacheReconcileIdCount(body) {
9729
9728
  return body.conversations.length + body.histories.reduce((n, history) => n + history.conversation_ids.length, 0) + body.threads.length;
9730
9729
  }
@@ -9770,27 +9769,33 @@ function parseCommunityLessonVideoUrl(url) {
9770
9769
  const parts = /^https?:\/\/([^/?#]+)([^?#]*)(?:\?([^#]*))?(?:#.*)?$/i.exec(url.trim());
9771
9770
  if (!parts) return null;
9772
9771
  const host = parts[1].toLowerCase();
9773
- const path39 = parts[2] ?? "";
9772
+ const path41 = parts[2] ?? "";
9774
9773
  const query = parts[3] ?? "";
9775
9774
  let id;
9776
9775
  if (host === YOUTU_BE) {
9777
- id = /^\/([A-Za-z0-9_-]{11})\/?$/.exec(path39)?.[1];
9776
+ id = /^\/([A-Za-z0-9_-]{11})\/?$/.exec(path41)?.[1];
9778
9777
  } else if (YOUTUBE_HOSTS.has(host)) {
9779
- if (path39 === "/watch" || path39 === "/watch/") {
9778
+ if (path41 === "/watch" || path41 === "/watch/") {
9780
9779
  id = query.split("&").find((param) => param.startsWith("v="))?.slice(2);
9781
9780
  } else {
9782
- id = YOUTUBE_ID_PATH.exec(path39)?.[1];
9781
+ id = YOUTUBE_ID_PATH.exec(path41)?.[1];
9783
9782
  }
9784
9783
  }
9785
9784
  return id !== void 0 && YOUTUBE_ID.test(id) ? { provider: "youtube", id } : null;
9786
9785
  }
9787
- 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;
9788
- var init_chunk_GTTWALH3 = __esm({
9789
- "../../packages/core/dist/chunk-GTTWALH3.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"() {
9790
9795
  "use strict";
9791
9796
  init_chunk_VORPHGDT();
9792
9797
  init_chunk_POL7B3EM();
9793
- init_chunk_T2RKGBYS();
9798
+ init_chunk_46WU2TD4();
9794
9799
  init_chunk_MIWZOSPI();
9795
9800
  init_chunk_3NS3HULK();
9796
9801
  init_chunk_JWPRTSIB();
@@ -10847,7 +10852,8 @@ var init_chunk_GTTWALH3 = __esm({
10847
10852
  });
10848
10853
  updateUserBody = external_exports.object({
10849
10854
  user_language: external_exports.string().optional(),
10850
- 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(),
10851
10857
  profile_name: external_exports.string().optional(),
10852
10858
  link_email_to_identities: external_exports.boolean().optional()
10853
10859
  }).refine(
@@ -12813,6 +12819,8 @@ var init_chunk_GTTWALH3 = __esm({
12813
12819
  COMMUNITIES_PER_ORG_MAX = 10;
12814
12820
  COMMUNITY_NAME_MAX_LENGTH = 100;
12815
12821
  COMMUNITY_REGISTRY_CLOSED_REASON = "community_registry_closed";
12822
+ COMMUNITY_ADMISSIONS = ["link", "invite"];
12823
+ COMMUNITY_ACCESS_ADD_MAX = 500;
12816
12824
  COMMUNITY_JOIN_SOURCE_MAX_LENGTH = 32;
12817
12825
  COMMUNITY_JOIN_FLOW_TOKEN_MAX_LENGTH = 128;
12818
12826
  COMMUNITY_ROOMS_MAX = 20;
@@ -12873,11 +12881,20 @@ var init_chunk_GTTWALH3 = __esm({
12873
12881
  communityOrgParam = external_exports.object({ orgId: uuidSchema });
12874
12882
  communityPeopleQuery = external_exports.object({ after: peopleUserIdSchema.optional() });
12875
12883
  communityCoursesLabelSchema = external_exports.string().trim().max(COMMUNITY_COURSES_LABEL_MAX_LENGTH);
12884
+ communityAdmissionSchema = external_exports.enum(COMMUNITY_ADMISSIONS);
12876
12885
  communitySettingsBody = external_exports.object({
12877
12886
  rules: external_exports.string().trim().max(COMMUNITY_RULES_MAX_LENGTH).nullable().optional().transform((rules) => rules === "" ? null : rules),
12878
12887
  slow_mode_seconds: external_exports.number().int().min(0).max(COMMUNITY_SLOW_MODE_MAX_SECONDS).optional(),
12879
- courses_label: communityCoursesLabelSchema.optional()
12880
- }).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 });
12881
12898
  communityBanBody = external_exports.object({ delete_messages: external_exports.boolean().optional() }).strict();
12882
12899
  communityLockBody = external_exports.object({ locked: external_exports.boolean() }).strict();
12883
12900
  communityPinBody = external_exports.object({ pinned: external_exports.boolean() }).strict();
@@ -12892,6 +12909,10 @@ var init_chunk_GTTWALH3 = __esm({
12892
12909
  communityEmailUnsubscribeTokenSchema = external_exports.string().max(200).regex(/^[0-9a-f-]{36}\.[A-Za-z0-9_-]{1,64}\.[0-9a-f]{64}$/i);
12893
12910
  COMMUNITY_EMAIL_UNSUBSCRIBE_PATH = "/api/communities/email-unsubscribe";
12894
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;
12895
12916
  COMMUNITY_COURSE_TITLE_MAX_LENGTH = 200;
12896
12917
  COMMUNITY_COURSE_DESCRIPTION_MAX_LENGTH = 2e3;
12897
12918
  COMMUNITY_LESSON_BODY_MAX_LENGTH = 2e4;
@@ -12952,6 +12973,9 @@ var init_chunk_GTTWALH3 = __esm({
12952
12973
  }).strict().refine((b) => Object.values(b).some((value) => value !== void 0), "nothing to change");
12953
12974
  COMMUNITY_LESSON_TREE_MAX_BYTES = 512 * 1024;
12954
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";
12955
12979
  LINK_SCHEME = new RegExp(`^(${COMMUNITY_LESSON_LINK_PROTOCOLS.join("|")}):`, "i");
12956
12980
  adminSetGuideHubBody = external_exports.object({
12957
12981
  hub_id: uuidSchema,
@@ -13263,10 +13287,10 @@ var init_chunk_BPDC67ML = __esm({
13263
13287
  var init_contracts = __esm({
13264
13288
  "../../packages/core/dist/contracts/index.js"() {
13265
13289
  "use strict";
13266
- init_chunk_GTTWALH3();
13290
+ init_chunk_5RGL5RXH();
13267
13291
  init_chunk_VORPHGDT();
13268
13292
  init_chunk_POL7B3EM();
13269
- init_chunk_T2RKGBYS();
13293
+ init_chunk_46WU2TD4();
13270
13294
  init_chunk_SNA2DXAC();
13271
13295
  init_chunk_MIWZOSPI();
13272
13296
  init_chunk_LKBSHANA();
@@ -13305,10 +13329,10 @@ function toDataProxyPath(v1Path) {
13305
13329
  }
13306
13330
  return `${DATA_PROXY_PREFIX}${v1Path.slice(REKOR_V1_PREFIX.length)}`;
13307
13331
  }
13308
- function withOrgSelector(path39, orgId) {
13309
- if (!orgId) return path39;
13310
- const separator = path39.includes("?") ? "&" : "?";
13311
- 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)}`;
13312
13336
  }
13313
13337
  function dataErrorMessage(err) {
13314
13338
  if (!(err instanceof ApiError)) return null;
@@ -13324,7 +13348,7 @@ function dataErrorMessage(err) {
13324
13348
  const message = parsed?.error?.message;
13325
13349
  return typeof message === "string" && message ? message : null;
13326
13350
  }
13327
- var RETRYABLE_BACKOFF_MS, delay, ApiError, ApiClient;
13351
+ var RETRYABLE_BACKOFF_MS, delay, courseAuthoring, ApiError, ApiClient;
13328
13352
  var init_api_client = __esm({
13329
13353
  "src/lib/api-client.ts"() {
13330
13354
  "use strict";
@@ -13333,6 +13357,7 @@ var init_api_client = __esm({
13333
13357
  init_mask_secrets();
13334
13358
  RETRYABLE_BACKOFF_MS = [500, 1e3, 2e3];
13335
13359
  delay = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
13360
+ courseAuthoring = (communityId, courseId) => `/api/communities/${encodeURIComponent(communityId)}/authoring/courses${courseId === void 0 ? "" : `/${encodeURIComponent(courseId)}`}`;
13336
13361
  ApiError = class extends Error {
13337
13362
  status;
13338
13363
  body;
@@ -13342,13 +13367,13 @@ var init_api_client = __esm({
13342
13367
  * telling them apart by body shape misreads one for the other.
13343
13368
  */
13344
13369
  path;
13345
- constructor(method, path39, status, body) {
13370
+ constructor(method, path41, status, body) {
13346
13371
  const safeBody = maskSecretsInMessage(body);
13347
- super(`API request failed: ${method} ${path39} (${status}): ${safeBody}`);
13372
+ super(`API request failed: ${method} ${path41} (${status}): ${safeBody}`);
13348
13373
  this.name = "ApiError";
13349
13374
  this.status = status;
13350
13375
  this.body = safeBody;
13351
- this.path = path39;
13376
+ this.path = path41;
13352
13377
  }
13353
13378
  /** True when the status code is a 4xx client error (expected user-facing condition, not a bug). */
13354
13379
  get isExpected() {
@@ -13492,6 +13517,33 @@ var init_api_client = __esm({
13492
13517
  body
13493
13518
  );
13494
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
+ }
13495
13547
  // Read which hub is the WayAI Guide (platform-admin gated, wayai#4948). Used by
13496
13548
  // `wayai admin guide status`.
13497
13549
  async adminGetGuide() {
@@ -13520,8 +13572,8 @@ var init_api_client = __esm({
13520
13572
  ...opts?.check && { check: true }
13521
13573
  });
13522
13574
  }
13523
- async lookup(path39, opts) {
13524
- const params = new URLSearchParams({ path: path39 });
13575
+ async lookup(path41, opts) {
13576
+ const params = new URLSearchParams({ path: path41 });
13525
13577
  if (opts?.organizationId) params.set("organization_id", opts.organizationId);
13526
13578
  return this.request("GET", `/api/ci/lookup?${params.toString()}`);
13527
13579
  }
@@ -14029,9 +14081,9 @@ var init_api_client = __esm({
14029
14081
  * sandbox for this conversation, or the blob was purged).
14030
14082
  */
14031
14083
  async downloadArchiveSandboxFs(hubId, conversationId) {
14032
- const path39 = `/api/archive/${encodeURIComponent(hubId)}/conversations/${encodeURIComponent(conversationId)}/sandbox`;
14033
- addApiBreadcrumb("GET", path39);
14034
- 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}`;
14035
14087
  let response = await this.send(url, "GET");
14036
14088
  if (response.status === 401 && this.onUnauthorized) {
14037
14089
  let refreshed;
@@ -14045,7 +14097,7 @@ var init_api_client = __esm({
14045
14097
  }
14046
14098
  }
14047
14099
  if (!response.ok) {
14048
- throw new ApiError("GET", path39, response.status, await response.text());
14100
+ throw new ApiError("GET", path41, response.status, await response.text());
14049
14101
  }
14050
14102
  return new Uint8Array(await response.arrayBuffer());
14051
14103
  }
@@ -14091,9 +14143,9 @@ var init_api_client = __esm({
14091
14143
  `/api/admin/data-explorer/debug/observability/${encodeURIComponent(hubId)}/conversations/${encodeURIComponent(conversationId)}${qs}`
14092
14144
  );
14093
14145
  }
14094
- async request(method, path39, body, extraHeaders, contentType) {
14095
- addApiBreadcrumb(method, path39);
14096
- const url = `${this.apiUrl}${path39}`;
14146
+ async request(method, path41, body, extraHeaders, contentType) {
14147
+ addApiBreadcrumb(method, path41);
14148
+ const url = `${this.apiUrl}${path41}`;
14097
14149
  let refreshedOn401 = false;
14098
14150
  for (let retry = 0; ; retry++) {
14099
14151
  let response = await this.send(url, method, body, extraHeaders, contentType);
@@ -14118,7 +14170,7 @@ var init_api_client = __esm({
14118
14170
  await delay(RETRYABLE_BACKOFF_MS[retry]);
14119
14171
  continue;
14120
14172
  }
14121
- throw new ApiError(method, path39, response.status, errorBody);
14173
+ throw new ApiError(method, path41, response.status, errorBody);
14122
14174
  }
14123
14175
  }
14124
14176
  /**
@@ -14180,9 +14232,9 @@ var init_api_client = __esm({
14180
14232
  * transient-retry loop — re-streaming a blob is not worth a cold-start probe.
14181
14233
  */
14182
14234
  async dataDownload(v1Path, orgId) {
14183
- const path39 = withOrgSelector(toDataProxyPath(v1Path), orgId);
14184
- addApiBreadcrumb("GET", path39);
14185
- const url = `${this.apiUrl}${path39}`;
14235
+ const path41 = withOrgSelector(toDataProxyPath(v1Path), orgId);
14236
+ addApiBreadcrumb("GET", path41);
14237
+ const url = `${this.apiUrl}${path41}`;
14186
14238
  let response = await this.send(url, "GET");
14187
14239
  if (response.status === 401 && this.onUnauthorized) {
14188
14240
  let refreshed;
@@ -14196,7 +14248,7 @@ var init_api_client = __esm({
14196
14248
  }
14197
14249
  }
14198
14250
  if (!response.ok) {
14199
- throw new ApiError("GET", path39, response.status, await response.text());
14251
+ throw new ApiError("GET", path41, response.status, await response.text());
14200
14252
  }
14201
14253
  return {
14202
14254
  bytes: new Uint8Array(await response.arrayBuffer()),
@@ -14267,6 +14319,15 @@ function extractApiErrorCode(err) {
14267
14319
  return null;
14268
14320
  }
14269
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
+ }
14270
14331
  function extractApiRetryAfterSeconds(err) {
14271
14332
  if (!(err instanceof ApiError)) return null;
14272
14333
  try {
@@ -14433,10 +14494,10 @@ var init_dist = __esm({
14433
14494
  "../../packages/core/dist/index.js"() {
14434
14495
  "use strict";
14435
14496
  init_chunk_WSROLRJN();
14436
- init_chunk_GTTWALH3();
14497
+ init_chunk_5RGL5RXH();
14437
14498
  init_chunk_VORPHGDT();
14438
14499
  init_chunk_POL7B3EM();
14439
- init_chunk_T2RKGBYS();
14500
+ init_chunk_46WU2TD4();
14440
14501
  init_chunk_SNA2DXAC();
14441
14502
  init_chunk_MIWZOSPI();
14442
14503
  init_chunk_LKBSHANA();
@@ -15106,6 +15167,12 @@ function writeFileNoFollow(root, abs, data) {
15106
15167
  fs.writeFileSync(abs, data);
15107
15168
  return true;
15108
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
+ }
15109
15176
  function createFileNoFollow(root, abs, data) {
15110
15177
  if (!ensureRealSubdirNoSymlink(root, path.dirname(abs), true)) return "refused";
15111
15178
  try {
@@ -15598,14 +15665,14 @@ function readOrgFields(doc) {
15598
15665
  if (name) config.organization_name = name;
15599
15666
  return { kind: "declared", config };
15600
15667
  }
15601
- function declarationFrom(load13) {
15602
- if (load13.kind === "absent") return { kind: "none" };
15603
- if (load13.kind === "malformed") return { kind: "invalid", detail: `it does not parse (${load13.reason})` };
15604
- 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);
15605
15672
  }
15606
- function legacyFieldsIn(load13) {
15607
- if (load13.kind !== "ok") return [];
15608
- 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);
15609
15676
  }
15610
15677
  function resolve3(gitRoot) {
15611
15678
  const manifestFile = workspaceManifestPath(gitRoot);
@@ -15791,13 +15858,13 @@ function writeRepoConfig(config, root) {
15791
15858
  if (stale.kind === "declared" && stale.config.organization_id !== config.organization_id) {
15792
15859
  throw expected(rebindRefusal(config.organization_id, stale.config.organization_id));
15793
15860
  }
15794
- const load13 = loadWorkspaceManifest(gitRoot);
15795
- if (load13.kind === "malformed") {
15861
+ const load14 = loadWorkspaceManifest(gitRoot);
15862
+ if (load14.kind === "malformed") {
15796
15863
  throw expected(
15797
- `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.`
15798
15865
  );
15799
15866
  }
15800
- 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 : {};
15801
15868
  const doc = { organization_id: config.organization_id };
15802
15869
  if (config.organization_name?.trim()) doc.organization_name = config.organization_name.trim();
15803
15870
  Object.assign(doc, rest);
@@ -16355,9 +16422,9 @@ function getVersionCachePath(filename = CLI_CACHE_FILE) {
16355
16422
  }
16356
16423
  function readVersionCache(filename = CLI_CACHE_FILE) {
16357
16424
  try {
16358
- const path39 = getVersionCachePath(filename);
16359
- if (!existsSync3(path39)) return null;
16360
- 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"));
16361
16428
  if (typeof parsed.lastCheck !== "number") return null;
16362
16429
  if (parsed.latest !== null && typeof parsed.latest !== "string") return null;
16363
16430
  return parsed;
@@ -16377,10 +16444,10 @@ function isVersionCacheStale(filename = CLI_CACHE_FILE, maxAgeMs = MAX_AGE_24H_M
16377
16444
  return Date.now() - cache.lastCheck > maxAgeMs;
16378
16445
  }
16379
16446
  function writeVersionCache(filename, cache) {
16380
- const path39 = getVersionCachePath(filename);
16381
- const dir = dirname6(path39);
16447
+ const path41 = getVersionCachePath(filename);
16448
+ const dir = dirname6(path41);
16382
16449
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
16383
- writeFileSync2(path39, JSON.stringify(cache));
16450
+ writeFileSync2(path41, JSON.stringify(cache));
16384
16451
  }
16385
16452
  function touchVersionCache(filename) {
16386
16453
  writeVersionCache(filename, { lastCheck: Date.now(), latest: readVersionCache(filename)?.latest ?? null });
@@ -16419,14 +16486,14 @@ function parseFrontmatterVersion(content) {
16419
16486
  function findInstalledSkills(projectRoot, paths = SKILL_INSTALL_PATHS) {
16420
16487
  const found = [];
16421
16488
  for (const rel of paths) {
16422
- const path39 = join8(projectRoot, rel);
16423
- if (!existsSync4(path39)) continue;
16489
+ const path41 = join8(projectRoot, rel);
16490
+ if (!existsSync4(path41)) continue;
16424
16491
  let version = null;
16425
16492
  try {
16426
- version = parseFrontmatterVersion(readFileSync6(path39, "utf-8"));
16493
+ version = parseFrontmatterVersion(readFileSync6(path41, "utf-8"));
16427
16494
  } catch {
16428
16495
  }
16429
- found.push({ path: path39, version });
16496
+ found.push({ path: path41, version });
16430
16497
  }
16431
16498
  return found;
16432
16499
  }
@@ -17958,18 +18025,22 @@ var init_init = __esm({
17958
18025
  import * as fs9 from "fs";
17959
18026
  import * as path11 from "path";
17960
18027
  import * as yaml6 from "js-yaml";
17961
- function readCommunityId(folder) {
17962
- 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);
17963
18035
  if (bytes === null) return null;
17964
- let doc;
17965
18036
  try {
17966
- doc = yaml6.load(bytes.toString("utf-8"));
18037
+ return uuidField(yaml6.load(bytes.toString("utf-8")), key);
17967
18038
  } catch {
17968
18039
  return null;
17969
18040
  }
17970
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
17971
- const id = doc.community_id;
17972
- 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");
17973
18044
  }
17974
18045
  function listCommunityFolders(communitiesDir) {
17975
18046
  let entries;
@@ -19607,9 +19678,9 @@ var init_base_workspace = __esm({
19607
19678
  // src/lib/subtree-routing.ts
19608
19679
  import * as path20 from "path";
19609
19680
  function readRepoDefaults(gitRoot) {
19610
- const load13 = loadWorkspaceManifest(gitRoot);
19611
- if (load13.kind !== "ok") return {};
19612
- const raw = load13.doc;
19681
+ const load14 = loadWorkspaceManifest(gitRoot);
19682
+ if (load14.kind !== "ok") return {};
19683
+ const raw = load14.doc;
19613
19684
  const str = (v) => {
19614
19685
  if (typeof v !== "string" || !v.trim()) return void 0;
19615
19686
  const value = v.trim();
@@ -19636,7 +19707,13 @@ function parseRoutingTokens(args2) {
19636
19707
  if (name === "--base") tokens2.baseFlag ??= value;
19637
19708
  if (name === "--community") tokens2.communityFlag ??= value;
19638
19709
  if (name === "--org") tokens2.orgFlag ??= value;
19639
- if (eq === -1) i++;
19710
+ if (eq === -1) {
19711
+ const taken = value.split("=")[0];
19712
+ if (SUBTREE_FLAGS.some(({ flag }) => flag === taken)) tokens2.takenAsValue ??= { flag: taken, by: name };
19713
+ i++;
19714
+ }
19715
+ } else {
19716
+ tokens2.trailingFlag = name;
19640
19717
  }
19641
19718
  continue;
19642
19719
  }
@@ -19705,31 +19782,42 @@ function hubAndBaseRefusal(verb, hubTarget, baseTarget) {
19705
19782
  ]);
19706
19783
  }
19707
19784
  function subtreeFlags(tokens2) {
19708
- const flags = [];
19709
- if (tokens2.hubFlag !== void 0) flags.push({ subtree: "hubs", target: tokens2.hubFlag });
19710
- if (tokens2.baseFlag !== void 0) flags.push({ subtree: "bases", target: tokens2.baseFlag });
19711
- if (tokens2.communityFlag !== void 0) flags.push({ subtree: "communities", target: tokens2.communityFlag });
19712
- return flags;
19785
+ return SUBTREE_FLAGS.flatMap(({ token, subtree }) => {
19786
+ const target = tokens2[token];
19787
+ return target === void 0 ? [] : [{ subtree, target }];
19788
+ });
19713
19789
  }
19714
19790
  function routeSubtree(options) {
19715
19791
  const result = routeToSubtree(options);
19716
19792
  if (!result.ok || result.subtree === "communities") return result;
19717
19793
  const communityOnly = (COMMUNITY_ONLY_FLAGS[options.verb] ?? []).find((flag) => options.args.includes(flag));
19718
- if (communityOnly === void 0) return result;
19719
- return {
19720
- ok: false,
19721
- refusal: [
19722
- // States only what holds for every flag and verb in the table: this target does not read the
19723
- // flag, and a community's pull does (`--dry-run` is also read by some pushes, `--theirs` by none).
19724
- `\`wayai ${options.verb}\` of ${SUBDIR[result.subtree]}/ does not read \`${communityOnly}\`, so it is refused rather than run without it. A community's pull takes it:`,
19725
- ` wayai pull ${SUBDIR.communities}/<folder> ${communityOnly}`
19726
- ].join("\n")
19727
- };
19794
+ if (communityOnly !== void 0) {
19795
+ return {
19796
+ ok: false,
19797
+ refusal: [
19798
+ // States only what holds for every flag and verb in the table: this target does not read the
19799
+ // flag, and a community's pull does (`--dry-run` is also read by some pushes, `--theirs` by none).
19800
+ `\`wayai ${options.verb}\` of ${SUBDIR[result.subtree]}/ does not read \`${communityOnly}\`, so it is refused rather than run without it. A community's pull takes it:`,
19801
+ ` wayai pull ${SUBDIR.communities}/<folder> ${communityOnly}`
19802
+ ].join("\n")
19803
+ };
19804
+ }
19805
+ if (options.verb === "push" && result.subtree === "hubs" && options.args.includes("--dry-run")) {
19806
+ return {
19807
+ ok: false,
19808
+ refusal: [
19809
+ `\`wayai push\` of ${SUBDIR.hubs}/ does not read \`--dry-run\`, so it is refused rather than run without it. For a hub already created (a \`hub_id\` in its hub.yaml), \`wayai diff\` lists the changes without pushing them:`,
19810
+ " wayai diff --hub <uuid|folder-name>"
19811
+ ].join("\n")
19812
+ };
19813
+ }
19814
+ return result;
19728
19815
  }
19729
19816
  function routeToSubtree(options) {
19730
19817
  const { verb, args: args2, gitRoot } = options;
19731
19818
  const cwd = options.cwd ?? process.cwd();
19732
- const { hubFlag, baseFlag, communityFlag, positional, orgFlag } = parseRoutingTokens(args2);
19819
+ const tokens2 = parseRoutingTokens(args2);
19820
+ const { hubFlag, baseFlag, communityFlag, positional, orgFlag } = tokens2;
19733
19821
  if (orgFlag !== void 0) {
19734
19822
  return {
19735
19823
  ok: false,
@@ -19742,18 +19830,26 @@ function routeToSubtree(options) {
19742
19830
  ].join("\n")
19743
19831
  };
19744
19832
  }
19745
- const layout = gitRoot ? resolveLayout(gitRoot) : null;
19746
- const flagged = subtreeFlags({ hubFlag, baseFlag, communityFlag });
19747
- if (flagged.length > 1) {
19748
- return { ok: false, refusal: mixedInvocationRefusal(verb, flagged) };
19833
+ if (tokens2.takenAsValue) {
19834
+ const { flag: flag2, by } = tokens2.takenAsValue;
19835
+ return {
19836
+ ok: false,
19837
+ refusal: `\`wayai ${verb} ${by}\` took \`${flag2}\` as its value, so \`${flag2}\` is not read. Give ${by} a value, or leave it out.`
19838
+ };
19749
19839
  }
19750
- if (communityFlag === void 0 && args2.includes("--community")) {
19840
+ const empty = SUBTREE_FLAGS.find(({ flag: flag2, token }) => tokens2[token] === "" || tokens2.trailingFlag === flag2);
19841
+ if (empty) {
19751
19842
  return {
19752
19843
  ok: false,
19753
- refusal: `\`wayai ${verb} --community\` needs a community, and none followed it.
19754
- wayai ${verb} --community <folder|community_id>`
19844
+ refusal: `\`wayai ${verb} ${empty.flag}\` needs ${empty.target}, and none followed it.
19845
+ wayai ${verb} ${empty.flag} ${empty.value}`
19755
19846
  };
19756
19847
  }
19848
+ const layout = gitRoot ? resolveLayout(gitRoot) : null;
19849
+ const flagged = subtreeFlags({ hubFlag, baseFlag, communityFlag });
19850
+ if (flagged.length > 1) {
19851
+ return { ok: false, refusal: mixedInvocationRefusal(verb, flagged) };
19852
+ }
19757
19853
  const fromPositional = positional !== void 0 ? resolveSelectorSubtree(layout, positional, cwd) : void 0;
19758
19854
  if (fromPositional?.kind === "both") {
19759
19855
  return {
@@ -19812,7 +19908,7 @@ function requireSubtree(verb, args2, gitRoot) {
19812
19908
  }
19813
19909
  return { subtree: result.subtree, selector: result.selector };
19814
19910
  }
19815
- var VALUE_FLAGS, COMMUNITY_ONLY_FLAGS, SUBDIR;
19911
+ var VALUE_FLAGS, SUBTREE_FLAGS, COMMUNITY_ONLY_FLAGS, SUBDIR;
19816
19912
  var init_subtree_routing = __esm({
19817
19913
  "src/lib/subtree-routing.ts"() {
19818
19914
  "use strict";
@@ -19822,6 +19918,11 @@ var init_subtree_routing = __esm({
19822
19918
  init_workspace();
19823
19919
  init_base_workspace();
19824
19920
  VALUE_FLAGS = /* @__PURE__ */ new Set(["--hub", "--base", "--community", "--label", "--org"]);
19921
+ SUBTREE_FLAGS = [
19922
+ { flag: "--hub", token: "hubFlag", subtree: "hubs", target: "a hub", value: "<uuid|folder-name>" },
19923
+ { flag: "--base", token: "baseFlag", subtree: "bases", target: "a base", value: "<id|folder-name>" },
19924
+ { flag: "--community", token: "communityFlag", subtree: "communities", target: "a community", value: "<folder|community_id>" }
19925
+ ];
19825
19926
  COMMUNITY_ONLY_FLAGS = { pull: ["--theirs", "--dry-run"], push: ["--theirs"] };
19826
19927
  SUBDIR = {
19827
19928
  hubs: WAYAI_LAYOUT.hubsSubdir,
@@ -19864,12 +19965,12 @@ async function createDataClient(orgId) {
19864
19965
  return orgId2;
19865
19966
  }
19866
19967
  return {
19867
- async request(method, path39, body) {
19868
- 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);
19869
19970
  return envelope.data;
19870
19971
  },
19871
- async requestRaw(method, path39, body) {
19872
- 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);
19873
19974
  return body_;
19874
19975
  },
19875
19976
  async collectPages(makePath) {
@@ -20713,6 +20814,133 @@ var init_sync = __esm({
20713
20814
  }
20714
20815
  });
20715
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
+
20716
20944
  // src/communities/api.ts
20717
20945
  async function communitiesClient() {
20718
20946
  const { config, accessToken } = await requireAuth();
@@ -20845,6 +21073,23 @@ function settingsContent(settings) {
20845
21073
  function roomContent(room) {
20846
21074
  return { name: toLf(room.name).trim(), archived: room.archived };
20847
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
+ }
20848
21093
  function rev(parts) {
20849
21094
  return `${REV_VERSION}:${createHash3("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 12)}`;
20850
21095
  }
@@ -20856,15 +21101,31 @@ function serverRoom(room) {
20856
21101
  const content = roomContent(room);
20857
21102
  return { content, rev: roomRev(room.room_id, content) };
20858
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
+ }
20859
21119
  function classify(base, file, server) {
20860
- const known = base !== void 0 && base.startsWith(`${REV_VERSION}:`) ? base : void 0;
21120
+ const known = knownBase(base);
20861
21121
  if (known === void 0) return file === server ? "converged" : "conflict";
20862
- const fileChanged = file !== known;
20863
- const serverChanged = server !== known;
21122
+ return threeWay(file !== known, server !== known, file === server);
21123
+ }
21124
+ function threeWay(fileChanged, serverChanged, agree) {
20864
21125
  if (!fileChanged && !serverChanged) return "in_sync";
20865
- if (fileChanged && !serverChanged) return "local";
21126
+ if (!serverChanged) return "local";
20866
21127
  if (!fileChanged) return "remote";
20867
- return file === server ? "converged" : "conflict";
21128
+ return agree ? "converged" : "conflict";
20868
21129
  }
20869
21130
  function createClientId(communityId, kind, localName2) {
20870
21131
  return uuidV5(communityId, `${kind}:${localName2}`);
@@ -20875,7 +21136,7 @@ function localName(name, id, taken, fallback) {
20875
21136
  const withPrefix = `${slug}--${id.slice(0, 8)}`;
20876
21137
  return taken.has(withPrefix) ? `${slug}--${id}` : withPrefix;
20877
21138
  }
20878
- var REV_VERSION, settingsRev, joinLinkRev, roomRev;
21139
+ var REV_VERSION, settingsRev, joinLinkRev, roomRev, courseRev, moduleRev, lessonRev;
20879
21140
  var init_model = __esm({
20880
21141
  "src/communities/model.ts"() {
20881
21142
  "use strict";
@@ -20884,10 +21145,25 @@ var init_model = __esm({
20884
21145
  settingsRev = (communityId, c) => rev(["settings", communityId, c.rules, c.slow_mode_seconds, c.courses_label]);
20885
21146
  joinLinkRev = (communityId, c) => rev(["join_link", communityId, c.enabled]);
20886
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
+ ]);
20887
21162
  }
20888
21163
  });
20889
21164
 
20890
- // src/communities/files.ts
21165
+ // src/communities/course-files.ts
21166
+ import * as fs18 from "fs";
20891
21167
  import * as path24 from "path";
20892
21168
  import * as yaml12 from "js-yaml";
20893
21169
  function isMapping(value) {
@@ -20904,21 +21180,359 @@ function optionalString(doc, key, where, problems2) {
20904
21180
  if (typeof value !== "string") problems2.push(`${where}: ${key} must be text`);
20905
21181
  return typeof value === "string" ? value : void 0;
20906
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
+ }
20907
21196
  function contractProblem(error) {
20908
21197
  const [issue] = error.issues;
20909
21198
  return issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ${issue.message}` : issue.message;
20910
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";
20911
21525
  function readCommunityFolder(folder) {
20912
- const where = path24.basename(folder);
20913
- const yamlPath = path24.join(folder, COMMUNITY_FILE);
21526
+ const where = path25.basename(folder);
21527
+ const yamlPath = path25.join(folder, COMMUNITY_FILE);
20914
21528
  const text = readRealFileOrThrow(folder, yamlPath);
20915
21529
  if (text === null) {
20916
21530
  throw expected(`No ${COMMUNITY_FILE} in ${where}/: pull the community first (wayai pull communities/<community_id>).`);
20917
21531
  }
20918
- const rules = toLf(readRealFileOrThrow(folder, path24.join(folder, RULES_FILE)) ?? "");
21532
+ const rules = toLf(readRealFileOrThrow(folder, path25.join(folder, RULES_FILE)) ?? "");
20919
21533
  let doc;
20920
21534
  try {
20921
- doc = yaml12.load(text);
21535
+ doc = yaml13.load(text);
20922
21536
  } catch (err) {
20923
21537
  throw expected(`${where}/${COMMUNITY_FILE} is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
20924
21538
  }
@@ -20945,6 +21559,9 @@ function readCommunityFolder(folder) {
20945
21559
  if (typeof doc.join_link_enabled !== "boolean") problems2.push(`${COMMUNITY_FILE}: join_link_enabled must be true or false`);
20946
21560
  const joinLinkRev2 = optionalString(doc, "join_link_rev", COMMUNITY_FILE, problems2);
20947
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));
20948
21565
  if (problems2.length > 0) {
20949
21566
  throw expected(`${where}/ cannot be synced:
20950
21567
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
@@ -20960,8 +21577,44 @@ ${problems2.map((p) => ` - ${p}`).join("\n")}`);
20960
21577
  rules,
20961
21578
  join_link_enabled: doc.join_link_enabled,
20962
21579
  ...joinLinkRev2 === void 0 ? {} : { join_link_rev: joinLinkRev2 },
20963
- rooms
21580
+ rooms,
21581
+ courses,
21582
+ ...synced2 === void 0 ? {} : { synced_courses: synced2 },
21583
+ unlistedCourses: unlisted
21584
+ };
21585
+ }
21586
+ function readCommunitySynced(raw, problems2) {
21587
+ if (raw === void 0 || raw === null) return void 0;
21588
+ if (!isMapping(raw)) {
21589
+ problems2.push(`${COMMUNITY_FILE}: synced must be a mapping`);
21590
+ return void 0;
21591
+ }
21592
+ unknownKeys(raw, ["courses"], `${COMMUNITY_FILE} synced`, problems2);
21593
+ return readIdList(raw.courses, `${COMMUNITY_FILE} synced.courses`, problems2);
21594
+ }
21595
+ function crossFileProblems(rooms, courses) {
21596
+ const problems2 = [];
21597
+ const seen = /* @__PURE__ */ new Map();
21598
+ const once = (id, where) => {
21599
+ if (id === void 0) return;
21600
+ const first = seen.get(id);
21601
+ if (first) problems2.push(`${where}: id ${id} is also ${first}'s \u2014 a copy keeps its original's id; remove the id (and its revs) to make a new one`);
21602
+ else seen.set(id, where);
20964
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;
20965
21618
  }
20966
21619
  function readRooms(raw, problems2) {
20967
21620
  if (raw === void 0 || raw === null) return [];
@@ -21033,31 +21686,30 @@ function renderCommunityYaml(files) {
21033
21686
  name: room.name,
21034
21687
  kind: room.kind,
21035
21688
  ...room.archived ? { archived: true } : {}
21036
- }))
21689
+ })),
21690
+ courses: files.courses.map((course) => course.slug),
21691
+ ...files.synced_courses ? { synced: { courses: files.synced_courses } } : {}
21037
21692
  };
21038
- return yaml12.dump(doc, YAML_DUMP_OPTIONS);
21693
+ return yaml13.dump(doc, YAML_DUMP_OPTIONS);
21039
21694
  }
21040
21695
  function renderRules(rules) {
21041
21696
  const text = toLf(rules).replace(/\n+$/, "");
21042
21697
  return text === "" ? "" : `${text}
21043
21698
  `;
21044
21699
  }
21045
- function writeCommunityFolder(root, folder, files) {
21700
+ function writeCommunityFiles(root, folder, files) {
21046
21701
  const written = [];
21047
21702
  for (const [name, content] of [
21048
21703
  [COMMUNITY_FILE, renderCommunityYaml(files)],
21049
21704
  [RULES_FILE, renderRules(files.rules)]
21050
21705
  ]) {
21051
- const abs = path24.join(folder, name);
21052
- const bytes = Buffer.from(content, "utf-8");
21053
- if (fileHasBytes(abs, bytes)) continue;
21054
- if (!writeFileNoFollow(root, abs, bytes)) {
21055
- throw symlinkRefusal(path24.relative(path24.dirname(root), abs), "file");
21056
- }
21057
- written.push(name);
21706
+ if (writeFileIfChangedNoFollow(root, path25.join(folder, name), content)) written.push(name);
21058
21707
  }
21059
21708
  return written;
21060
21709
  }
21710
+ function writeCommunityFolder(root, folder, files) {
21711
+ return [...writeCommunityFiles(root, folder, files), ...files.courses.flatMap((course) => writeCourseFolder(root, folder, course))];
21712
+ }
21061
21713
  var TOP_KEYS, SETTINGS_KEYS, ROOM_KEYS;
21062
21714
  var init_files = __esm({
21063
21715
  "src/communities/files.ts"() {
@@ -21070,20 +21722,1365 @@ var init_files = __esm({
21070
21722
  init_community_workspace();
21071
21723
  init_utils();
21072
21724
  init_model();
21073
- 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"];
21074
21727
  SETTINGS_KEYS = ["rev", "slow_mode_seconds", "courses_label"];
21075
21728
  ROOM_KEYS = ["key", "id", "rev", "name", "kind", "archived"];
21076
21729
  }
21077
21730
  });
21078
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
+
21079
23076
  // src/communities/sync.ts
21080
23077
  var sync_exports2 = {};
21081
23078
  __export(sync_exports2, {
21082
23079
  pullCommunity: () => pullCommunity,
21083
23080
  pushCommunity: () => pushCommunity
21084
23081
  });
21085
- import * as fs18 from "fs";
21086
- import * as path25 from "path";
23082
+ import * as fs19 from "fs";
23083
+ import * as path27 from "path";
21087
23084
  function parseArgs5(args2) {
21088
23085
  return {
21089
23086
  autoConfirm: args2.includes("--yes") || args2.includes("-y"),
@@ -21144,11 +23141,22 @@ function planSync(files, server) {
21144
23141
  },
21145
23142
  rooms,
21146
23143
  untracked: server.rooms.filter((room) => !tracked.has(room.room_id)),
21147
- unknownIds
23144
+ unknownIds,
23145
+ courses: planCourses(files, server.courses)
21148
23146
  };
21149
23147
  }
21150
23148
  function unknownIdProblems(plan) {
21151
- 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
+ }
21152
23160
  }
21153
23161
  function settingsChanges(from, to) {
21154
23162
  const out = [];
@@ -21157,31 +23165,6 @@ function settingsChanges(from, to) {
21157
23165
  if (from.rules !== to.rules) out.push("rules (rules.md)");
21158
23166
  return out;
21159
23167
  }
21160
- function lineDiff(from, to) {
21161
- const a = from ? sanitizeTerminalText(from).split("\n") : [];
21162
- const b = to ? sanitizeTerminalText(to).split("\n") : [];
21163
- if (a.length * b.length > 25e4) return [...a.map((l) => `- ${l}`), ...b.map((l) => `+ ${l}`)];
21164
- const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
21165
- for (let i2 = a.length - 1; i2 >= 0; i2--) {
21166
- for (let j2 = b.length - 1; j2 >= 0; j2--) {
21167
- lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
21168
- }
21169
- }
21170
- const out = [];
21171
- let i = 0;
21172
- let j = 0;
21173
- while (i < a.length || j < b.length) {
21174
- if (i < a.length && j < b.length && a[i] === b[j]) {
21175
- i++;
21176
- j++;
21177
- } else if (j >= b.length || i < a.length && lcs[i + 1][j] >= lcs[i][j + 1]) {
21178
- out.push(`- ${a[i++]}`);
21179
- } else {
21180
- out.push(`+ ${b[j++]}`);
21181
- }
21182
- }
21183
- return out;
21184
- }
21185
23168
  function roomChanges(from, to) {
21186
23169
  const out = [];
21187
23170
  if (from.name !== to.name) out.push(`name ${show(from.name)} \u2192 ${show(to.name)}`);
@@ -21191,19 +23174,6 @@ function roomChanges(from, to) {
21191
23174
  function joinLinkChange(from, to) {
21192
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`;
21193
23176
  }
21194
- function printSection(title, lines) {
21195
- if (lines.length === 0) return;
21196
- console.log(`
21197
- ${title}`);
21198
- for (const line of lines) console.log(` ${line}`);
21199
- }
21200
- function printListing(listing) {
21201
- printSection("Changes:", listing.changes);
21202
- printSection("Changed on the server since your last pull \u2014 left as is; pull to take it:", listing.serverChanged);
21203
- printSection("Kept \u2014 edited in your files, not pushed yet:", listing.kept);
21204
- printSection("Not in your files \u2014 left as is:", listing.leftAsIs);
21205
- printSection("Conflicts \u2014 changed in your files and on the server since your last pull:", listing.conflicts);
21206
- }
21207
23177
  function pushProblems(files, server, plan) {
21208
23178
  const problems2 = unknownIdProblems(plan);
21209
23179
  if (files.name !== server.community.name) {
@@ -21215,12 +23185,13 @@ function pushProblems(files, server, plan) {
21215
23185
  }
21216
23186
  }
21217
23187
  const creates = plan.rooms.filter((room) => room.kind === "create").length;
21218
- const certainlyNew = Math.max(0, creates - plan.untracked.length);
21219
- 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) {
21220
23190
  problems2.push(
21221
23191
  `rooms: a community holds at most ${COMMUNITY_ROOMS_MAX} rooms, archived ones included; it has ${server.rooms.length}, and the files create ${creates}`
21222
23192
  );
21223
23193
  }
23194
+ problems2.push(...courseCapProblems(files, plan.courses));
21224
23195
  return problems2;
21225
23196
  }
21226
23197
  function settingsPatch(files, server) {
@@ -21260,7 +23231,18 @@ function takeJoinLink(files, enabled) {
21260
23231
  files.join_link_enabled = enabled;
21261
23232
  files.join_link_rev = joinLinkRev(files.community_id, { enabled });
21262
23233
  }
21263
- 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) {
21264
23246
  const writes = [];
21265
23247
  if (plan.settings.state === "local") {
21266
23248
  const body = settingsPatch(files, plan.settings.server);
@@ -21350,8 +23332,8 @@ function refreshConverged(files, plan) {
21350
23332
  }
21351
23333
  return refreshed;
21352
23334
  }
21353
- function pushListing(plan) {
21354
- const listing = { changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] };
23335
+ function pushListing(plan, courses) {
23336
+ const listing = emptyListing();
21355
23337
  const { settings, joinLink } = plan;
21356
23338
  if (settings.state === "local") {
21357
23339
  listing.changes.push(`~ settings: ${settingsChanges(settings.server, settings.file).join("; ")}`);
@@ -21376,6 +23358,7 @@ function pushListing(plan) {
21376
23358
  listing.serverChanged = named("remote");
21377
23359
  listing.conflicts = named("conflict");
21378
23360
  listing.leftAsIs = plan.untracked.map((room) => `room ${show(room.name)} (${room.room_id})`);
23361
+ courses.list(listing);
21379
23362
  return listing;
21380
23363
  }
21381
23364
  async function pushCommunity(gitRootOrNull, selector, args2) {
@@ -21394,37 +23377,39 @@ async function pushCommunity(gitRootOrNull, selector, args2) {
21394
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.");
21395
23378
  }
21396
23379
  const { organization_id: orgId } = requireRepoConfig(gitRoot);
21397
- const label = path25.basename(folder);
23380
+ const label = path27.basename(folder);
21398
23381
  assertInScope("communities", files.community_id, label);
23382
+ warnUnlisted(files);
21399
23383
  const remote = await openRemote(files.community_id);
21400
23384
  const server = await remote.read(orgId);
21401
23385
  const plan = planSync(files, server);
21402
- 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)}/`);
21403
23387
  const problems2 = pushProblems(files, server, plan);
21404
23388
  if (problems2.length > 0) {
21405
23389
  throw expected(`Nothing was pushed:
21406
23390
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
21407
23391
  }
21408
- 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);
21409
23398
  printListing(listing);
21410
23399
  if (listing.conflicts.length > 0) {
21411
23400
  throw expected(`
21412
23401
  Nothing was pushed: ${listing.conflicts.length} conflict(s). ${resolveConflicts(folder)}`);
21413
23402
  }
21414
- const { communitiesDir } = resolveLayout(gitRoot);
21415
- const save = () => {
21416
- writeCommunityFolder(communitiesDir, folder, files);
21417
- };
21418
- const writes = pushWrites(files, plan, remote, save);
21419
- if (writes.length === 0) {
21420
- 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();
21421
23407
  if (refreshed > 0) save();
21422
23408
  console.log(`
21423
- No changes to push${refreshed > 0 ? ` (${refreshed} rev(s) refreshed in ${COMMUNITY_FILE})` : ""}.`);
23409
+ No changes to push${refreshed > 0 ? ` (${refreshed} rev(s) refreshed)` : ""}.`);
21424
23410
  if (!dryRun) seedScopeIfEmpty("communities", files.community_id);
21425
23411
  return;
21426
23412
  }
21427
- const requests = writes.reduce((sum, write) => sum + write.requests, 0);
21428
23413
  console.log(
21429
23414
  `
21430
23415
  This push makes ${requests} request(s) to the live community \u2014 ${paceDuration(requests)} at ${COMMUNITY_REQUESTS_PER_MINUTE} a minute.`
@@ -21446,24 +23431,67 @@ This push makes ${requests} request(s) to the live community \u2014 ${paceDurati
21446
23431
  }
21447
23432
  }
21448
23433
  refreshConverged(files, plan);
23434
+ courses.refreshConverged();
21449
23435
  save();
21450
- for (const [index, write] of writes.entries()) {
21451
- try {
21452
- await write.apply();
21453
- } catch (err) {
21454
- throw expected(
21455
- `Push stopped at ${write.label}: ${stopReason(err)}
21456
- What it applied before stopping is on the community, and ${COMMUNITY_FILE} records it. Fix the cause and push again.`
21457
- );
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
+ }
21458
23459
  }
21459
- console.log(` [${index + 1}/${writes.length}] ${write.label}`);
23460
+ } finally {
23461
+ stop.release();
21460
23462
  }
21461
23463
  console.log(
21462
23464
  `
21463
- 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.`
21464
23466
  );
21465
23467
  seedScopeIfEmpty("communities", files.community_id);
21466
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
+ }
21467
23495
  function changedSince(before, after) {
21468
23496
  const id = before.community.community_id;
21469
23497
  const changed = [];
@@ -21475,6 +23503,7 @@ function changedSince(before, after) {
21475
23503
  for (const room of after.rooms) {
21476
23504
  if (revs.get(room.room_id) !== serverRoom(room).rev) changed.push(`room ${show(room.name)}`);
21477
23505
  }
23506
+ changed.push(...coursesChangedSince(before.courses, after.courses));
21478
23507
  return changed;
21479
23508
  }
21480
23509
  function stopReason(err) {
@@ -21498,15 +23527,18 @@ function filesFromServer(server) {
21498
23527
  settings: { slow_mode_seconds: 0, courses_label: "" },
21499
23528
  rules: "",
21500
23529
  join_link_enabled: false,
21501
- rooms: []
23530
+ rooms: [],
23531
+ courses: [],
23532
+ unlistedCourses: []
21502
23533
  };
21503
23534
  takeSettings(files, server.settings);
21504
23535
  takeJoinLink(files, server.joinLink.enabled);
21505
23536
  addUntracked(files, server.rooms);
23537
+ coursesFromServer(files, server.courses);
21506
23538
  return files;
21507
23539
  }
21508
23540
  function pullMerge(files, server, plan, theirs) {
21509
- const listing = { changes: [], serverChanged: [], conflicts: [], leftAsIs: [], kept: [] };
23541
+ const listing = emptyListing();
21510
23542
  const takes = (state) => state === "remote" || state === "conflict" && theirs;
21511
23543
  const note = (state, what) => {
21512
23544
  if (state === "local") listing.kept.push(what);
@@ -21544,6 +23576,7 @@ function pullMerge(files, server, plan, theirs) {
21544
23576
  }
21545
23577
  refreshConverged(files, plan);
21546
23578
  listing.changes.push(...addUntracked(files, plan.untracked));
23579
+ pullCourses(files, plan.courses, takes, note, listing);
21547
23580
  return listing;
21548
23581
  }
21549
23582
  async function pullCommunity(gitRootOrNull, selector, args2) {
@@ -21559,26 +23592,28 @@ async function pullCommunity(gitRootOrNull, selector, args2) {
21559
23592
  communityId = existing.files.community_id;
21560
23593
  }
21561
23594
  const { organization_id: orgId } = requireRepoConfig(gitRoot);
21562
- assertInScope("communities", communityId, existing ? path25.basename(existing.folder) : void 0);
23595
+ assertInScope("communities", communityId, existing ? path27.basename(existing.folder) : void 0);
21563
23596
  const { communitiesDir } = resolveLayout(gitRoot);
21564
23597
  const remote = await openRemote(communityId);
21565
23598
  const server = await remote.read(orgId);
21566
23599
  if (!existing) {
21567
23600
  const taken = new Set(safeReaddir2(communitiesDir));
21568
- 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"));
21569
23602
  const files = filesFromServer(server);
21570
- 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)`;
21571
23605
  if (dryRun) {
21572
- 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}.`);
21573
23607
  return;
21574
23608
  }
21575
23609
  requireRealSubdirNoSymlink(gitRoot, communitiesDir, true);
21576
23610
  writeCommunityFolder(communitiesDir, folder2, files);
21577
- 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.`);
21578
23612
  seedScopeIfEmpty("communities", communityId);
21579
23613
  return;
21580
23614
  }
21581
23615
  const { folder, files: local } = existing;
23616
+ warnUnlisted(local);
21582
23617
  const plan = planSync(local, server);
21583
23618
  const problems2 = unknownIdProblems(plan);
21584
23619
  if (problems2.length > 0) {
@@ -21586,7 +23621,7 @@ async function pullCommunity(gitRootOrNull, selector, args2) {
21586
23621
  ${problems2.map((p) => ` - ${p}`).join("\n")}`);
21587
23622
  }
21588
23623
  const listing = pullMerge(local, server, plan, theirs);
21589
- 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)}/`);
21590
23625
  printListing(listing);
21591
23626
  if (dryRun) {
21592
23627
  console.log("\nDry run \u2014 nothing was written.");
@@ -21601,16 +23636,16 @@ Wrote ${written.join(", ")}.` : "\nAlready up to date.");
21601
23636
  }
21602
23637
  }
21603
23638
  function resolveConflicts(folder) {
21604
- 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.`;
21605
23640
  }
21606
23641
  function safeReaddir2(dir) {
21607
23642
  try {
21608
- return fs18.readdirSync(dir);
23643
+ return fs19.readdirSync(dir);
21609
23644
  } catch {
21610
23645
  return [];
21611
23646
  }
21612
23647
  }
21613
- var show, Remote, onOff;
23648
+ var Remote, onOff, plannedCount, plannedRequests;
21614
23649
  var init_sync2 = __esm({
21615
23650
  "src/communities/sync.ts"() {
21616
23651
  "use strict";
@@ -21626,11 +23661,14 @@ var init_sync2 = __esm({
21626
23661
  init_worktree_scope();
21627
23662
  init_community_workspace();
21628
23663
  init_helpers();
23664
+ init_eval_session_control();
21629
23665
  init_api2();
21630
23666
  init_pacer();
21631
23667
  init_files();
23668
+ init_course_files();
23669
+ init_courses();
23670
+ init_listing();
21632
23671
  init_model();
21633
- show = (text) => JSON.stringify(sanitizeTerminalText(text));
21634
23672
  Remote = class {
21635
23673
  constructor(api, pacer, communityId) {
21636
23674
  this.api = api;
@@ -21641,8 +23679,9 @@ var init_sync2 = __esm({
21641
23679
  return (await callCommunities(this.pacer.run(request), notFound)).data;
21642
23680
  }
21643
23681
  /**
21644
- * The four reads (gh #5003 Decision 8's bounded read, courses aside). The org's list both binds
21645
- * 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.
21646
23685
  */
21647
23686
  async read(orgId) {
21648
23687
  const communities = await this.send(() => this.api.listOrgCommunities(orgId), orgNotFound(orgId));
@@ -21652,12 +23691,14 @@ var init_sync2 = __esm({
21652
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.`
21653
23692
  );
21654
23693
  }
21655
- const [settings, rooms, joinLink] = await Promise.all([
23694
+ const [settings, rooms, joinLink, courses] = await Promise.all([
21656
23695
  this.send(() => this.api.getCommunitySettings(this.communityId)),
21657
23696
  this.send(() => this.api.listCommunityRooms(this.communityId)),
21658
- 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))))
21659
23700
  ]);
21660
- return { community, settings, rooms, joinLink };
23701
+ return { community, settings, rooms, joinLink, courses: new ServerCourses(courses) };
21661
23702
  }
21662
23703
  updateSettings(body) {
21663
23704
  return this.send(() => this.api.updateCommunitySettings(this.communityId, body));
@@ -21672,8 +23713,34 @@ var init_sync2 = __esm({
21672
23713
  updateRoom(roomId, body) {
21673
23714
  return this.send(() => this.api.updateCommunityRoom(this.communityId, roomId, body));
21674
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
+ }
21675
23740
  };
21676
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;
21677
23744
  }
21678
23745
  });
21679
23746
 
@@ -21692,9 +23759,9 @@ __export(push_exports, {
21692
23759
  shouldWarnIgnoredPreviewLabel: () => shouldWarnIgnoredPreviewLabel,
21693
23760
  syncAfterPush: () => syncAfterPush
21694
23761
  });
21695
- import * as fs19 from "fs";
21696
- import * as path26 from "path";
21697
- 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";
21698
23765
  function parseArgs6(args2) {
21699
23766
  let autoConfirm = false;
21700
23767
  let label;
@@ -21799,14 +23866,14 @@ function printLocalFileChanges(delta) {
21799
23866
  }
21800
23867
  async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, remoteConfig) {
21801
23868
  requireRealHubFolder(hubFolder, false);
21802
- const agentsDir = path26.join(hubFolder, "agents");
23869
+ const agentsDir = path28.join(hubFolder, "agents");
21803
23870
  let agentsWithIds = [];
21804
- if (fs19.existsSync(agentsDir)) {
21805
- 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"));
21806
23873
  for (const file of yamlFiles) {
21807
- const content = readRealFileOrThrow(hubFolder, path26.join(agentsDir, file)) ?? "";
23874
+ const content = readRealFileOrThrow(hubFolder, path28.join(agentsDir, file)) ?? "";
21808
23875
  try {
21809
- const agent = yaml13.load(content);
23876
+ const agent = yaml14.load(content);
21810
23877
  if (agent?.id && agent.name) {
21811
23878
  agentsWithIds.push({ id: agent.id, name: agent.name });
21812
23879
  }
@@ -21819,7 +23886,7 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21819
23886
  const yamlPath = resolveHubYamlPath(hubFolder);
21820
23887
  if (!yamlPath) return;
21821
23888
  const yamlContent = readRealFileOrThrow(hubFolder, yamlPath) ?? "";
21822
- const config = yaml13.load(yamlContent);
23889
+ const config = yaml14.load(yamlContent);
21823
23890
  agentsWithIds = (config.agents || []).filter((a) => !!a.id && !!a.name);
21824
23891
  }
21825
23892
  if (agentsWithIds.length === 0) return;
@@ -21850,18 +23917,18 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21850
23917
  }
21851
23918
  }
21852
23919
  if (renames.length === 0) return;
21853
- if (!fs19.existsSync(agentsDir)) return;
21854
- for (const file of fs19.readdirSync(agentsDir)) {
23920
+ if (!fs20.existsSync(agentsDir)) return;
23921
+ for (const file of fs20.readdirSync(agentsDir)) {
21855
23922
  if (file.startsWith("__rename_temp_") && (file.endsWith(".md") || file.endsWith(".yaml"))) {
21856
23923
  console.warn(` Warning: removing orphaned temp file agents/${file}`);
21857
- fs19.unlinkSync(path26.join(agentsDir, file));
23924
+ fs20.unlinkSync(path28.join(agentsDir, file));
21858
23925
  }
21859
23926
  }
21860
23927
  const renameFileIfExists = (dir, oldName, newName) => {
21861
- const oldPath = path26.join(dir, oldName);
21862
- const newPath = path26.join(dir, newName);
21863
- if (!fs19.existsSync(oldPath)) return false;
21864
- 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);
21865
23932
  return true;
21866
23933
  };
21867
23934
  const oldSlugs = new Set(renames.map((r) => r.oldSlug));
@@ -21894,9 +23961,9 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21894
23961
  }
21895
23962
  } else {
21896
23963
  for (const { oldSlug, newSlug } of renames) {
21897
- 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}`)));
21898
23965
  if (!hasOldFile) continue;
21899
- 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}`)));
21900
23967
  if (hasNewFile) {
21901
23968
  console.warn(` Warning: skipping rename agents/${oldSlug}.* \u2192 agents/${newSlug}.* (target already exists)`);
21902
23969
  continue;
@@ -21923,7 +23990,7 @@ async function autoRenameAgentFiles(client, hubId, hubFolder, organizationId, re
21923
23990
  );
21924
23991
  if (updatedYaml !== mainYamlContent) {
21925
23992
  writeFileNoFollow(hubFolder, mainYamlPath, Buffer.from(updatedYaml, "utf-8"));
21926
- console.log(` Updated instructions paths in ${path26.basename(mainYamlPath)}`);
23993
+ console.log(` Updated instructions paths in ${path28.basename(mainYamlPath)}`);
21927
23994
  }
21928
23995
  }
21929
23996
  }
@@ -21991,7 +24058,7 @@ async function pushSingleHub(client, hubId, hubFolder, autoConfirm, organization
21991
24058
  function selectExistingHub(workspaceDir, allHubs, selector, wsLabel) {
21992
24059
  if (selector) {
21993
24060
  const match = allHubs.find(
21994
- (h) => h.hubId === selector || path26.basename(h.hubFolder) === selector
24061
+ (h) => h.hubId === selector || path28.basename(h.hubFolder) === selector
21995
24062
  );
21996
24063
  if (!match) {
21997
24064
  console.error(`No hub matching --hub ${selector} found in ${wsLabel}/.`);
@@ -22060,7 +24127,7 @@ function reportUnresolvedPushTarget(reason, existingHubs, newHubs, selector, wsL
22060
24127
  if (existingHubs.length > 0) {
22061
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\`:`);
22062
24129
  for (const h of existingHubs) {
22063
- console.error(` ${path26.basename(h.hubFolder)} (${h.hubId}, production mirror)`);
24130
+ console.error(` ${path28.basename(h.hubFolder)} (${h.hubId}, production mirror)`);
22064
24131
  }
22065
24132
  } else {
22066
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\`).`);
@@ -22074,10 +24141,10 @@ function reportUnresolvedPushTarget(reason, existingHubs, newHubs, selector, wsL
22074
24141
  console.error(`Multiple hub folders found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22075
24142
  }
22076
24143
  for (const h of existingHubs) {
22077
- console.error(` ${path26.basename(h.hubFolder)} (${h.hubId})`);
24144
+ console.error(` ${path28.basename(h.hubFolder)} (${h.hubId})`);
22078
24145
  }
22079
24146
  for (const h of newHubs) {
22080
- console.error(` ${path26.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
24147
+ console.error(` ${path28.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
22081
24148
  }
22082
24149
  if (hint) console.error(hint);
22083
24150
  process.exit(1);
@@ -22104,7 +24171,7 @@ async function pushCommand(args2) {
22104
24171
  const client = new ApiClient({ apiUrl: config.api_url, accessToken });
22105
24172
  const workspaceDir = resolveWorkspaceDir();
22106
24173
  const wsLabel = hubsDirLabel(gitRoot);
22107
- if (!fs19.existsSync(workspaceDir)) {
24174
+ if (!fs20.existsSync(workspaceDir)) {
22108
24175
  console.error(`No ${wsLabel}/ directory found. Run \`wayai pull\` first or create hub files in ${wsLabel}/<hub>/hub.yaml.`);
22109
24176
  const hint = communityFoldersHint(gitRoot, "push");
22110
24177
  if (hint) console.error(hint);
@@ -22126,8 +24193,8 @@ async function pushCommand(args2) {
22126
24193
  if (label) {
22127
24194
  console.warn("Note: --label is only used when creating a new hub. To change an existing hub's label, use `wayai relabel`.");
22128
24195
  }
22129
- console.log(`Target hub: ${path26.basename(existing.hubFolder)} (${existing.hubId})`);
22130
- 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));
22131
24198
  await pushSingleHub(client, existing.hubId, existing.hubFolder, autoConfirm, organizationId);
22132
24199
  return;
22133
24200
  }
@@ -22165,8 +24232,8 @@ __export(pull_exports, {
22165
24232
  resolveHubTarget: () => resolveHubTarget,
22166
24233
  writeProductionMirror: () => writeProductionMirror2
22167
24234
  });
22168
- import * as fs20 from "fs";
22169
- import * as path27 from "path";
24235
+ import * as fs21 from "fs";
24236
+ import * as path29 from "path";
22170
24237
  function parseArgs7(args2) {
22171
24238
  return { autoConfirm: args2.includes("--yes") || args2.includes("-y") };
22172
24239
  }
@@ -22176,7 +24243,7 @@ function resolveHubTarget(workspaceDir, selector) {
22176
24243
  const allHubs = scanWorkspaceHubs(workspaceDir);
22177
24244
  if (selector) {
22178
24245
  const match = allHubs.find(
22179
- (h) => h.hubId === selector || path27.basename(h.hubFolder) === selector
24246
+ (h) => h.hubId === selector || path29.basename(h.hubFolder) === selector
22180
24247
  );
22181
24248
  if (match) return { hubId: match.hubId, hubFolder: match.hubFolder };
22182
24249
  if (UUID_RE3.test(selector)) return { hubId: selector, hubFolder: null };
@@ -22195,7 +24262,7 @@ function resolveHubTarget(workspaceDir, selector) {
22195
24262
  }
22196
24263
  console.error(`Multiple hubs found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22197
24264
  for (const h of previewHubs) {
22198
- console.error(` ${path27.basename(h.hubFolder)} (${h.hubId})`);
24265
+ console.error(` ${path29.basename(h.hubFolder)} (${h.hubId})`);
22199
24266
  }
22200
24267
  process.exit(1);
22201
24268
  }
@@ -22244,7 +24311,7 @@ async function pullCommand(args2) {
22244
24311
  );
22245
24312
  let wroteFiles = false;
22246
24313
  if (!existingFolder && !resolveHubYamlPath(hubFolder)) {
22247
- fs20.mkdirSync(path27.dirname(hubFolder), { recursive: true });
24314
+ fs21.mkdirSync(path29.dirname(hubFolder), { recursive: true });
22248
24315
  console.log("Writing hub configuration...");
22249
24316
  await materializeHubFolder(hubFolder, payload);
22250
24317
  const finalFolder = autoRenameHubFolder(hubFolder, payload.hub.name, payload.hub_environment, payload.hub_id, payload.preview_label, payload.branch_name);
@@ -22329,7 +24396,7 @@ ${why}`);
22329
24396
  }
22330
24397
  async function writeProductionMirror2(workspaceDir, prodPayload) {
22331
24398
  const folder = resolveHubFolder(workspaceDir, prodPayload.hub_id, prodPayload.hub.name, "production", null, null);
22332
- fs20.mkdirSync(path27.dirname(folder), { recursive: true });
24399
+ fs21.mkdirSync(path29.dirname(folder), { recursive: true });
22333
24400
  await materializeHubFolder(folder, prodPayload, { seedAgentContext: false });
22334
24401
  const finalFolder = autoRenameHubFolder(folder, prodPayload.hub.name, "production", prodPayload.hub_id, null, null);
22335
24402
  prependMirrorMarker(finalFolder, prodPayload.hub_id);
@@ -22345,13 +24412,13 @@ async function mirrorLinkedProduction2(client, workspaceDir, productionHubId, or
22345
24412
  }
22346
24413
  }
22347
24414
  function prependMirrorMarker(hubFolder, productionHubId) {
22348
- const hubYaml = path27.join(hubFolder, "hub.yaml");
24415
+ const hubYaml = path29.join(hubFolder, "hub.yaml");
22349
24416
  try {
22350
- const content = fs20.readFileSync(hubYaml, "utf-8");
24417
+ const content = fs21.readFileSync(hubYaml, "utf-8");
22351
24418
  if (content.startsWith(MIRROR_MARKER_PREFIX2)) return;
22352
24419
  const marker = `${MIRROR_MARKER_PREFIX2} ${productionHubId}. Edits are ignored; push is blocked. Edit the linked preview hub instead.
22353
24420
  `;
22354
- fs20.writeFileSync(hubYaml, marker + content, "utf-8");
24421
+ fs21.writeFileSync(hubYaml, marker + content, "utf-8");
22355
24422
  } catch {
22356
24423
  }
22357
24424
  }
@@ -22399,8 +24466,8 @@ var create_exports = {};
22399
24466
  __export(create_exports, {
22400
24467
  createCommand: () => createCommand
22401
24468
  });
22402
- import * as path28 from "path";
22403
- import * as fs21 from "fs";
24469
+ import * as path30 from "path";
24470
+ import * as fs22 from "fs";
22404
24471
  function parseArgs8(args2) {
22405
24472
  let autoConfirm = false;
22406
24473
  let folderSelector;
@@ -22427,7 +24494,7 @@ async function createCommand(args2) {
22427
24494
  const gitRoot = findGitRoot();
22428
24495
  const wsLabel = hubsDirLabel(gitRoot);
22429
24496
  if (gitRoot) warnLayoutOnce(gitRoot);
22430
- if (!fs21.existsSync(workspaceDir)) {
24497
+ if (!fs22.existsSync(workspaceDir)) {
22431
24498
  console.error(`No ${wsLabel}/ directory found. Create hub files in ${wsLabel}/<hub>/hub.yaml with \`hub: { name: ... }\` first.`);
22432
24499
  process.exit(1);
22433
24500
  }
@@ -22436,7 +24503,7 @@ async function createCommand(args2) {
22436
24503
  const resolution = resolveNewHubForCreate(workspaceDir, existingHubs, newHubs, folderSelector);
22437
24504
  if (!resolution.ok) {
22438
24505
  if (resolution.reason === "exists") {
22439
- const folder = path28.basename(resolution.existing.hubFolder);
24506
+ const folder = path30.basename(resolution.existing.hubFolder);
22440
24507
  console.error(`Hub "${folder}" already exists (${resolution.existing.hubId}). Use \`wayai push --hub ${folder}\` to update it.`);
22441
24508
  process.exit(1);
22442
24509
  }
@@ -22450,7 +24517,7 @@ async function createCommand(args2) {
22450
24517
  }
22451
24518
  console.error(`Multiple new hub folders found in ${wsLabel}/. Pass the folder name (\`wayai create <folder>\`) or run from inside one:`);
22452
24519
  for (const h of newHubs) {
22453
- console.error(` ${path28.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
24520
+ console.error(` ${path30.basename(h.hubFolder)} (new \u2014 "${h.hubName}")`);
22454
24521
  }
22455
24522
  process.exit(1);
22456
24523
  }
@@ -22561,8 +24628,8 @@ var replicate_exports = {};
22561
24628
  __export(replicate_exports, {
22562
24629
  replicateCommand: () => replicateCommand
22563
24630
  });
22564
- import * as fs22 from "fs";
22565
- import * as path29 from "path";
24631
+ import * as fs23 from "fs";
24632
+ import * as path31 from "path";
22566
24633
  function parseArgs10(args2) {
22567
24634
  let label;
22568
24635
  let hubSelector;
@@ -22608,8 +24675,8 @@ async function replicateCommand(args2) {
22608
24675
  payload.preview_label,
22609
24676
  payload.branch_name
22610
24677
  );
22611
- const folderPreExisted = fs22.existsSync(hubFolder);
22612
- fs22.mkdirSync(path29.dirname(hubFolder), { recursive: true });
24678
+ const folderPreExisted = fs23.existsSync(hubFolder);
24679
+ fs23.mkdirSync(path31.dirname(hubFolder), { recursive: true });
22613
24680
  const delta = await materializeHubFolder(hubFolder, payload);
22614
24681
  hubFolder = autoRenameHubFolder(
22615
24682
  hubFolder,
@@ -22621,8 +24688,8 @@ async function replicateCommand(args2) {
22621
24688
  );
22622
24689
  seedScopeIfEmpty("hubs", previewHubId);
22623
24690
  if (folderPreExisted) printLocalFileChanges(delta);
22624
- console.log(`Preview written to ${path29.basename(hubFolder)}`);
22625
- 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)}`);
22626
24693
  }
22627
24694
  var init_replicate = __esm({
22628
24695
  "src/commands/replicate.ts"() {
@@ -22645,7 +24712,7 @@ var relabel_exports = {};
22645
24712
  __export(relabel_exports, {
22646
24713
  relabelCommand: () => relabelCommand
22647
24714
  });
22648
- import * as path30 from "path";
24715
+ import * as path32 from "path";
22649
24716
  function parseArgs11(args2) {
22650
24717
  let label;
22651
24718
  let clear = false;
@@ -22689,7 +24756,7 @@ async function relabelCommand(args2) {
22689
24756
  } else {
22690
24757
  console.error(`Multiple hubs found in ${wsLabel}/. Pass --hub <uuid|folder-name> to choose, or run from inside a hub folder:`);
22691
24758
  for (const h of previewHubs) {
22692
- console.error(` ${path30.basename(h.hubFolder)} (${h.hubId})`);
24759
+ console.error(` ${path32.basename(h.hubFolder)} (${h.hubId})`);
22693
24760
  }
22694
24761
  }
22695
24762
  process.exit(1);
@@ -22698,7 +24765,7 @@ async function relabelCommand(args2) {
22698
24765
  console.log("This is a read-only production mirror \u2014 production hubs have no preview label. Relabel the linked preview hub instead.");
22699
24766
  return;
22700
24767
  }
22701
- assertInScope("hubs", target.hubId, path30.basename(target.hubFolder));
24768
+ assertInScope("hubs", target.hubId, path32.basename(target.hubFolder));
22702
24769
  console.log(normalizedLabel ? `Setting preview label to "${normalizedLabel}"...` : "Clearing preview label...");
22703
24770
  const { data } = await client.relabelPreview(target.hubId, normalizedLabel);
22704
24771
  const row = data[0];
@@ -22714,7 +24781,7 @@ async function relabelCommand(args2) {
22714
24781
  );
22715
24782
  console.log(serverLabel ? `Preview label set to "${serverLabel}".` : "Preview label cleared.");
22716
24783
  if (finalFolder !== target.hubFolder) {
22717
- console.log(`Hub folder is now ${path30.basename(finalFolder)}`);
24784
+ console.log(`Hub folder is now ${path32.basename(finalFolder)}`);
22718
24785
  }
22719
24786
  }
22720
24787
  var init_relabel = __esm({
@@ -22740,7 +24807,7 @@ __export(publish_exports, {
22740
24807
  renderHubDiff: () => renderHubDiff,
22741
24808
  runPublish: () => runPublish
22742
24809
  });
22743
- import * as path31 from "path";
24810
+ import * as path33 from "path";
22744
24811
  function parseArgs12(args2) {
22745
24812
  let autoConfirm = false;
22746
24813
  let hubSelector;
@@ -22849,7 +24916,7 @@ async function publishCommand(args2) {
22849
24916
  return;
22850
24917
  }
22851
24918
  }
22852
- assertInScope("hubs", hubId, hubFolder ? path31.basename(hubFolder) : void 0);
24919
+ assertInScope("hubs", hubId, hubFolder ? path33.basename(hubFolder) : void 0);
22853
24920
  await runPublish(client, { hubId, localConfig, organizationId, autoConfirm });
22854
24921
  }
22855
24922
  var STATUS_STYLE, SECTION_TITLES;
@@ -22897,7 +24964,7 @@ var init_publish = __esm({
22897
24964
  });
22898
24965
 
22899
24966
  // src/lib/scope-selector.ts
22900
- import * as path32 from "path";
24967
+ import * as path34 from "path";
22901
24968
  function findAddFlag(args2) {
22902
24969
  return args2.find((arg) => arg === "--add" || arg.startsWith("--add="));
22903
24970
  }
@@ -22954,7 +25021,7 @@ function resolveHubSelectorToId(gitRoot, selector) {
22954
25021
  const match = findHubByFolderName(workspaceDir, selector);
22955
25022
  if (match) return match.hubId;
22956
25023
  const newHubs = scanNewHubs(workspaceDir);
22957
- const pending = newHubs.find((h) => path32.basename(h.hubFolder) === selector);
25024
+ const pending = newHubs.find((h) => path34.basename(h.hubFolder) === selector);
22958
25025
  if (pending) {
22959
25026
  throw expected(
22960
25027
  `"${selector}" is a new hub that hasn't been created on the platform yet, so it has no id to bind to.
@@ -22965,9 +25032,9 @@ Create it first \u2014 the worktree scope picks it up automatically afterward:
22965
25032
  throw expected(
22966
25033
  [
22967
25034
  `No hub matching "${selector}" found in ${hubsDirLabel(gitRoot)}/. Pass a UUID or a folder name from:`,
22968
- ...scanWorkspaceHubs(workspaceDir).map((h) => ` ${path32.basename(h.hubFolder)} (${h.hubId})`),
25035
+ ...scanWorkspaceHubs(workspaceDir).map((h) => ` ${path34.basename(h.hubFolder)} (${h.hubId})`),
22969
25036
  ...newHubs.map(
22970
- (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)}\`)`
22971
25038
  )
22972
25039
  ].join("\n")
22973
25040
  );
@@ -23057,11 +25124,11 @@ __export(migrate_exports, {
23057
25124
  migrateCommand: () => migrateCommand
23058
25125
  });
23059
25126
  import { execFileSync as execFileSync3 } from "child_process";
23060
- import * as fs23 from "fs";
23061
- import * as path33 from "path";
25127
+ import * as fs24 from "fs";
25128
+ import * as path35 from "path";
23062
25129
  function isTracked(gitRoot, p) {
23063
25130
  try {
23064
- execFileSync3("git", ["ls-files", "--error-unmatch", "--", path33.relative(gitRoot, p)], {
25131
+ execFileSync3("git", ["ls-files", "--error-unmatch", "--", path35.relative(gitRoot, p)], {
23065
25132
  cwd: gitRoot,
23066
25133
  stdio: ["pipe", "pipe", "pipe"]
23067
25134
  });
@@ -23071,10 +25138,10 @@ function isTracked(gitRoot, p) {
23071
25138
  }
23072
25139
  }
23073
25140
  function moveDir(gitRoot, from, to) {
23074
- fs23.mkdirSync(path33.dirname(to), { recursive: true });
25141
+ fs24.mkdirSync(path35.dirname(to), { recursive: true });
23075
25142
  if (isTracked(gitRoot, from)) {
23076
25143
  try {
23077
- execFileSync3("git", ["mv", path33.relative(gitRoot, from), path33.relative(gitRoot, to)], {
25144
+ execFileSync3("git", ["mv", path35.relative(gitRoot, from), path35.relative(gitRoot, to)], {
23078
25145
  cwd: gitRoot,
23079
25146
  stdio: ["pipe", "pipe", "pipe"]
23080
25147
  });
@@ -23082,7 +25149,7 @@ function moveDir(gitRoot, from, to) {
23082
25149
  } catch {
23083
25150
  }
23084
25151
  }
23085
- fs23.renameSync(from, to);
25152
+ fs24.renameSync(from, to);
23086
25153
  return "fs";
23087
25154
  }
23088
25155
  async function migrateCommand(_args) {
@@ -23091,12 +25158,12 @@ async function migrateCommand(_args) {
23091
25158
  console.error("Not inside a git repository.");
23092
25159
  process.exit(1);
23093
25160
  }
23094
- const rel = (p) => path33.relative(gitRoot, p);
23095
- const newWs = path33.join(gitRoot, WAYAI_LAYOUT.wsDir);
23096
- const legacyWs = path33.join(gitRoot, WAYAI_LAYOUT.legacy.wsDir);
23097
- const legacyOrg = path33.join(gitRoot, WAYAI_LAYOUT.legacy.orgAtRoot);
23098
- const newHubs = path33.join(newWs, WAYAI_LAYOUT.hubsSubdir);
23099
- 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);
23100
25167
  requireRealSubdirNoSymlink(gitRoot, newHubs, false);
23101
25168
  requireRealSubdirNoSymlink(gitRoot, newOrg, false);
23102
25169
  readRealFileOrThrow(gitRoot, workspaceManifestPath(gitRoot));
@@ -23249,12 +25316,12 @@ var send_message_exports = {};
23249
25316
  __export(send_message_exports, {
23250
25317
  sendMessageCommand: () => sendMessageCommand
23251
25318
  });
23252
- import * as fs24 from "fs";
23253
- import * as path34 from "path";
25319
+ import * as fs25 from "fs";
25320
+ import * as path36 from "path";
23254
25321
  function statAttachment(filePath) {
23255
25322
  let stat2;
23256
25323
  try {
23257
- stat2 = fs24.statSync(filePath);
25324
+ stat2 = fs25.statSync(filePath);
23258
25325
  } catch {
23259
25326
  console.error(`Error: file not found: ${filePath}`);
23260
25327
  process.exit(1);
@@ -23266,11 +25333,11 @@ function statAttachment(filePath) {
23266
25333
  return { filePath, size: stat2.size };
23267
25334
  }
23268
25335
  function readAttachment(filePath, size) {
23269
- const fileName = path34.basename(filePath);
23270
- const ext = path34.extname(fileName).replace(/^\./, "");
25336
+ const fileName = path36.basename(filePath);
25337
+ const ext = path36.extname(fileName).replace(/^\./, "");
23271
25338
  return {
23272
25339
  file_name: fileName,
23273
- file_binary: fs24.readFileSync(filePath).toString("base64"),
25340
+ file_binary: fs25.readFileSync(filePath).toString("base64"),
23274
25341
  file_size: size,
23275
25342
  ...ext && { file_extension: ext }
23276
25343
  };
@@ -23317,7 +25384,7 @@ async function sendMessageCommand(args2) {
23317
25384
  }
23318
25385
  const stats = filePaths.map(statAttachment);
23319
25386
  const filesTotal = stats.reduce((sum, s) => sum + base64Length(s.size), 0);
23320
- 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(", ");
23321
25388
  if (filesTotal > MAX_MESSAGE_BODY_BYTES) {
23322
25389
  console.error(
23323
25390
  `Error: ${fileDetail()} encode to ~${asMb(filesTotal)} MB, over the ${asMb(MAX_MESSAGE_BODY_BYTES)} MB per-request limit.`
@@ -23325,10 +25392,10 @@ async function sendMessageCommand(args2) {
23325
25392
  console.error("Base64 inflates a file by about a third, so ~7 MB of files is the practical ceiling.");
23326
25393
  process.exit(1);
23327
25394
  }
23328
- const audioStats = stats.filter((s) => isAudioAttachmentFileName(path34.basename(s.filePath)));
25395
+ const audioStats = stats.filter((s) => isAudioAttachmentFileName(path36.basename(s.filePath)));
23329
25396
  if (audioStats.length > 1) {
23330
25397
  console.error(
23331
- `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(", ")}).`
23332
25399
  );
23333
25400
  console.error("Audio is transcribed via the hub's STT connection, and only one file per message is transcribed.");
23334
25401
  process.exit(1);
@@ -23336,7 +25403,7 @@ async function sendMessageCommand(args2) {
23336
25403
  const oversizeAudio = audioStats.find((s) => base64Length(s.size) > MAX_AUDIO_FILE_BASE64_BYTES);
23337
25404
  if (oversizeAudio) {
23338
25405
  console.error(
23339
- `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.`
23340
25407
  );
23341
25408
  process.exit(1);
23342
25409
  }
@@ -24812,133 +26879,6 @@ var init_evals = __esm({
24812
26879
  }
24813
26880
  });
24814
26881
 
24815
- // src/lib/eval-session-control.ts
24816
- function formatEvalSessionStopCommand(sessionId, hubId) {
24817
- return `wayai eval session stop ${sessionId} --hub ${hubId}`;
24818
- }
24819
- function formatEvalResultsCommand(sessionId, hubId) {
24820
- return `wayai eval-results --session ${sessionId} --hub ${hubId}`;
24821
- }
24822
- function describeSeedRelease(status) {
24823
- if (status === "pending") {
24824
- return "A turn that started before the stop is still finishing \u2014 its fixture stays reserved until that turn settles.";
24825
- }
24826
- if (status === "releasing") return "Its fixture is being released.";
24827
- return "A turn already in progress may still be finishing \u2014 wait for it before starting another run.";
24828
- }
24829
- function classifyStopOutcome(response) {
24830
- return response.data.stopped ? { kind: "cancelled", seedRelease: response.data.seed_release_status } : { kind: "already_finished", status: response.data.session.session_status };
24831
- }
24832
- function isTerminalStopFailure(err) {
24833
- return err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 408 && err.status !== 429;
24834
- }
24835
- async function stopEvalSessionWithRetries(stop, opts = {}) {
24836
- const attempts = opts.attempts ?? STOP_ATTEMPTS;
24837
- const attemptTimeoutMs = opts.attemptTimeoutMs ?? STOP_ATTEMPT_TIMEOUT_MS;
24838
- const backoffMs = opts.backoffMs ?? STOP_BACKOFF_MS;
24839
- const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
24840
- let lastError = new Error("Stop was never attempted");
24841
- for (let attempt = 1; attempt <= attempts; attempt++) {
24842
- let timeout;
24843
- try {
24844
- return await Promise.race([
24845
- stop(),
24846
- new Promise((_resolve, reject) => {
24847
- timeout = setTimeout(
24848
- () => reject(new Error(`Stop attempt ${attempt} timed out after ${attemptTimeoutMs}ms`)),
24849
- attemptTimeoutMs
24850
- );
24851
- })
24852
- ]);
24853
- } catch (err) {
24854
- lastError = err;
24855
- if (isTerminalStopFailure(err)) break;
24856
- } finally {
24857
- if (timeout !== void 0) clearTimeout(timeout);
24858
- }
24859
- const delay2 = backoffMs[attempt - 1];
24860
- if (attempt < attempts && delay2) await sleep3(delay2);
24861
- }
24862
- throw lastError;
24863
- }
24864
- function installEvalSignalHandlers(input) {
24865
- const exit = input.exit ?? ((code) => process.exit(code));
24866
- const manualStopCommand = formatEvalSessionStopCommand(input.sessionId, input.hubId);
24867
- let signalCount = 0;
24868
- let disposed = false;
24869
- let settle;
24870
- const settled = new Promise((resolve12) => {
24871
- settle = resolve12;
24872
- });
24873
- const listeners = /* @__PURE__ */ new Map();
24874
- const dispose = () => {
24875
- if (disposed) return;
24876
- disposed = true;
24877
- for (const [signal, listener] of listeners) process.off(signal, listener);
24878
- };
24879
- const finish = (exitCode) => {
24880
- dispose();
24881
- settle();
24882
- exit(exitCode);
24883
- };
24884
- const handle = (signal) => {
24885
- signalCount += 1;
24886
- const signalExitCode = SIGNAL_EXIT_CODE[signal];
24887
- if (signalCount > 1) {
24888
- console.error("\nSecond signal received; exiting immediately. The remote eval session may still be running.");
24889
- console.error(`Run: ${manualStopCommand}`);
24890
- finish(signalExitCode);
24891
- return;
24892
- }
24893
- console.error(`
24894
- ${signal} received; stopping remote eval session ${input.sessionId}...`);
24895
- void stopEvalSessionWithRetries(input.stop).then((response) => {
24896
- if (signalCount > 1) return;
24897
- const outcome = classifyStopOutcome(response);
24898
- if (outcome.kind === "already_finished") {
24899
- console.error(`Session had already finished (${outcome.status}) \u2014 nothing was cancelled.`);
24900
- console.error(`See its results: ${formatEvalResultsCommand(input.sessionId, input.hubId)}`);
24901
- } else {
24902
- console.error("Remote session cancellation requested.");
24903
- console.error(describeSeedRelease(outcome.seedRelease));
24904
- }
24905
- finish(signalExitCode);
24906
- }).catch((err) => {
24907
- if (signalCount > 1) return;
24908
- console.error(`Failed to confirm remote cancellation: ${extractApiMessage(err)}`);
24909
- console.error(`The session may still be running. Run: ${manualStopCommand}`);
24910
- finish(1);
24911
- });
24912
- };
24913
- for (const signal of HANDLED_SIGNALS) {
24914
- const listener = () => handle(signal);
24915
- listeners.set(signal, listener);
24916
- process.on(signal, listener);
24917
- }
24918
- return {
24919
- manualStopCommand,
24920
- async interrupted() {
24921
- if (signalCount === 0) return false;
24922
- await settled;
24923
- return true;
24924
- },
24925
- dispose
24926
- };
24927
- }
24928
- var SIGNAL_EXIT_CODE, HANDLED_SIGNALS, STOP_ATTEMPTS, STOP_ATTEMPT_TIMEOUT_MS, STOP_BACKOFF_MS;
24929
- var init_eval_session_control = __esm({
24930
- "src/lib/eval-session-control.ts"() {
24931
- "use strict";
24932
- init_api_client();
24933
- init_errors2();
24934
- SIGNAL_EXIT_CODE = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 };
24935
- HANDLED_SIGNALS = Object.keys(SIGNAL_EXIT_CODE);
24936
- STOP_ATTEMPTS = 3;
24937
- STOP_ATTEMPT_TIMEOUT_MS = 1e4;
24938
- STOP_BACKOFF_MS = [500, 1e3];
24939
- }
24940
- });
24941
-
24942
26882
  // src/lib/eval-fixture-queue.ts
24943
26883
  function isQueueableLaunchError(err) {
24944
26884
  const code = extractApiErrorCode(err);
@@ -25491,7 +27431,7 @@ var init_eval_results = __esm({
25491
27431
  import { createHash as createHash4 } from "crypto";
25492
27432
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync17, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
25493
27433
  import { homedir as homedir5 } from "os";
25494
- 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";
25495
27435
  function parseWav(buf, name = "clip") {
25496
27436
  if (buf.length < 12 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
25497
27437
  throw new Error(`${name} is not a WAV file.`);
@@ -25530,7 +27470,7 @@ function parseWav(buf, name = "clip") {
25530
27470
  throw new Error(`${name} holds no audio data.`);
25531
27471
  }
25532
27472
  function defaultClipCacheDir() {
25533
- return join30(homedir5(), ".wayai", "call-clips");
27473
+ return join31(homedir5(), ".wayai", "call-clips");
25534
27474
  }
25535
27475
  function pcmClip(buf) {
25536
27476
  const samples = new Int16Array(Math.floor(buf.length / 2));
@@ -25579,15 +27519,15 @@ var init_caller_audio = __esm({
25579
27519
  }
25580
27520
  describe;
25581
27521
  async clipFor(line) {
25582
- const path39 = line.clip ? isAbsolute2(line.clip) ? line.clip : resolve10(this.planDir, line.clip) : this.dir ? join30(this.dir, `${line.n}.wav`) : null;
25583
- 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.`);
25584
27524
  let buf;
25585
27525
  try {
25586
- buf = readFileSync17(path39);
27526
+ buf = readFileSync17(path41);
25587
27527
  } catch {
25588
- 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.`);
25589
27529
  }
25590
- return parseWav(buf, path39);
27530
+ return parseWav(buf, path41);
25591
27531
  }
25592
27532
  };
25593
27533
  OPENAI_PCM_SAMPLE_RATE = 24e3;
@@ -25614,20 +27554,20 @@ var init_caller_audio = __esm({
25614
27554
  const key = createHash4("sha256").update(`${this.model}
25615
27555
  ${this.voice}
25616
27556
  ${text}`).digest("hex");
25617
- return join30(this.cacheDir, `${key}.pcm`);
27557
+ return join31(this.cacheDir, `${key}.pcm`);
25618
27558
  }
25619
27559
  clipFor(line) {
25620
- const path39 = this.cachePath(line.say);
25621
- let pending = this.inFlight.get(path39);
27560
+ const path41 = this.cachePath(line.say);
27561
+ let pending = this.inFlight.get(path41);
25622
27562
  if (!pending) {
25623
- pending = this.readOrSynthesize(line, path39);
25624
- this.inFlight.set(path39, pending);
27563
+ pending = this.readOrSynthesize(line, path41);
27564
+ this.inFlight.set(path41, pending);
25625
27565
  }
25626
27566
  return pending;
25627
27567
  }
25628
- async readOrSynthesize(line, path39) {
27568
+ async readOrSynthesize(line, path41) {
25629
27569
  try {
25630
- return pcmClip(readFileSync17(path39));
27570
+ return pcmClip(readFileSync17(path41));
25631
27571
  } catch {
25632
27572
  }
25633
27573
  const res = await this.doFetch("https://api.openai.com/v1/audio/speech", {
@@ -25641,10 +27581,10 @@ ${text}`).digest("hex");
25641
27581
  }
25642
27582
  const buf = Buffer.from(await res.arrayBuffer());
25643
27583
  if (buf.length < 2) throw new Error(`OpenAI speech synthesis returned no audio for line ${line.n}.`);
25644
- mkdirSync12(dirname13(path39), { recursive: true });
25645
- const tmp = `${path39}.${process.pid}.tmp`;
27584
+ mkdirSync12(dirname12(path41), { recursive: true });
27585
+ const tmp = `${path41}.${process.pid}.tmp`;
25646
27586
  writeFileSync7(tmp, buf);
25647
- renameSync5(tmp, path39);
27587
+ renameSync5(tmp, path41);
25648
27588
  return pcmClip(buf);
25649
27589
  }
25650
27590
  };
@@ -27134,7 +29074,7 @@ __export(eval_call_exports, {
27134
29074
  suiteExitCode: () => suiteExitCode
27135
29075
  });
27136
29076
  import { accessSync, constants as fsConstants, existsSync as existsSync16, readFileSync as readFileSync18, statSync as statSync8, writeFileSync as writeFileSync8 } from "fs";
27137
- import { dirname as dirname14, resolve as resolve11 } from "path";
29077
+ import { dirname as dirname13, resolve as resolve11 } from "path";
27138
29078
  function fail(message) {
27139
29079
  throw new Error(message);
27140
29080
  }
@@ -27209,15 +29149,15 @@ function parseEvalCallArgs(args2) {
27209
29149
  if (parsed.jsonPath) checkReportPath(parsed.jsonPath);
27210
29150
  return parsed;
27211
29151
  }
27212
- function checkReportPath(path39) {
27213
- const target = resolve11(path39);
27214
- const dir = dirname14(target);
27215
- if (!existsSync16(dir) || !statSync8(dir).isDirectory()) fail(`--json ${path39}: the folder ${dir} does not exist.`);
27216
- 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.`);
27217
29157
  try {
27218
29158
  accessSync(existsSync16(target) ? target : dir, fsConstants.W_OK);
27219
29159
  } catch {
27220
- fail(`--json ${path39} cannot be written here.`);
29160
+ fail(`--json ${path41} cannot be written here.`);
27221
29161
  }
27222
29162
  }
27223
29163
  function suiteExitCode(report2, runs, reportWritten) {
@@ -27228,7 +29168,7 @@ function formatCallPages(runs) {
27228
29168
  return pages.length > 0 ? ["Call pages (the transcript, and the recording once it is ready):", ...pages] : [];
27229
29169
  }
27230
29170
  function chooseAudioSource(options, plan, env = process.env) {
27231
- const planDir = options.planPath ? dirname14(resolve11(options.planPath)) : process.cwd();
29171
+ const planDir = options.planPath ? dirname13(resolve11(options.planPath)) : process.cwd();
27232
29172
  const planHasClips = Object.values(plan.lines).some((line) => line.clip);
27233
29173
  if (options.clipsDir || planHasClips) {
27234
29174
  if (options.clipsDir && !existsSync16(options.clipsDir)) fail(`--clips ${options.clipsDir} does not exist.`);
@@ -27427,9 +29367,9 @@ var eval_capture_exports = {};
27427
29367
  __export(eval_capture_exports, {
27428
29368
  evalCaptureCommand: () => evalCaptureCommand
27429
29369
  });
27430
- import * as fs25 from "fs";
27431
- import * as path35 from "path";
27432
- 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";
27433
29373
  function isValidSetName(name) {
27434
29374
  if (name.length === 0 || name === "." || name === "..") return false;
27435
29375
  if (name.startsWith(".")) return false;
@@ -27494,25 +29434,25 @@ async function evalCaptureCommand(args2) {
27494
29434
  const setFolderName = targetSetName;
27495
29435
  const scenarioName = parsed.evalName ?? `Capture ${parsed.conversationId.slice(0, 8)}`;
27496
29436
  const slug = slugify2(scenarioName);
27497
- const evalsDir = path35.join(hubFolder, "evals");
27498
- const targetDir = path35.join(evalsDir, setFolderName);
27499
- const targetPath = path35.join(targetDir, `${slug}.yaml`);
27500
- if (!targetPath.startsWith(evalsDir + path35.sep)) {
27501
- 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.`);
27502
29442
  process.exit(1);
27503
29443
  }
27504
29444
  if (!ensureRealSubdirNoSymlink(hubFolder, targetDir, false)) {
27505
- 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.`);
27506
29446
  process.exit(1);
27507
29447
  }
27508
29448
  let targetTaken = true;
27509
29449
  try {
27510
- fs25.lstatSync(targetPath);
29450
+ fs26.lstatSync(targetPath);
27511
29451
  } catch {
27512
29452
  targetTaken = false;
27513
29453
  }
27514
29454
  if (targetTaken) {
27515
- 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.`);
27516
29456
  process.exit(1);
27517
29457
  }
27518
29458
  console.log("Resolving scenario set...");
@@ -27544,14 +29484,14 @@ async function evalCaptureCommand(args2) {
27544
29484
  ...captured.evaluator_instructions ? { evaluator_instructions: captured.evaluator_instructions } : {}
27545
29485
  };
27546
29486
  const yamlObj = buildEvalYamlObject(evalEntry, slug);
27547
- 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"));
27548
29488
  if (outcome !== "created") {
27549
29489
  console.error(
27550
- `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.`
27551
29491
  );
27552
29492
  process.exit(1);
27553
29493
  }
27554
- const relPath = path35.relative(process.cwd(), targetPath);
29494
+ const relPath = path37.relative(process.cwd(), targetPath);
27555
29495
  console.log(`
27556
29496
  Wrote ${relPath}`);
27557
29497
  console.log("Run `wayai pull` to refresh the agent display name, then commit. The scenario is already on the platform.");
@@ -28931,20 +30871,20 @@ var init_set_connection_credential = __esm({
28931
30871
  });
28932
30872
 
28933
30873
  // src/lib/org-workspace.ts
28934
- import * as fs26 from "fs";
28935
- import * as path36 from "path";
28936
- 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";
28937
30877
  function getOrgDir(gitRoot) {
28938
30878
  return resolveLayout(gitRoot).orgDir;
28939
30879
  }
28940
30880
  function orgManifestExists(orgDir) {
28941
- return fs26.existsSync(path36.join(orgDir, ORG_MANIFEST_NAME));
30881
+ return fs27.existsSync(path38.join(orgDir, ORG_MANIFEST_NAME));
28942
30882
  }
28943
30883
  function parseOrgResources(orgDir) {
28944
- const resourcesDir = path36.join(orgDir, "resources");
30884
+ const resourcesDir = path38.join(orgDir, "resources");
28945
30885
  requireRealSubdirNoSymlink(orgDir, resourcesDir, false);
28946
- const manifestText = readRealFileOrThrow(orgDir, path36.join(orgDir, ORG_MANIFEST_NAME));
28947
- 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) ?? {};
28948
30888
  const rawResources = Array.isArray(manifest.resources) ? manifest.resources : [];
28949
30889
  const resources = rawResources.map((res) => {
28950
30890
  const resource = { name: res.name };
@@ -28956,9 +30896,9 @@ function parseOrgResources(orgDir) {
28956
30896
  if (res.environment) resource.environment = res.environment;
28957
30897
  if (Array.isArray(res.tags)) resource.tags = res.tags;
28958
30898
  if (Array.isArray(res.folders)) resource.folders = res.folders;
28959
- const resDir = path36.join(resourcesDir, slugify2(resource.name));
30899
+ const resDir = path38.join(resourcesDir, slugify2(resource.name));
28960
30900
  requireRealSubdirNoSymlink(orgDir, resDir, false);
28961
- if (fs26.existsSync(resDir)) {
30901
+ if (fs27.existsSync(resDir)) {
28962
30902
  const files = scanResourceFiles(resDir, "");
28963
30903
  if (files.length > 0) resource.files = files;
28964
30904
  }
@@ -28967,8 +30907,8 @@ function parseOrgResources(orgDir) {
28967
30907
  return { version: 1, resources };
28968
30908
  }
28969
30909
  function writeOrgResources(orgDir, payload) {
28970
- fs26.mkdirSync(orgDir, { recursive: true });
28971
- const resourcesDir = path36.join(orgDir, "resources");
30910
+ fs27.mkdirSync(orgDir, { recursive: true });
30911
+ const resourcesDir = path38.join(orgDir, "resources");
28972
30912
  requireRealSubdirNoSymlink(orgDir, resourcesDir, false);
28973
30913
  const resources = payload.resources ?? [];
28974
30914
  const manifestResources = resources.map((r) => {
@@ -28977,19 +30917,19 @@ function writeOrgResources(orgDir, payload) {
28977
30917
  });
28978
30918
  writeFileNoFollow(
28979
30919
  orgDir,
28980
- path36.join(orgDir, ORG_MANIFEST_NAME),
28981
- 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")
28982
30922
  );
28983
30923
  const currentSlugs = /* @__PURE__ */ new Set();
28984
30924
  for (const resource of resources) {
28985
30925
  const resSlug = slugify2(resource.name);
28986
30926
  currentSlugs.add(resSlug);
28987
- writeResourceFileTree(path36.join(resourcesDir, resSlug), resource.files || [], orgDir);
30927
+ writeResourceFileTree(path38.join(resourcesDir, resSlug), resource.files || [], orgDir);
28988
30928
  }
28989
- if (fs26.existsSync(resourcesDir)) {
28990
- for (const entry of fs26.readdirSync(resourcesDir, { withFileTypes: true })) {
30929
+ if (fs27.existsSync(resourcesDir)) {
30930
+ for (const entry of fs27.readdirSync(resourcesDir, { withFileTypes: true })) {
28991
30931
  if (entry.isDirectory() && !currentSlugs.has(entry.name)) {
28992
- fs26.rmSync(path36.join(resourcesDir, entry.name), { recursive: true, force: true });
30932
+ fs27.rmSync(path38.join(resourcesDir, entry.name), { recursive: true, force: true });
28993
30933
  }
28994
30934
  }
28995
30935
  }
@@ -28998,7 +30938,7 @@ async function downloadOrgBinaryFiles(orgDir, payload) {
28998
30938
  let count = 0;
28999
30939
  for (const resource of payload.resources ?? []) {
29000
30940
  if (!resource.files) continue;
29001
- const resDir = path36.join(orgDir, "resources", slugify2(resource.name));
30941
+ const resDir = path38.join(orgDir, "resources", slugify2(resource.name));
29002
30942
  count += await downloadBinaryFiles(resDir, resource.files, orgDir);
29003
30943
  }
29004
30944
  if (count > 0) console.log(`Downloaded ${count} binary resource file(s).`);
@@ -29621,8 +31561,8 @@ var init_report_edit_args = __esm({
29621
31561
  });
29622
31562
 
29623
31563
  // src/lib/file-map.ts
29624
- import * as fs27 from "fs";
29625
- import * as path37 from "path";
31564
+ import * as fs28 from "fs";
31565
+ import * as path39 from "path";
29626
31566
  function isSafeRelPath(rel) {
29627
31567
  if (rel.length === 0 || rel.length > 300) return false;
29628
31568
  if (rel.startsWith("/") || rel.includes("\\")) return false;
@@ -29637,9 +31577,9 @@ function writeFileMap(targetDir, files) {
29637
31577
  if (!isSafeRelPath(rel)) {
29638
31578
  throw new Error(`Refusing to write unsafe path: ${rel}`);
29639
31579
  }
29640
- const abs = path37.join(targetDir, rel);
29641
- fs27.mkdirSync(path37.dirname(abs), { recursive: true });
29642
- 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");
29643
31583
  written.push(rel);
29644
31584
  }
29645
31585
  return written;
@@ -29655,8 +31595,8 @@ var admin_exports = {};
29655
31595
  __export(admin_exports, {
29656
31596
  adminCommand: () => adminCommand
29657
31597
  });
29658
- import * as fs28 from "fs";
29659
- import * as path38 from "path";
31598
+ import * as fs29 from "fs";
31599
+ import * as path40 from "path";
29660
31600
  async function adminCommand(args2) {
29661
31601
  const [group, ...afterGroup] = args2;
29662
31602
  if (!group) {
@@ -30095,7 +32035,7 @@ async function runArchiveRead(positional, flagArgs) {
30095
32035
  exitOnApiError(err);
30096
32036
  throw err;
30097
32037
  }
30098
- fs28.writeFileSync(outPath, zip);
32038
+ fs29.writeFileSync(outPath, zip);
30099
32039
  console.log(`Wrote ${zip.byteLength} bytes to ${outPath}`);
30100
32040
  return;
30101
32041
  }
@@ -30237,13 +32177,13 @@ async function runSkillInstall(positional) {
30237
32177
  throw err;
30238
32178
  }
30239
32179
  const root = findGitRoot() ?? process.cwd();
30240
- 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)));
30241
32181
  const targets = present.length > 0 ? present : HARNESS_SKILL_DIRS;
30242
32182
  const fileCount = Object.keys(res.files).length;
30243
32183
  const relDirs = targets.map((harness) => {
30244
- const targetDir = path38.join(root, harness, "skills", name);
32184
+ const targetDir = path40.join(root, harness, "skills", name);
30245
32185
  writeFileMap(targetDir, res.files);
30246
- return `${path38.relative(root, targetDir)}/`;
32186
+ return `${path40.relative(root, targetDir)}/`;
30247
32187
  });
30248
32188
  console.log(`Installed skill "${name}" (${fileCount} file${fileCount === 1 ? "" : "s"}) \u2192 ${relDirs.join(", ")}`);
30249
32189
  console.log("Reload your agent (e.g. restart Claude Code) to pick up the skill.");
@@ -32836,12 +34776,12 @@ var init_providers = __esm({
32836
34776
  // src/data/commands/report.ts
32837
34777
  import { Command as Command6 } from "commander";
32838
34778
  import { readFileSync as readFileSync20 } from "fs";
32839
- import { dirname as dirname16, join as join35 } from "path";
34779
+ import { dirname as dirname15, join as join36 } from "path";
32840
34780
  import { fileURLToPath as fileURLToPath2 } from "url";
32841
34781
  function resolveCliVersion() {
32842
34782
  for (const candidate of [
32843
- join35(here, "..", "package.json"),
32844
- join35(here, "..", "..", "..", "package.json")
34783
+ join36(here, "..", "package.json"),
34784
+ join36(here, "..", "..", "..", "package.json")
32845
34785
  ]) {
32846
34786
  try {
32847
34787
  const version = JSON.parse(readFileSync20(candidate, "utf-8")).version;
@@ -33040,7 +34980,7 @@ var init_report3 = __esm({
33040
34980
  init_terminal_output();
33041
34981
  init_workspace();
33042
34982
  init_skill_version();
33043
- here = dirname16(fileURLToPath2(import.meta.url));
34983
+ here = dirname15(fileURLToPath2(import.meta.url));
33044
34984
  }
33045
34985
  });
33046
34986
 
@@ -33353,16 +35293,16 @@ MCP URL: ${toolsetMcpUrl(slug)}`);
33353
35293
  ).option("--force", "Revoke even if the token appears to be in live use").action(async function(tokenId, opts) {
33354
35294
  const client = await createDataClient();
33355
35295
  const id = pathSegment(tokenId, "token id");
33356
- const path39 = (force) => `/v1/tokens/${id}${force ? "?force=true" : ""}`;
35296
+ const path41 = (force) => `/v1/tokens/${id}${force ? "?force=true" : ""}`;
33357
35297
  const format = outputFormat(this);
33358
35298
  const revoked = (forced) => printOutput({ token_id: tokenId, revoked: true, forced }, format);
33359
35299
  if (opts.force) {
33360
- await client.request("DELETE", path39(true));
35300
+ await client.request("DELETE", path41(true));
33361
35301
  revoked(true);
33362
35302
  return;
33363
35303
  }
33364
35304
  try {
33365
- await client.request("DELETE", path39(false));
35305
+ await client.request("DELETE", path41(false));
33366
35306
  revoked(false);
33367
35307
  } catch (err) {
33368
35308
  if (!(err instanceof ApiError) || err.status !== 409 || dataErrorDetails(err)?.requires_force !== true) {
@@ -33377,7 +35317,7 @@ MCP URL: ${toolsetMcpUrl(slug)}`);
33377
35317
  console.error("Aborted.");
33378
35318
  return process.exit(1);
33379
35319
  }
33380
- await client.request("DELETE", path39(true));
35320
+ await client.request("DELETE", path41(true));
33381
35321
  revoked(true);
33382
35322
  }
33383
35323
  });
@@ -33525,9 +35465,9 @@ var init_base_tags = __esm({
33525
35465
 
33526
35466
  // src/data/commands/bases.ts
33527
35467
  import { Command as Command10 } from "commander";
33528
- function pageOf(path39, cursor) {
33529
- if (!cursor) return path39;
33530
- 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)}`;
33531
35471
  }
33532
35472
  function parseEnum(flag, value, allowed) {
33533
35473
  if (value === void 0) return void 0;
@@ -33684,9 +35624,9 @@ function buildBasesCommand() {
33684
35624
  });
33685
35625
  bases.command("list-previews <origin-id>").description("List preview bases cloned from a base (production or preview)").action(async function(originId) {
33686
35626
  const client = await createDataClient();
33687
- const path39 = `/v1/${pathSegment(originId, "origin base id")}/previews`;
35627
+ const path41 = `/v1/${pathSegment(originId, "origin base id")}/previews`;
33688
35628
  printOutput(
33689
- await client.collectPages((cursor) => pageOf(path39, cursor)),
35629
+ await client.collectPages((cursor) => pageOf(path41, cursor)),
33690
35630
  outputFormat(this)
33691
35631
  );
33692
35632
  });
@@ -33733,9 +35673,9 @@ function buildBasesCommand() {
33733
35673
  });
33734
35674
  bases.command("promotions <production-id>").description("List promotion history for a production base").action(async function(productionId) {
33735
35675
  const client = await createDataClient();
33736
- const path39 = `/v1/${pathSegment(productionId, "production base id")}/promotions`;
35676
+ const path41 = `/v1/${pathSegment(productionId, "production base id")}/promotions`;
33737
35677
  printOutput(
33738
- await client.collectPages((cursor) => pageOf(path39, cursor)),
35678
+ await client.collectPages((cursor) => pageOf(path41, cursor)),
33739
35679
  outputFormat(this)
33740
35680
  );
33741
35681
  });
@@ -33874,7 +35814,7 @@ var init_file_types = __esm({
33874
35814
  // src/data/commands/files.ts
33875
35815
  import { Command as Command12 } from "commander";
33876
35816
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
33877
- import { basename as basename23 } from "path";
35817
+ import { basename as basename25 } from "path";
33878
35818
  function renderFileDiff(fileType, filePath, from, to, d) {
33879
35819
  console.log(sanitizeTerminalText(`${fileType}/${filePath}: v${from} \u2192 v${to}`));
33880
35820
  const md = d.metadata_delta;
@@ -33906,7 +35846,7 @@ function renderFileDiff(fileType, filePath, from, to, d) {
33906
35846
  }
33907
35847
  function downloadTarget(remotePath, to) {
33908
35848
  if (to) return to;
33909
- const derived = basename23(remotePath);
35849
+ const derived = basename25(remotePath);
33910
35850
  if (derived === "" || derived === "." || derived === "..") {
33911
35851
  throw expected(
33912
35852
  `Cannot derive a local filename from "${remotePath}" \u2014 pass --to <local> to name it.`
@@ -34282,8 +36222,8 @@ function buildRecordsCommand() {
34282
36222
  const body = { data: parseData(opts.data) };
34283
36223
  if (opts.externalId) body.external_id = opts.externalId;
34284
36224
  if (opts.externalSource) body.external_source = opts.externalSource;
34285
- const path39 = opts.id ? `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${pathSegment(opts.id, "--id")}` : `/v1/${base}/records/${pathSegment(recordType2, "record_type")}`;
34286
- 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));
34287
36227
  });
34288
36228
  records.command("query <record_type>").description("List/search records: exact filters + fuzzy `search`, sorting, pagination").option(
34289
36229
  "--external-source <source>",
@@ -34318,9 +36258,9 @@ function buildRecordsCommand() {
34318
36258
  ).action(async function(recordType2, id, opts) {
34319
36259
  const base = pathSegment(requireBase(this), "--base");
34320
36260
  const client = await createDataClient();
34321
- let path39 = `/v1/${base}/records/${pathSegment(recordType2, "record_type")}/${recordKey(id, opts.externalSource)}`;
34322
- if (opts.externalSource) path39 += `?external_source=${encodeURIComponent(opts.externalSource)}`;
34323
- 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));
34324
36264
  });
34325
36265
  records.command("delete <record_type> <id>").description(
34326
36266
  "Delete a record by internal ID, or by external_id (pass --external-source to scope it)"
@@ -34334,11 +36274,11 @@ function buildRecordsCommand() {
34334
36274
  }
34335
36275
  const base = pathSegment(requireBase(this), "--base");
34336
36276
  const client = await createDataClient();
34337
- 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)}`;
34338
36278
  if (opts.externalSource) {
34339
- path39 += `?external_source=${encodeURIComponent(opts.externalSource)}&external_id=${encodeURIComponent(id)}`;
36279
+ path41 += `?external_source=${encodeURIComponent(opts.externalSource)}&external_id=${encodeURIComponent(id)}`;
34340
36280
  }
34341
- await client.request("DELETE", path39);
36281
+ await client.request("DELETE", path41);
34342
36282
  console.log("Deleted");
34343
36283
  });
34344
36284
  records.command("history <id>").description(
@@ -34503,8 +36443,8 @@ function buildRelationshipsCommand() {
34503
36443
  if (opts.data) body.data = parseData(opts.data);
34504
36444
  const base = pathSegment(requireBase(this), "--base");
34505
36445
  const client = await createDataClient();
34506
- const path39 = opts.id ? `/v1/${base}/relationships/${pathSegment(opts.id, "--id")}` : `/v1/${base}/relationships`;
34507
- 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));
34508
36448
  });
34509
36449
  relationships.command("get <id>").description(
34510
36450
  "Get a relationship by ID (or by its external key: pass the external_id with --rel-type)"
@@ -34737,8 +36677,8 @@ function buildToolsetsCommand() {
34737
36677
  toolsets.command("get <slug>").description("Get a toolset").option("--resolved", "Include resolved record_type schemas").action(async function(slug, opts) {
34738
36678
  const base = pathSegment(requireBase(this), "--base");
34739
36679
  const client = await createDataClient();
34740
- const path39 = opts.resolved ? `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}/resolved` : `/v1/${base}/toolsets/${pathSegment(slug, "toolset slug")}`;
34741
- 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));
34742
36682
  });
34743
36683
  toolsets.command("list").description("List all toolsets").action(async function() {
34744
36684
  const base = pathSegment(requireBase(this), "--base");
@@ -34973,7 +36913,7 @@ init_utils();
34973
36913
  init_registry();
34974
36914
  import { readFileSync as readFileSync24 } from "fs";
34975
36915
  import { fileURLToPath as fileURLToPath3 } from "url";
34976
- import { dirname as dirname17, join as join36 } from "path";
36916
+ import { dirname as dirname16, join as join37 } from "path";
34977
36917
 
34978
36918
  // src/lib/version-refresh.ts
34979
36919
  init_version_cache();
@@ -35128,8 +37068,8 @@ Run \`wayai admin skill install\` to update.`);
35128
37068
  }
35129
37069
 
35130
37070
  // src/index.ts
35131
- var __dirname = dirname17(fileURLToPath3(import.meta.url));
35132
- 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"));
35133
37073
  var [, , command, ...args] = process.argv;
35134
37074
  var isBackgroundRefresh = command === REFRESH_COMMAND;
35135
37075
  if (!isBackgroundRefresh) initSentry(command, pkg.version);