@mutmutco/hub 4.3.68 → 4.4.1

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.
Files changed (2) hide show
  1. package/dist/index.cjs +188 -38
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -21,8 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ ACCENT: () => ACCENT,
25
+ MM_GREEN: () => MM_GREEN,
26
+ RESET: () => RESET,
27
+ accent: () => accent,
24
28
  acquireSharedDocLock: () => acquireSharedDocLock,
29
+ brandLine: () => brandLine,
25
30
  buildWatcherCommand: () => buildWatcherCommand,
31
+ checklistLine: () => checklistLine,
26
32
  claudeArm: () => claudeArm,
27
33
  claudeHostEvidence: () => claudeHostEvidence,
28
34
  clearLaunchTarget: () => clearLaunchTarget,
@@ -61,9 +67,12 @@ __export(index_exports, {
61
67
  progressiveRow: () => progressiveRow,
62
68
  readLaunchTarget: () => readLaunchTarget,
63
69
  readPending: () => readPending,
70
+ receipt: () => receipt,
71
+ receiptText: () => receiptText,
64
72
  reconcile: () => reconcile,
65
73
  reconcileCursorUserHooks: () => reconcileCursorUserHooks,
66
74
  releaseSharedDocLock: () => releaseSharedDocLock,
75
+ reportCounts: () => reportCounts,
67
76
  reportFooterLines: () => reportFooterLines,
68
77
  retiredSurfaces: () => retiredSurfaces,
69
78
  retryHint: () => retryHint,
@@ -72,6 +81,7 @@ __export(index_exports, {
72
81
  sharedDocLockPath: () => sharedDocLockPath,
73
82
  sharedDocLockTimeoutReceipt: () => sharedDocLockTimeoutReceipt,
74
83
  shouldHandOff: () => shouldHandOff,
84
+ signOff: () => signOff,
75
85
  startSpinner: () => startSpinner,
76
86
  surfaceArmPlan: () => surfaceArmPlan,
77
87
  systemdServiceUnit: () => systemdServiceUnit,
@@ -79,6 +89,7 @@ __export(index_exports, {
79
89
  taskXml: () => taskXml,
80
90
  updateHeader: () => updateHeader,
81
91
  watcherLockPath: () => watcherLockPath,
92
+ welcome: () => welcome,
82
93
  withSharedDocLock: () => withSharedDocLock,
83
94
  wrapWords: () => wrapWords,
84
95
  writeLaunchTarget: () => writeLaunchTarget,
@@ -2816,6 +2827,15 @@ var GREEN = "\x1B[32m";
2816
2827
  var YELLOW = "\x1B[33m";
2817
2828
  var RED = "\x1B[31m";
2818
2829
  var RESET = "\x1B[0m";
2830
+ var ACCENT = "\x1B[38;2;125;211;252m";
2831
+ var MUTED = "\x1B[38;2;150;150;150m";
2832
+ var MM_GREEN = "\x1B[38;2;31;209;138m";
2833
+ function accent(text, color) {
2834
+ return color ? ACCENT + text + RESET : text;
2835
+ }
2836
+ function brandLine(rest, color = false) {
2837
+ return `${accent("mmi-hub", color)} \xB7 Mutatis Mutandis \u2014 ${rest}`;
2838
+ }
2819
2839
  var RETRY_HINTS = [
2820
2840
  [/applies when the host exits|applies on a quiet tick/, "installs when the app closes"],
2821
2841
  [/in flight|changed while/, "rerun update \u2014 host race"],
@@ -2885,6 +2905,36 @@ function glyphFor(kind) {
2885
2905
  function colorFor(kind) {
2886
2906
  return kind === "failed" ? RED : kind === "retry" ? YELLOW : GREEN;
2887
2907
  }
2908
+ var ARM_TITLE = {
2909
+ updater: "Updating mmi-hub",
2910
+ cli: "Installing mmi-cli",
2911
+ claude: "Arming Claude Code",
2912
+ codex: "Arming Codex",
2913
+ cursor: "Arming Cursor",
2914
+ hermes: "Arming Hermes",
2915
+ jervcode: "Arming JervCode"
2916
+ };
2917
+ var CHECKLIST_TAIL = {
2918
+ updated: "",
2919
+ current: " current",
2920
+ keep: " keep",
2921
+ retry: " will retry",
2922
+ failed: " failed",
2923
+ pending: " pending"
2924
+ };
2925
+ function checklistLine(arm, seconds, options = {}) {
2926
+ const tty = options.tty ?? false;
2927
+ const color = tty && (options.color ?? true);
2928
+ const dryRun = options.dryRun ?? false;
2929
+ const kind = classify(arm).kind;
2930
+ const tone = kind === "failed" ? RED : kind === "retry" ? YELLOW : MM_GREEN;
2931
+ const paint = (text) => color ? tone + text + RESET : text;
2932
+ const glyph = kind === "failed" ? "\u2716" : kind === "retry" ? "\u25CF" : "\u2714";
2933
+ const title = ARM_TITLE[arm.surface] ?? `Arming ${arm.surface}`;
2934
+ const tail2 = dryRun && kind === "updated" ? " would update" : CHECKLIST_TAIL[kind];
2935
+ const time = seconds !== null && kind !== "retry" && kind !== "failed" ? ` ${Math.max(1, Math.round(seconds))}s` : "";
2936
+ return ` ${paint(glyph)} ${title}${tail2}${time}`;
2937
+ }
2888
2938
  function wrapWords(text, available) {
2889
2939
  if (available < 1) return [text];
2890
2940
  const lines = [];
@@ -2931,8 +2981,11 @@ function headline(detail, width) {
2931
2981
  const first = (detail ?? "").split(/(?<=[.;:])\s/)[0]?.replace(/\s+/g, " ").trim() ?? "";
2932
2982
  return wrapWords(first || "blocked", Math.max(10, width - 4))[0];
2933
2983
  }
2934
- function updateHeader(target, dryRun = false) {
2935
- return dryRun ? `MMI Hub \u2014 update plan to ${target} (dry run)` : `MMI Hub \u2014 updating to ${target} (verified)`;
2984
+ function updateHeader(target, dryRun = false, options) {
2985
+ return brandLine(
2986
+ dryRun ? `update plan to ${target} (dry run)` : `updating to ${target} (verified)`,
2987
+ Boolean(options?.color)
2988
+ );
2936
2989
  }
2937
2990
  function renderRowLines(row, ctx) {
2938
2991
  const paint = (kind, text) => ctx.color ? colorFor(kind) + text + RESET : text;
@@ -2952,6 +3005,16 @@ function progressiveRow(arm, options = {}) {
2952
3005
  const dryRun = options.dryRun ?? false;
2953
3006
  return renderRowLines(classify(arm), { tty, color, dryRun, width, statusWidth: progressiveStatusWidth(dryRun) }).join("\n");
2954
3007
  }
3008
+ function reportCounts(summary) {
3009
+ const rows = collectRows(summary);
3010
+ return {
3011
+ updated: rows.filter((row) => row.kind === "updated").length,
3012
+ current: rows.filter((row) => row.kind === "current" || row.kind === "keep").length,
3013
+ retry: rows.filter((row) => row.kind === "retry").length,
3014
+ failed: rows.filter((row) => row.kind === "failed").length,
3015
+ total: rows.length
3016
+ };
3017
+ }
2955
3018
  function reportFooterLines(summary, options = {}) {
2956
3019
  const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
2957
3020
  const tty = options.tty ?? false;
@@ -2960,14 +3023,12 @@ function reportFooterLines(summary, options = {}) {
2960
3023
  const journalPath = options.journalPath ?? "~/.mmi/updater/journal.jsonl";
2961
3024
  const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
2962
3025
  const rows = collectRows(summary);
2963
- const counts = {
2964
- updated: rows.filter((row) => row.kind === "updated").length,
2965
- retry: rows.filter((row) => row.kind === "retry").length,
2966
- failed: rows.filter((row) => row.kind === "failed").length,
2967
- total: rows.length
2968
- };
3026
+ const counts = reportCounts(summary);
2969
3027
  const footer = dryRun ? `${counts.updated}/${counts.total} would update` : `${counts.updated}/${counts.total} updated` + (counts.retry ? ` \xB7 ${counts.retry} will retry` : "") + (counts.failed ? ` \xB7 ${counts.failed} failed` : "");
2970
3028
  const out = ["", " " + footer];
3029
+ if (tty && !dryRun && counts.failed === 0) {
3030
+ out.push(" " + (color ? MUTED + "Suggested next: mmi-cli doctor" + RESET : "Suggested next: mmi-cli doctor"));
3031
+ }
2971
3032
  const self = summary.updater;
2972
3033
  if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
2973
3034
  out.push("");
@@ -2996,13 +3057,7 @@ function formatReconcileReport(summary, options = {}) {
2996
3057
  const out = [];
2997
3058
  const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
2998
3059
  const rows = collectRows(summary);
2999
- const counts = {
3000
- updated: rows.filter((row) => row.kind === "updated").length,
3001
- current: rows.filter((row) => row.kind === "current" || row.kind === "keep").length,
3002
- retry: rows.filter((row) => row.kind === "retry").length,
3003
- failed: rows.filter((row) => row.kind === "failed").length,
3004
- total: rows.length
3005
- };
3060
+ const counts = reportCounts(summary);
3006
3061
  if (!summary.target) {
3007
3062
  if ((summary.exit >= 1 || summary.deferred) && summary.detail) {
3008
3063
  out.push("MMI Hub \u2014 cannot update now");
@@ -3018,7 +3073,7 @@ function formatReconcileReport(summary, options = {}) {
3018
3073
  out.push(`MMI Hub \u2014 MMI is up to date (${summary.target} \xB7 verified)`);
3019
3074
  return out.join("\n") + "\n";
3020
3075
  }
3021
- out.push(updateHeader(summary.target, dryRun));
3076
+ out.push(updateHeader(summary.target, dryRun, { color }));
3022
3077
  out.push("");
3023
3078
  const statusWidth = Math.max(...rows.map((row) => statusText(row.kind, dryRun).length), 7);
3024
3079
  for (const row of rows) out.push(...renderRowLines(row, { tty, color, dryRun, width, statusWidth }));
@@ -3026,6 +3081,68 @@ function formatReconcileReport(summary, options = {}) {
3026
3081
  return out.join("\n") + "\n";
3027
3082
  }
3028
3083
 
3084
+ // src/face.ts
3085
+ var DEFAULT_WIDTH2 = 100;
3086
+ function welcome(color, output = process.stdout) {
3087
+ const mark = color ? MM_GREEN + "\u25C6" + RESET : "\u25C6";
3088
+ output.write(`${mark} ${accent("mmi-hub", color)} \u2014 Mutatis Mutandis
3089
+ `);
3090
+ output.write("\u2502\n");
3091
+ output.write("\u2502 Welcome back. Checking your surfaces\u2026\n");
3092
+ output.write("\u2502\n");
3093
+ }
3094
+ var TONES = { "\u2714": MM_GREEN, "\u2716": RED, "\u25CF": YELLOW };
3095
+ function paintGlyph(line, color) {
3096
+ if (!color) return line;
3097
+ const glyph = line.trimStart()[0];
3098
+ const tone = TONES[glyph];
3099
+ if (!tone) return line;
3100
+ const at = line.indexOf(glyph);
3101
+ return line.slice(0, at) + tone + glyph + RESET + line.slice(at + 1);
3102
+ }
3103
+ function receipt(text, options = {}) {
3104
+ const color = options.color ?? false;
3105
+ const width = Math.max(40, options.width ?? DEFAULT_WIDTH2);
3106
+ const output = options.output ?? process.stdout;
3107
+ const edges = 2 * 2 + 2;
3108
+ const available = width - edges;
3109
+ const lines = [];
3110
+ for (const paragraph of text.split("\n")) {
3111
+ if (!paragraph.trim()) lines.push(null);
3112
+ else if (paragraph.length <= available) lines.push(paragraph);
3113
+ else for (const line of wrapWords(paragraph, available)) lines.push(line);
3114
+ }
3115
+ const inner = Math.min(available, Math.max(20, ...lines.map((line) => line?.length ?? 0)));
3116
+ const rule = "\u2500".repeat(inner + 2 * 2);
3117
+ const frame = (text2) => color ? ACCENT + text2 + RESET : text2;
3118
+ const blank = `${frame("\u2502")}${" ".repeat(inner + 2 * 2)}${frame("\u2502")}`;
3119
+ output.write(frame(`\u256D${rule}\u256E`) + "\n");
3120
+ for (const line of lines) {
3121
+ output.write(line === null ? blank + "\n" : `${frame("\u2502")} ${paintGlyph(line.padEnd(inner), color)} ${frame("\u2502")}
3122
+ `);
3123
+ }
3124
+ output.write(frame(`\u2570${rule}\u256F`) + "\n");
3125
+ }
3126
+ function signOff(color, output = process.stdout) {
3127
+ output.write(`\u2570 ${accent("mmi-hub \xB7 Mutatis Mutandis", color)}
3128
+ `);
3129
+ }
3130
+ function receiptText(summary, autoupdate, options = {}) {
3131
+ const dry = options.dryRun ?? false;
3132
+ const counts = reportCounts(summary);
3133
+ const health = "Check health any time: mmi-hub status";
3134
+ if (!summary.target) {
3135
+ return `${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)
3136
+ ${health}`;
3137
+ }
3138
+ const ready = counts.failed === 0;
3139
+ const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25C7" : "\u2714";
3140
+ const state = counts.failed > 0 ? `${counts.failed} of ${counts.total} surfaces failed` : counts.retry > 0 ? `${counts.retry} of ${counts.total} surfaces will retry` : counts.updated > 0 ? `${counts.updated}/${counts.total} surfaces ${dry ? "would update" : "updated"}` : `all ${counts.total} surfaces current`;
3141
+ const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
3142
+ return `${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}
3143
+ ${health}`;
3144
+ }
3145
+
3029
3146
  // src/spinner.ts
3030
3147
  var import_node_worker_threads = require("node:worker_threads");
3031
3148
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -3160,18 +3277,36 @@ var HELP = `mmi-hub ${ownVersion()} \u2014 install and maintain MMI tooling
3160
3277
  State remains under $MMI_UPDATER_HOME (default ~/.mmi/updater), and the Windows task name remains
3161
3278
  "MMI Fleet Updater", so existing installations continue in place.
3162
3279
  Status performs no npm install, host repair, scheduler write, state mkdir, or repository cleanup.`;
3163
- function printReconcile(summary, json, dryRun = false) {
3164
- if (json) {
3165
- process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
3166
- return;
3167
- }
3168
- process.stdout.write(formatReconcileReport(summary, {
3280
+ function humanOptions(dryRun) {
3281
+ return {
3169
3282
  tty: Boolean(process.stdout.isTTY),
3170
3283
  color: !process.env.NO_COLOR,
3171
3284
  width: process.stdout.columns ?? 100,
3172
3285
  dryRun,
3173
3286
  journalPath: statePaths(process.env).journalPath
3174
- }));
3287
+ };
3288
+ }
3289
+ function printReconcile(summary, json, dryRun = false) {
3290
+ if (json) {
3291
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
3292
+ return;
3293
+ }
3294
+ process.stdout.write(formatReconcileReport(summary, humanOptions(dryRun)));
3295
+ }
3296
+ function faceEnabled(args, json) {
3297
+ return !json && !args.includes("--verbose") && !process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
3298
+ }
3299
+ function openFace(args, json) {
3300
+ const face = faceEnabled(args, json);
3301
+ if (face && !process.env.MMI_UPDATER_REEXEC) welcome(true);
3302
+ return face;
3303
+ }
3304
+ function finishFace(summary, autoupdate, dryRun) {
3305
+ const options = humanOptions(dryRun);
3306
+ if (!summary.target) process.stdout.write(formatReconcileReport(summary, options));
3307
+ else if (summary.exit >= 1) process.stdout.write(reportFooterLines(summary, options).join("\n") + "\n");
3308
+ receipt(receiptText(summary, autoupdate, { dryRun }), { color: options.color, width: options.width });
3309
+ signOff(true);
3175
3310
  }
3176
3311
  function handOff(reexec, args) {
3177
3312
  const child = (0, import_node_child_process9.spawnSync)(process.execPath, [reexec.dist, ...args], {
@@ -3202,7 +3337,7 @@ function heldOutput(write) {
3202
3337
  }
3203
3338
  };
3204
3339
  }
3205
- function runUpdate(args, json) {
3340
+ function runUpdate(args, json, face = false) {
3206
3341
  const dryRun = args.includes("--dry-run");
3207
3342
  const verbose = args.includes("--verbose");
3208
3343
  const narrate = (line) => {
@@ -3213,30 +3348,27 @@ function runUpdate(args, json) {
3213
3348
  if (!progressive) {
3214
3349
  return { summary: reconcile({ dryRun, narrate: verbose ? narrate : void 0 }), rendered: false };
3215
3350
  }
3216
- const reportOptions = {
3217
- tty: Boolean(process.stdout.isTTY),
3218
- color: !process.env.NO_COLOR,
3219
- width: process.stdout.columns ?? 100,
3220
- dryRun,
3221
- journalPath: statePaths(process.env).journalPath
3222
- };
3351
+ const reportOptions = humanOptions(dryRun);
3223
3352
  const spinner = startSpinner(true);
3224
3353
  let headerEmitted = false;
3225
3354
  let summary;
3226
3355
  const { emit, stream } = heldOutput((line) => spinner.persist(line));
3227
3356
  let arms = 0;
3357
+ let mark = Date.now();
3228
3358
  try {
3229
3359
  summary = reconcile({
3230
3360
  dryRun,
3231
3361
  onPhase: (line) => spinner.label(line),
3232
- onTargetResolved: (target) => {
3233
- emit(updateHeader(target, dryRun));
3362
+ onTargetResolved: face ? void 0 : (target) => {
3363
+ emit(updateHeader(target, dryRun, reportOptions));
3234
3364
  emit("");
3235
3365
  headerEmitted = true;
3236
3366
  },
3237
3367
  onArm: (arm) => {
3368
+ const seconds = (Date.now() - mark) / 1e3;
3369
+ mark = Date.now();
3238
3370
  if (arms++) stream();
3239
- emit(progressiveRow(arm, reportOptions));
3371
+ emit(face ? checklistLine(arm, seconds, reportOptions) : progressiveRow(arm, reportOptions));
3240
3372
  }
3241
3373
  });
3242
3374
  } finally {
@@ -3244,6 +3376,7 @@ function runUpdate(args, json) {
3244
3376
  }
3245
3377
  if (summary.reexec) return { summary, rendered: true };
3246
3378
  stream();
3379
+ if (face) return { summary, rendered: true };
3247
3380
  if (headerEmitted) process.stdout.write(reportFooterLines(summary, reportOptions).join("\n") + "\n");
3248
3381
  else process.stdout.write(formatReconcileReport(summary, reportOptions));
3249
3382
  return { summary, rendered: true };
@@ -3265,13 +3398,17 @@ function main(args = process.argv.slice(2)) {
3265
3398
  return status.failures.length ? 1 : 0;
3266
3399
  }
3267
3400
  if (command === "update") {
3268
- const { summary, rendered } = runUpdate(args, json);
3401
+ const face = openFace(args, json);
3402
+ const { summary, rendered } = runUpdate(args, json, face);
3269
3403
  if (summary.reexec) return handOff(summary.reexec, args);
3270
- if (!rendered) printReconcile(summary, json, args.includes("--dry-run"));
3404
+ if (face) finishFace(summary, null, args.includes("--dry-run"));
3405
+ else if (!rendered) printReconcile(summary, json, args.includes("--dry-run"));
3271
3406
  return summary.exit;
3272
3407
  }
3273
3408
  if (command === "install") {
3274
- const { summary, rendered } = runUpdate(args, json);
3409
+ const face = openFace(args, json);
3410
+ const dryRun = args.includes("--dry-run");
3411
+ const { summary, rendered } = runUpdate(args, json, face);
3275
3412
  if (summary.reexec) {
3276
3413
  const handedOff = handOff(summary.reexec, ["update", ...args.filter((arg) => arg !== "install")]);
3277
3414
  const scheduled = installTask();
@@ -3287,8 +3424,10 @@ function main(args = process.argv.slice(2)) {
3287
3424
  const schedule = installTask();
3288
3425
  if (json) {
3289
3426
  process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
3427
+ } else if (face) {
3428
+ finishFace(summary, schedule, dryRun);
3290
3429
  } else {
3291
- if (!rendered) printReconcile(summary, false, args.includes("--dry-run"));
3430
+ if (!rendered) printReconcile(summary, false, dryRun);
3292
3431
  process.stdout.write(schedule.ok ? ` Auto-update on \u2014 hourly task "MMI Fleet Updater".
3293
3432
  ` : ` Auto-update NOT enabled \u2014 ${schedule.detail}
3294
3433
  `);
@@ -3315,8 +3454,14 @@ if (typeof require !== "undefined" && require.main === module) {
3315
3454
  }
3316
3455
  // Annotate the CommonJS export names for ESM import in node:
3317
3456
  0 && (module.exports = {
3457
+ ACCENT,
3458
+ MM_GREEN,
3459
+ RESET,
3460
+ accent,
3318
3461
  acquireSharedDocLock,
3462
+ brandLine,
3319
3463
  buildWatcherCommand,
3464
+ checklistLine,
3320
3465
  claudeArm,
3321
3466
  claudeHostEvidence,
3322
3467
  clearLaunchTarget,
@@ -3355,9 +3500,12 @@ if (typeof require !== "undefined" && require.main === module) {
3355
3500
  progressiveRow,
3356
3501
  readLaunchTarget,
3357
3502
  readPending,
3503
+ receipt,
3504
+ receiptText,
3358
3505
  reconcile,
3359
3506
  reconcileCursorUserHooks,
3360
3507
  releaseSharedDocLock,
3508
+ reportCounts,
3361
3509
  reportFooterLines,
3362
3510
  retiredSurfaces,
3363
3511
  retryHint,
@@ -3366,6 +3514,7 @@ if (typeof require !== "undefined" && require.main === module) {
3366
3514
  sharedDocLockPath,
3367
3515
  sharedDocLockTimeoutReceipt,
3368
3516
  shouldHandOff,
3517
+ signOff,
3369
3518
  startSpinner,
3370
3519
  surfaceArmPlan,
3371
3520
  systemdServiceUnit,
@@ -3373,6 +3522,7 @@ if (typeof require !== "undefined" && require.main === module) {
3373
3522
  taskXml,
3374
3523
  updateHeader,
3375
3524
  watcherLockPath,
3525
+ welcome,
3376
3526
  withSharedDocLock,
3377
3527
  wrapWords,
3378
3528
  writeLaunchTarget,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.3.68",
3
+ "version": "4.4.1",
4
4
  "description": "Install and maintain the MMI CLI and every present host surface from one release-gated Hub command.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",